Level = copy_table(BaseView)
Level.definition = {}
Level.available_themes = { "hearts", "clubs", "diamonds", "spades" }
Level.game_type = "klondike"

Level.positions = {
    deck_x = 60,
    deck_y = 70,

    tray_x = 155,
    tray_y = 74,

    home_x = 400,
    home_y = 70,
    home_spacing = 90,

    column_x = 130,
    column_y = 200,
    column_spacing = 90,
    
    stack_spacing = 20,
}

Level.config = {
    decks = 1,
    home_spot_count = 4,
    column_count = 7,
    retry_view = Level,
}

Level.scoring = {
    tray_to_table = 50,
    tray_to_home = 100,
    table_to_home = 100,
    home_to_table = -150,
    reveal = 50,
    time_penalty = -25,
}

--- BASICS -----------------------------------------------------------

function Level:init()
    BaseView.init(self)
    self:start()
    timer_catchup()
end


function Level:destroy()
    BaseView.destroy(self)
end


function Level:update(dt)
    BaseView.update(self, dt)

    if self.level_ended then
        if self.level_end_delay then
            self.level_end_delay = self.level_end_delay - dt
            if self.level_end_delay < 0 then
                self.level_end_delay = nil
                self:start_level_end_effect()
            end
        end

        if self.ending then
            self.ending:update(dt)
            if self.ending:is_finished() then
                self.stats.game_guid = self.game_type
                fw:add_view(Congratulations, self.stats)
            end
        end
    else
        -- timers
        self.move_timer = self.move_timer + dt
        if self.move_timer >= 10 then
            self:add_score(340, 565, self.scoring.time_penalty, true)
            self.move_timer = 0
        end

        local old_time = math.floor(self.stats.time)
        self.stats.time = self.stats.time + dt
        if math.floor(self.stats.time) > old_time then
            self:refresh_ui()
        end

        if self.drag then
            self.drag.card:move_to(mouse.x + self.drag.dx, mouse.y + self.drag.dy)

            if mouse.left_up then
                self:finish_dragging()
            end
        end

        if self.sort_request then
            self.sort_request = nil
            self:sort_objects()
        end
    end
    
    if cheats_enabled then
        if kbd_down("tab") and kbd_struck("f") then
            self:level_completed()
        end

        if dev_mode and kbd_struck("c") then
            fw:add_view(Congratulations, { score = 12000, time = 131, bonus = 3400 })
        end
    end
end



function Level:render()
    BaseView.render(self)
    
    if self.ending then
        local width = self.ending.val.stripe_width
        local alpha = self.ending.val.stripe_alpha
        gfx_render_box(0, 0, width, 200, alpha, 0, "000000")
        gfx_render_box(800 - width, 200, width, 200, alpha, 0, "000000")
        gfx_render_box(0, 400, width, 200, alpha, 0, "000000")
    end
end


function Level:start_dragging(card)
    self.drag = {
        card = card,
        dx = card.x - mouse.x,
        dy = card.y - mouse.y,
        old_state = card.state
    }

    self.drag.old_parent = self:break_chain(card)

    while card do
        card.state = "float"
        card.z = self:get_top_z()
        card = card.has_stack
    end
    self:sort_objects()
end


