--[[
             __________               __   ___.
   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
                     \/            \/     \/    \/            \/
 $Id$

 Port of Chain Reaction (which is based on Boomshine) to Rockbox in Lua.
 See http://www.yvoschaap.com/chainrxn/ and http://www.k2xl.com/games/boomshine/

 Copyright (C) 2009 by Maurus Cuelenaere

 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation; either version 2
 of the License, or (at your option) any later version.

 This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 KIND, either express or implied.

]]--

require "actions"

local CYCLETIME = rb.HZ / 50
local HAS_TOUCHSCREEN = rb.action_get_touchscreen_press ~= nil
local DEFAULT_BALL_SIZE = rb.LCD_HEIGHT > rb.LCD_WIDTH and rb.LCD_WIDTH  / 30
                                                       or  rb.LCD_HEIGHT / 30
local MAX_BALL_SPEED = DEFAULT_BALL_SIZE / 2
local DEFAULT_FOREGROUND_COLOR = rb.lcd_get_foreground ~= nil
                                                     and rb.lcd_get_foreground()
                                                     or  0

local levels = {
            --  {GOAL, TOTAL_BALLS},
                {1,   5},
                {2,  10},
                {4,  15},
                {6,  20},
                {10, 25},
                {15, 30},
                {18, 35},
                {22, 40},
                {30, 45},
                {37, 50},
                {48, 55},
                {55, 60}
           }

local Ball = {
                size = DEFAULT_BALL_SIZE,
                exploded = false,
                implosion = false
             }

function Ball:new(o)
    if o == nil then
        o = {
                x = math.random(self.size, rb.LCD_WIDTH - self.size),
                y = math.random(self.size, rb.LCD_HEIGHT - self.size),
                color = random_color(),
                up_speed = Ball:generateSpeed(),
                right_speed = Ball:generateSpeed(),
                explosion_size = math.random(2*self.size, 4*self.size),
                life_duration = math.random(rb.HZ, rb.HZ*5)
            }
    end

    setmetatable(o, self)
    self.__index = self
    return o
end

function Ball:generateSpeed()
    local speed = math.random(-MAX_BALL_SPEED, MAX_BALL_SPEED)
    if speed == 0 then
        speed = 1       -- Make sure all balls move
    end

    return speed
end

function Ball:draw()
    --[[
         I know these aren't circles, but as there's no current circle
         implementation in Rockbox, rectangles will just do fine (drawing
         circles from within Lua is far too slow).
    ]]--
    set_foreground(self.color)
    rb.lcd_fillrect(self.x, self.y, self.size, self.size)
end

function Ball:step()
    if self.exploded then
        if self.implosion and self.size > 0 then
            self.size = self.size - 2
            self.x = self.x + 1 -- We do this because we want to stay centered
            self.y = self.y + 1
        elseif self.size < self.explosion_size then
            self.size = self.size + 2
            self.x = self.x - 1 -- We do this for the same reasons as above
            self.y = self.y - 1
        end
        return
    end

    self.x = self.x + self.right_speed
    self.y = self.y + self.up_speed
    if (self.x + self.size) >= rb.LCD_WIDTH or self.x <= self.size then
        self.right_speed = self.right_speed * (-1)
    elseif (self.y + self.size) >= rb.LCD_HEIGHT or self.y <= self.size then
        self.up_speed = self.up_speed * (-1)
    end
end

function Ball:checkHit(other)
    local x_dist = math.abs(other.x - self.x)
    local y_dist = math.abs(other.y - self.y)
    local x_size = self.x > other.x and other.size or self.size
    local y_size = self.y > other.y and other.size or self.size

    if (x_dist <= x_size) and (y_dist <= y_size) then
        assert(not self.exploded)
        self.exploded = true
        self.death_time = rb.current_tick() + self.life_duration
        if not other.exploded then
            other.exploded = true
            other.death_time = rb.current_tick() + other.life_duration
        end
        return true
    end

    return false
end

local Cursor = {
                size = DEFAULT_BALL_SIZE*2,
                x = rb.LCD_WIDTH/2,
                y = rb.LCD_HEIGHT/2
             }

function Cursor:new()
    return self
end

function Cursor:do_action(action)
    if action == rb.actions.ACTION_TOUCHSCREEN and HAS_TOUCHSCREEN then
        _, self.x, self.y = rb.action_get_touchscreen_press()
        return true
    elseif action == rb.actions.ACTION_KBD_SELECT then
        return true
    elseif (action == rb.actions.ACTION_KBD_RIGHT) then
        self.x = self.x + self.size
    elseif (action == rb.actions.ACTION_KBD_LEFT) then
        self.x = self.x - self.size
    elseif (action == rb.actions.ACTION_KBD_UP) then
        self.y = self.y - self.size
    elseif (action == rb.actions.ACTION_KBD_DOWN) then
        self.y = self.y + self.size
    end

    if self.x > rb.LCD_WIDTH then
        self.x = 0
    elseif self.x < 0 then
        self.x = rb.LCD_WIDTH
    end

    if self.y > rb.LCD_HEIGHT then
        self.y = 0
    elseif self.y < 0 then
        self.y = rb.LCD_HEIGHT
    end

    return false
end