function Level:finish_dragging()
    local card = self.drag.card
    local best
    local best_dist, dist

    -- check if we the move was valid
    for i, v in ipairs(self.objects) do
        if v.is_spot and not v.has_stack and shapes_overlap(card, v) then
            if v:accepts_stack(card) then
                dist = (card.x - v.x) * (card.x - v.x) + (card.y - v.y) * (card.y - v.y)
                if not best or dist < best_dist then
                    best = v
                    best_dist = dist
                end
            end
        end
    end

    -- check if we the move was valid
    for i, v in ipairs(self.objects) do
        if v.is_card and v ~= card and not v.has_stack and
           (v.state == "table" or v.state == "home") and shapes_overlap(card, v) then
            if v:accepts_stack(card) then
                dist = (card.x - v.x) * (card.x - v.x) + (card.y - v.y) * (card.y - v.y)
                if not best or dist < best_dist then
                    best = v
                    best_dist = dist
                end
            end
        end
    end
    
    local significant_distance
    if self.drag.old_parent then
        significant_distance = self:is_distance_significant(card, self.drag.old_parent.x, self.drag.old_parent.y)
    else
        significant_distance = self:is_distance_significant(card, self.positions.tray_x, self.positions.tray_y)
    end

    if best then
        -- best candidate found, move there
        self:move_stack(card, best)
        self:add_score(card.x, card.y, self:calculate_score(self.drag.old_state, card.state))
        if significant_distance then
            snd_play_sound("card_placed")
        end
    else
        -- move failed, restore to original position
        if self.drag.old_parent then
            self:move_stack(card, self.drag.old_parent)
        else
            card:move_to(self.positions.tray_x, self.positions.tray_y)
            card.state = "tray"
        end
        
        if significant_distance then
            snd_play_sound("fast_move")
        end
    end
    
    self.drag = nil
end


function Level:is_distance_significant(card, dist_x, dist_y)
    return (card.x - dist_x) * (card.x - dist_x) + (card.y - dist_y) * (card.y - dist_y) > card.w * card.w
end


function Level:break_chain(card)
    for i,o in ipairs(self.objects) do
        if o.has_stack == card then
            o.has_stack = nil
            if not o.covered then
                o.highlight_on = true
            end
            return o
        end
    end
end


function Level:move_stack(card, dest)
    -- create new chain
    dest.has_stack = card
    if not (dest.is_card and dest.state == "table" and not dest.covered) then
        dest.highlight_on = false
    end
    card.state = dest.state

    local x, y
    x, y = dest:stack_position()
    card:move_to(x, y)

    -- process stack position
    local next = card
    while next do
        next.z = self:get_top_z()
        next.state = dest.state
        next = next.has_stack
    end
    
    self:sort_objects()
    self:check_level_end()
    self:check_tray_empty()
    self:reveal_available_cards()
end


function Level:check_level_end()
    for i, v in ipairs(self.objects) do
        if v.is_card and v.state ~= "home" then
            return
        end
    end
    
    -- we got here? so it must be the end!
    self:level_completed()
end


function Level:level_completed()
    if self.stats.time > 30 then
        if self.game_type == "double_klondike" then
            self.stats.bonus = math.floor(250000000 / (self.stats.time * self.stats.time)) -- for Double Klondike
        else
            self.stats.bonus = math.floor(20000000 / (self.stats.time * self.stats.time)) -- for Klondike
        end
        self:ingame_bonus(str.ingame_extra_bonus, self.stats.bonus, 400, 300)
    end
    self.level_ended = true
    self.level_end_delay = 0.3
end


function Level:check_tray_empty()
    for i,v in ipairs(self.objects) do
        if v.is_card and (v.state == "tray" or v.state == "deck") then
            return
        end
    end

    self.shortcuts.click_to_turn.enabled = false
end


function Level:reveal_available_cards()
    for i,v in ipairs(self.objects) do
        if v.is_card and v.state == "table" and v.covered and not v.has_stack then
            v:reveal()
            self:add_score(v.x, v.y, self.scoring.reveal)
        end
    end
end


--- CARDS ------------------------------------------------------------

function Level:add_deck()
    self.decks_count = self.decks_count + 1

    self.deck = {}
    for suit = 1, 4 do
        for value = 1, 13 do
            table.insert(self.deck, {suit, value})
        end
    end
end