function Cursor:draw()
    set_foreground(DEFAULT_FOREGROUND_COLOR)

    rb.lcd_hline(self.x - self.size/2, self.x - self.size/4, self.y - self.size/2)
    rb.lcd_hline(self.x + self.size/4, self.x + self.size/2, self.y - self.size/2)
    rb.lcd_hline(self.x - self.size/2, self.x - self.size/4, self.y + self.size/2)
    rb.lcd_hline(self.x + self.size/4, self.x + self.size/2, self.y + self.size/2)
    rb.lcd_vline(self.x - self.size/2, self.y - self.size/2, self.y - self.size/4)
    rb.lcd_vline(self.x - self.size/2, self.y + self.size/4, self.y + self.size/2)
    rb.lcd_vline(self.x + self.size/2, self.y - self.size/2, self.y - self.size/4)
    rb.lcd_vline(self.x + self.size/2, self.y + self.size/4, self.y + self.size/2)

    rb.lcd_hline(self.x - self.size/4, self.x + self.size/4, self.y)
    rb.lcd_vline(self.x, self.y - self.size/4, self.y + self.size/4)
end

function draw_positioned_string(bottom, right, str)
    local _, w, h = rb.font_getstringsize(str, rb.FONT_UI)

    rb.lcd_putsxy((rb.LCD_WIDTH-w)*right, (rb.LCD_HEIGHT-h)*bottom, str)
end

function set_foreground(color)
    if rb.lcd_set_foreground ~= nil then
        rb.lcd_set_foreground(color)
    end
end

function random_color()
    if rb.lcd_rgbpack ~= nil then --color target
        return rb.lcd_rgbpack(math.random(1,255), math.random(1,255), math.random(1,255))
    end

    return math.random(1, rb.LCD_DEPTH)
end

function start_round(level, goal, nrBalls, total)
    local player_added, score, exit, nrExpandedBalls = false, 0, false, 0
    local balls, explodedBalls = {}, {}
    local cursor = Cursor:new()

    -- Initialize the balls
    for _=1,nrBalls do
        table.insert(balls, Ball:new())
    end

    -- Make sure there are no unwanted touchscreen presses
    rb.button_clear_queue()

    while true do
        local endtick = rb.current_tick() + CYCLETIME

        -- Check if the round is over
        if #explodedBalls == 0 and player_added then
            break
        end

        -- Check for actions
        local action = rb.get_action(rb.contexts.CONTEXT_KEYBOARD, 0)
        if(action == rb.actions.ACTION_KBD_ABORT) then
            exit = true
            break
        end
        if not player_added and cursor:do_action(action) then
            local player = Ball:new({
                                x = cursor.x,
                                y = cursor.y,
                                color = DEFAULT_FOREGROUND_COLOR,
                                size = 10,
                                explosion_size = 3*DEFAULT_BALL_SIZE,
                                exploded = true,
                                death_time = rb.current_tick() + rb.HZ * 3
                            })
            table.insert(explodedBalls, player)
            player_added = true
        end

        -- Check for hits
        for i, ball in ipairs(balls) do
            for _, explodedBall in ipairs(explodedBalls) do
                if ball:checkHit(explodedBall) then
                    score = score + 100*level
                    nrExpandedBalls = nrExpandedBalls + 1
                    table.insert(explodedBalls, ball)
                    table.remove(balls, i)
                    break
                end
            end
        end

        -- Check if we're dead yet
        for i, explodedBall in ipairs(explodedBalls) do
            if rb.current_tick() >= explodedBall.death_time then
                if explodedBall.size > 0 then
                    explodedBall.implosion = true -- We should be dying
                else
                    table.remove(explodedBalls, i) -- We're imploded!
                end
            end
        end

        -- Drawing phase
        rb.lcd_clear_display()

        set_foreground(DEFAULT_FOREGROUND_COLOR)
        draw_positioned_string(0, 0, string.format("%d balls expanded", nrExpandedBalls))
        draw_positioned_string(0, 1, string.format("Level %d", level))
        draw_positioned_string(1, 1, string.format("%d level points", score))
        draw_positioned_string(1, 0, string.format("%d total points", total+score))

        for _, ball in ipairs(balls) do
            ball:step()
            ball:draw()
        end

        for _, explodedBall in ipairs(explodedBalls) do
            explodedBall:step()
            explodedBall:draw()
        end

        if not HAS_TOUCHSCREEN and not player_added then
            cursor:draw()
        end

        -- Push framebuffer to the LCD
        rb.lcd_update()

        if rb.current_tick() < endtick then
            rb.sleep(endtick - rb.current_tick())
        else
            rb.yield()
        end
    end

    return exit, score, nrExpandedBalls
end

-- Helper function to display a message
function display_message(...)
    local message = string.format(...)
    local _, w, h = rb.font_getstringsize(message, rb.FONT_UI)
    local x, y = (rb.LCD_WIDTH - w) / 2, (rb.LCD_HEIGHT - h) / 2

    rb.lcd_clear_display()
    set_foreground(DEFAULT_FOREGROUND_COLOR)
    if w > rb.LCD_WIDTH then
        rb.lcd_puts_scroll(x/w, y/h, message)
    else
        rb.lcd_putsxy(x, y, message)
    end
    rb.lcd_update()

    rb.sleep(rb.HZ * 2)

    rb.lcd_stop_scroll() -- Stop our scrolling message
end

if HAS_TOUCHSCREEN then
    rb.touchscreen_set_mode(rb.TOUCHSCREEN_POINT)
end
rb.backlight_force_on()

local idx, highscore = 1, 0
while levels[idx] ~= nil do
    local goal, nrBalls = levels[idx][1], levels[idx][2]

    display_message("Level %d: get %d out of %d balls", idx, goal, nrBalls)

    local exit, score, nrExpandedBalls = start_round(idx, goal, nrBalls, highscore)
    if exit then
        break -- Exiting..
    else
        if nrExpandedBalls >= goal then
            display_message("You won!")
            idx = idx + 1
            highscore = highscore + score
        else
            display_message("You lost!")
        end
    end
end

if idx > #levels then
    display_message("You finished the game with %d points!", highscore)
else
    display_message("You made it till level %d with %d points!", idx, highscore)
end

-- Restore user backlight settings
rb.backlight_use_settings()