function Level:pop_from_deck()
    if not self.deck or #self.deck <=0 then
        self:add_deck()
    end
    local card = table.remove(self.deck, math.random(#self.deck))
    return card[1], card[2]
end


function Level:get_top_z()
    -- helper to make sure new card lies higher than the previous one
    self.last_card_z = self.last_card_z + 1
    return self.last_card_z
end


function Level:reset_cards()
    for i,o in ipairs(self.objects) do
        if o.is_card then
            o.to_destroy = true
        end
    end
    
    self:prune_objects()
    self.cards_count = 0
    self.decks_count = 0
    self.last_card_z = 100
end


function Level:find_top_card(px, py)
    -- cards are z sorted, so we just find the first matching card,
    -- walking from the end of the list (in decreasing z-order)
    local c
    for i = #self.objects, 1, -1 do
        c = self.objects[i]
        if c.is_card and c:point_inside(px, py) then
            return c
        end
    end
end


--- DECK -------------------------------------------------------------

function Level:select_new_deck_top(old_top)
    -- find card or spot that was below the old top and make it new top
    for i,v in ipairs(self.objects) do
        if v.has_stack == old_top then
            v.has_stack = nil
            v.highlight_on = true
            break
        end
    end
end


function Level:change_tray_top(new_top)
    -- find old top and change it
    for i,v in ipairs(self.objects) do
        if v.is_card and v.has_stack == nil and v.state == "tray" and v ~= new_top then
            v.has_stack = new_top
            v.highlight_on = false
            break
        end
    end
end


function Level:turn_over_deck()
    assert(self.deck_spot.has_stack == nil)
    
    local done = false
    local match = nil
    local prev_card = self.deck_spot
    local delay = 0
    local counter = 0
    while not done do
        done = true
        for i,v in ipairs(self.objects) do
            if v.is_card and v.has_stack == match and v.state == "tray" then
                prev_card.has_stack = v
                prev_card = v
                v.has_stack = nil
                match = v
                done = false
                v:send_to_deck(self.positions.deck_x + math.floor(counter/6) * 4, self.positions.deck_y + math.floor(counter/6), delay)
                delay = delay + 0.07
                counter = counter + 1
                break
            end
        end
    end
    
    if match then
        match.highlight_on = true
    end
end


--- LOAD/SAVE --------------------------------------------------------

function Level:start()
    self.stats = {
        time = 0,
        score = 0,
        bonus = 0,
    }
    self.move_timer = 0
    
    self:reset_cards()
    self:release_themed_bitmaps()
    self.theme = self.available_themes[math.random(1, #self.available_themes)]
    self.card_back_sprite = "back"..math.random(3)

    local def, s

    def = { type = "Bitmap",
        bitmap = "themes/"..self.theme.."/tray",
        sprite = "main",
        x=self.positions.deck_x, y=self.positions.deck_y, z=1 }
    s = Spot:new(def, self)
    s.state = "deck"
    self:add_object(s)
    self.deck_spot = s
    
    def = { type = "Bitmap",
        bitmap = "gfx/click_to_turn",
        sprite = "main",
        shortcut = "click_to_turn",
        x=self.positions.deck_x, y=self.positions.deck_y, z=2 }
    self:create_and_add_object(def)


    def = { type = "Bitmap",
        bitmap = "themes/"..self.theme.."/tray_double",
        sprite = "main",
        x = self.positions.home_x,
        y = self.positions.home_y,
        z = 1,
        area = { x0 = 33, y0 = 17, x1 = 97, y1 = 110 }
    }
    self.home_spot = {}
    for i = 1, self.config.home_spot_count do
        s = Spot:new(def, self)
        s.state = "home"
        self:add_object(s)
        self.home_spot[i] = s
        def.x = def.x + self.positions.home_spacing
    end
    

    def = {
            type = "Text",
            shortcut = "score_info",
            font = "hud18",
            text = str.level_score,
            x=260, y=573, z=10001,
        }
    self:create_and_add_object(def)

    def = {
            type = "Text",
            shortcut = "time_info",
            font = "hud18",
            text = str.level_time,
            align = "center",
            x=156, y=573, z=10001,
        }
    self:create_and_add_object(def)
    
    def = {
        {
            type = "Bitmap",
            bitmap = "themes/"..self.theme.."/panel",
            sprite = "main",
            x=0, y=600, z=1000
        },

        {
            type = "LabeledButton",
            bitmap_hi = "themes/"..self.theme.."/button_menu",
            text = str.panel_menu,
            text_shift_x = 50, text_shift_y = 26,
            mouse_click = "menu_button",
            x=8, y=554, z=10001, w=100, h=46, cx=0, cy=0
        },

        {
            type = "LabeledButton",
            shortcut = "retry_button",
            bitmap_hi = "themes/"..self.theme.."/button_retry",
            text = "Retry",
            text_shift_x = 65, text_shift_y = 26,
            mouse_click = "retry_button",
            x=662, y=554, z=10001, w=131, h=46, cx=0, cy=0
        },

        {
            type = "LabeledButton",
            shortcut = "help_button",
            bitmap_hi = "themes/"..self.theme.."/button_shuffle",
            text = "Help",
            text_shift_x = 67, text_shift_y = 26,
            mouse_click = "help_button",
            x=521, y=554, z=10001, w=135, h=46, cx=0, cy=0
        },

        {
            type = "LabeledButton",
            shortcut = "pause_button",
            bitmap_hi = "themes/"..self.theme.."/button_undo",
            text = "Pause",
            text_shift_x = 61, text_shift_y = 26,
            mouse_click = "pause_button",
            x=392, y=554, z=10001, w=122, h=46, cx=0, cy=0
        },
    }

    for k,v in pairs(def) do
        o = self:create_and_add_object(v, false)
        o.is_themed_bitmap = true
    end


    self:load_themed_bitmaps()
    self:setup_cards()
    self:setup_deck()
    self:refresh_ui()
    
    snd_play_music("music/"..self.theme..".ogg")
end


function Level:setup_cards()
    local def, s
    def = { type = "Bitmap",
        bitmap = "themes/"..self.theme.."/tray_double",
        --bitmap = "gfx/card_shade",
        sprite = "main",
        x = self.positions.column_x, y = self.positions.column_y, z=1,
        area = { x0 = 33, y0 = 17, x1 = 97, y1 = 110 }
    }
    self.base_spot = {}

    local suit, value
    local prev_card
    for col = 0, self.config.column_count - 1 do
    
        s = Spot:new(def, self)
        s.is_base = true
        self:add_object(s)
        self.base_spot[col + 1] = s
        s.state = "table"
        def.x = def.x + self.positions.column_spacing

        prev_card = s
        for row = 0, self.config.column_count - 1 do
            if row <= col then
                suit, value = self:pop_from_deck()
                local c = Card:new(self, suit, value, self.positions.column_x + col * self.positions.column_spacing, self.positions.column_y + row * self.positions.stack_spacing, 0)
                c.state = "table"
                c.z = self:get_top_z()
                if prev_card then
                    prev_card.has_stack = c
                    if prev_card.is_card then
                        prev_card:cover()
                    end
                end
                prev_card = c
                self:add_object(c)
            end
        end
    end
end


function Level:setup_deck()
    local last = #self.deck
    local suit, value
    local prev_card = self.deck_spot
    
    if self.config.decks == 2 then
        last = last + 52
    end

    for i = 1, last do
        suit, value = self:pop_from_deck()
        local c = Card:new(self, suit, value, self.positions.deck_x + math.floor(i/6) * 4, self.positions.deck_y + math.floor(i/6), 0)
        c.state = "deck"
        c:cover()
        c.z = self:get_top_z()
        self:add_object(c)

        if prev_card then
            prev_card.has_stack = c
        end
        prev_card = c

        if i == last then
            c.highlight_on = true
        end
    end
end


function Level:load_themed_bitmaps()
    local def, o

    def = { type = "Bitmap",
        bitmap = "themes/"..self.theme.."/bkg"..math.random(1,4),
        x=0, y=0, z=0 }
    if self.game_type == "double_klondike" then
        def.bitmap = def.bitmap.."_d"
    end
    o = self:create_and_add_object(def, false)
    o.is_themed_bitmap = true

    self:sort_objects()
end


function Level:release_themed_bitmaps()
    for i,o in ipairs(self.objects) do
        if o.is_themed_bitmap then
            o.to_destroy = true
        end
    end
    self:prune_objects()
end


function Level:fast_move_home(card)
    if card.has_stack then
        return false
    end
    
    local best
    local best_dist, dist

    -- check if we the move was valid
    for i, v in ipairs(self.objects) do
        if (v.is_card or v.is_spot) and v.state == "home" and not v.has_stack then
            if v:accepts_stack(card) then
                dist = (card.x - v.x) * (card.x - v.x) + (card.y - v.y) * (card.y - v.y)
                if not best or dist < best_dist then
                    best = v
                    best_dist = dist
                end
            end
        end
    end

    if best then
        local old_state = card.state
        self:break_chain(card)
        self:move_stack(card, best)
        self:add_score(best.x, best.y, self:calculate_score(old_state, card.state))
        return true
    end

    return false
end


function Level:start_level_end_effect()
    delay = 0.0
    for i = #self.objects, 1, -1 do
        if self.objects[i].is_card then
            self.objects[i]:shuffle_away(delay)
            delay = delay + 2.4 / 52
        end
    end

    self.ending = Animator:new()
    self.ending:key("stripe_width", {{0, 0}, {2, 0}, {4.0, 800}} )
    self.ending:key("stripe_alpha", {{0, 0}, {2, 0}, {4.0, 0.75}} )

    snd_play_sound("shuffle")
end


function Level:add_score(x, y, score, skip_effect)
    self.move_timer = 0 -- reset penalty timer, even if score is 0 (the move still counts)
    if score == 0 then return end
    
    self.stats.score = self.stats.score + score
    self:refresh_ui()

    if not skip_effect then
        local fx = load_combo_fx("score_pop")
        if score >= 0 then
            fx.objects.score.text = "+"..score
        else
            fx.objects.score.text = score
        end
        create_combo_fx(self, fx, x, y, 1000)

        snd_play_sound("score_add")
    end

end


function Level:calculate_score(old_state, new_state)
    if old_state == "tray" and new_state == "table" then
        return self.scoring.tray_to_table
    elseif old_state == "tray" and new_state == "home" then
        return self.scoring.tray_to_home
    elseif old_state == "table" and new_state == "home" then
        return self.scoring.table_to_home
    elseif old_state == "home" and new_state == "table" then
        return self.scoring.home_to_table
    end
    return 0
end


function Level:ingame_bonus(text, score, x, y)
    local fx = load_combo_fx("ingame_bonus")

    fx.objects.text.text = text
    if score and score ~= 0 then
        fx.objects.score.text = "+"..score
        self.stats.score = self.stats.score + score
        self:refresh_ui()
    else
        fx.objects.score.text = ""
    end

    create_combo_fx(self, fx, x, y, 1000)
    snd_play_sound("ingame_bonus")
end


function Level:refresh_ui()
    local seconds = math.floor(self.stats.time) % 60
    local minutes = math.floor(self.stats.time / 60)
    self.shortcuts.time_info:set_text(string.format(str.level_time, minutes, seconds))
    self.shortcuts.score_info:set_text(string.format(str.level_score, self.stats.score))
end


function Level:menu_button()
    fw:add_view(IngameOptions)
end


function Level:pause_button()
    if not self.level_ended then
        fw:add_view(Paused)
    end
end


function Level:help_button()
    if not self.level_ended then
        fw:add_view(Help)
    end
end


function Level:retry_button()
    if not self.level_ended then
        fw:change_view(self.config.retry_view)
    end
end
