#include <stdint.h>
#include <string.h>

#include "ubox.h"
#include "spman.h"
#include "mplayer.h"
#include "ap.h"

#include "helpers.h"
#include "main.h"

#define LOCAL
#include "game.h"

// generated
#include "map.h"
#include "player.h"
#include "enemy.h"

void init_map_entities()
{
    const uint8_t *m = cur_map;
    uint8_t typ, last = 0;
    uint16_t i;

    // init sprite and patterns
    spman_init();

    // this sets everything to 0, which is useful as
    // entity ET_UNUSED is 0
    memset(entities, 0, sizeof(struct entity) * MAX_ENTITIES);

    // get to the beginning of the entities:
    // map size + 3 bytes of header (the map size and the entities size)
    m += (uint16_t)(m[0] | m[1] << 8) + 3;

    // the entity list ends with 255
    while (*m != 0xff)
    {
        // first byte is type + direction flag on MSB
        // remove MSB
        typ = m[0] & (~DIR_FLAG);

        entities[last].type = typ;
        entities[last].x = m[1];
        entities[last].y = m[2];
        // in the map: param is 1 (int) to look left
        entities[last].dir = m[0] & DIR_FLAG ? DIR_LEFT : DIR_RIGHT;

        switch (typ)
        {
            // can be only one; always first entity
            // because our entities are sorted by type!
            case ET_PLAYER:
                // 3 frames x 2 sprites = 6
                entities[last].pat = spman_alloc_pat(PAT_PLAYER, player_sprite[0], 6, 0);
                spman_alloc_pat(PAT_PLAYER_FLIP, player_sprite[0], 6, 1);
                entities[last].update = update_player;
                break;

            case ET_ENEMY:
                // 3 frames
                entities[last].pat = spman_alloc_pat(PAT_ENEMY, enemy_sprite[0], 3, 0);
                spman_alloc_pat(PAT_ENEMY_FLIP, enemy_sprite[0], 3, 1);
                entities[last].update = update_enemy;
                break;
        }

        // next entity
        last++;

        // all our entities are 3 bytes
        m += 3;
    }

    // count how many batteries are in the map
    batteries = 0;
    for (i = 0; i < MAP_W * MAP_H; ++i)
        if (cur_map_data[i] == BATTERY_TILE)
            batteries++;

}

void draw_map()
{
    // draw the map!
    //
    //  - is not compressed (which limits how many maps we can store)
    //  - our map is just the tile numbers
    //
    // so we do it in one call by coping our map to the backtround tile map
    // addr in the VDP VRAM
    ubox_wait_vsync();
    ubox_write_vm((uint8_t *)0x1800, MAP_W * MAP_H, cur_map_data);
}

void draw_hud()
{
    uint8_t i;

    put_text(0, 21, "LIVES");

    for (i = 0; i < MAX_LIVES; ++i)
        if (i < lives)
            // our hearts tile
            ubox_put_tile(1 + i, 22, 193);
        else
            ubox_put_tile(1 + i, 22, WHITESPACE_TILE);
}

// x and y in pixels
void erase_battery(uint8_t x, uint8_t y)
{
    uint8_t t;
    int8_t mod;

    // find out the bg tile to use

    // border of the map
    if ((x >> 3) == 0)
        mod = 1;
    else
        mod = -1;

    switch (cur_map_data[mod + (x >> 3) + (y >> 3) * MAP_W])
    {
        case 12:
            t = 13;
            break;
        case 13:
            t = 12;
            break;
        default:
            // this is next to a wall
            if (mod == 1)
                t = 13;
            else
                t = 12;
            break;
    }

    // change the map data so we don't pick it up again
    cur_map_data[(x >> 3) + (y >> 3) * MAP_W] = t;

    // erase on the screen
    ubox_put_tile(x >> 3, y >> 3, t);
    ubox_put_tile(x >> 3, (y >> 3) - 1, t);
}

// x and y in pixels
uint8_t is_map_blocked(uint8_t x, uint8_t y)
{
    return cur_map_data[(x >> 3) + (y >> 3) * MAP_W] < LAST_SOLID_TILE + 1;
}

// x and y in pixels
uint8_t is_map_battery(uint8_t x, uint8_t y)
{
    return cur_map_data[(x >> 3) + (y >> 3) * MAP_W] == BATTERY_TILE;
}

// x and y in pixels; always check the bottom tile!
uint8_t is_map_elevator_down(uint8_t x, uint8_t y)
{
    uint8_t t = cur_map_data[(x >> 3) + (y >> 3) * MAP_W];

    // first check the elevator platform comparing tiles
    if (t != 9 && t != 10)
        return 0;

    // then check the elevator tube comparing tiles
    t = cur_map_data[(x >> 3) + ((y >> 3) - 1) * MAP_W];
    return (t != 14 && t != 15);
}

// x and y in pixels; always check the bottom tile!
uint8_t is_map_elevator_up(uint8_t x, uint8_t y)
{
    uint8_t t = cur_map_data[(x >> 3) + (y >> 3) * MAP_W];

    // first check the elevator platform comparing tiles
    if (t != 9 && t != 10)
        return 0;

    // then check the elevator tube comparing tiles
    t = cur_map_data[(x >> 3) + ((y >> 3) - 1) * MAP_W];
    return (t == 14 || t == 15);
}

void update_enemy()
{
    // check for the player; if alive and not invulnerable!
    // we use small hit boxes
    if (lives && !invuln
            && entities[0].x + 6 < self->x + 10 && self->x + 6 < entities[0].x + 10
            && self->y == entities[0].y)
    {
        // change direction
        self->dir ^= 1;

        // remove one life (is more like "hits")
        lives--;
        draw_hud();
        invuln = INVUL_TIME;

        if (!lives)
        {
            // different sound effects if is game over
            mplayer_init(SONG, SONG_SILENCE);
            mplayer_play_effect_p(EFX_DEAD, EFX_CHAN_NO, 0);
            gameover_delay = GAMEOVER_DELAY;
        }
        else
            mplayer_play_effect_p(EFX_HIT, EFX_CHAN_NO, 0);
    }

    // left or right?
    if (self->dir)
    {
        // change direction
        if (self->x == 2 || is_map_blocked(self->x, self->y + 15))
            self->dir ^= 1;
        else
            self->x -= 1;
    }
    else
    {
        // change direction
        if (self->x == 255 - 16 || is_map_blocked(self->x + 15, self->y + 15))
            self->dir ^= 1;
        else
            self->x += 1;
    }

    // update the walking animation
    if (self->delay++ == FRAME_WAIT)
    {
        self->delay = 0;
        if (++self->frame == WALK_CYCLE)
            self->frame = 0;
    }

    // allocate the sprites
    sp.x = self->x;
    // y on the screen starts in 255
    sp.y = self->y - 1;
    // find which pattern to show
    sp.pattern = self->pat + (walk_frames[self->frame] + self->dir * 3) * 4;
    // red
    sp.attr = 9;
    spman_alloc_sprite(&sp);
}

void update_player()
{
    // to know if we need to update the walk animation
    uint8_t moved = 0;

    // player is dead
    if (!lives)
        return;

    // decrease counter if set
    if (invuln)
        invuln--;

    if (control & UBOX_MSX_CTL_RIGHT)
    {
        self->dir = DIR_RIGHT;
        moved = 1;

        // wrap horizontally
        if (self->x == 255 - 16)
            self->x = 0;
        // check if not solid, using bottom right
        else if (!is_map_blocked(self->x + 15, self->y + 15))
            self->x += 2;
    }

    if (control & UBOX_MSX_CTL_LEFT)
    {
        self->dir = DIR_LEFT;
        moved = 1;

        // wrap horizontally
        if (self->x == 2)
            self->x = (uint8_t)(255 - 16);
        // check if not solid, using bottom left
        else if (!is_map_blocked(self->x, self->y + 15))
            self->x -= 2;
    }

    // are we touching a battery?
    // use bottom center
    if (is_map_battery(self->x + 8, self->y + 15))
    {
        mplayer_play_effect_p(EFX_BATTERY, EFX_CHAN_NO, 0);
        batteries--;
        erase_battery(self->x + 8, self->y + 15);
    }

    if (control & UBOX_MSX_CTL_FIRE1)
    {
        // use flags to prevent repeat: the player will
        // have to release fire to use the elevator again
        if (!self->flags)
        {
            self->flags = 1;

            // check elevator down; using bottom middle
            if (is_map_elevator_down(self->x + 8, self->y + 16))
            {
                mplayer_play_effect_p(EFX_ELEVATOR, EFX_CHAN_NO, 0);
                self->y += 8 * 4;
            }
            // then elevator up; using bottom middle
            else if (is_map_elevator_up(self->x + 8, self->y + 16))
            {
                mplayer_play_effect_p(EFX_ELEVATOR, EFX_CHAN_NO, 0);
                self->y -= 8 * 4;
            }
        }
    }
    else
    {
        if (self->flags)
            self->flags = 0;
    }

    // it moved, or at least pushed against a wall
    if (moved)
    {
        // update the walking animation
        if (self->delay++ == FRAME_WAIT)
        {
            self->delay = 0;
            if (++self->frame == WALK_CYCLE)
                self->frame = 0;
        }
    }
    else
    {
        // just stand
        if (self->frame)
        {
            self->frame = 0;
            self->delay = 0;
        }
    }

    // if we are invulnerable, don't draw odd frames
    // and we get a nice blinking effect
    if (invuln & 1)
        return;

    // allocate the player sprites; fixed so they never flicker
    sp.x = self->x;
    // y on the screen starts in 255
    sp.y = self->y - 1;
    // find which pattern to show
    sp.pattern = self->pat + (walk_frames[self->frame] + self->dir * 3) * 8;
    // green
    sp.attr = 12;
    spman_alloc_fixed_sprite(&sp);
    // second one is 4 patterns away (16x16 sprites)
    sp.pattern += 4;
    // white
    sp.attr = 15;
    spman_alloc_fixed_sprite(&sp);
}

void run_game()
{
    uint8_t i;

    // init some variables; look at game.h for description
    lives = MAX_LIVES;
    invuln = 0;
    gameover_delay = 0;

    ubox_disable_screen();

    ubox_fill_screen(WHITESPACE_TILE);

    // we only have one map, select it
    cur_map = map[0];
    // uncompress map data into RAM, we will modify it
    // map data starts on byte 3 (skip map data size and entities size)
    ap_uncompress(cur_map_data, cur_map + 3);

    // init entities before drawing
    init_map_entities();
    draw_map();

    draw_hud();

    ubox_enable_screen();

    mplayer_init(SONG, SONG_IN_GAME);

    // our game loop
    while (1)
    {
        // exit the game
        if (ubox_read_keys(7) == UBOX_MSX_KEY_ESC)
            break;

        // game completed!
        if (!batteries)
            break;

        // we are in the gameover delay
        if (gameover_delay)
        {
            // if finished, exit
            if (--gameover_delay == 0)
                break;
        }

        // read the selected control
        control = ubox_read_ctl(ctl);

        // update all the entities:
        // - self is a pointer to THIS entity
        // - because we don't create/destroy entities dynamically
        //   when we found one that is unused we are done
        for (i = 0, self = entities; i < MAX_ENTITIES && self->type; i++, self++)
            self->update();

        // ensure we wait to our desired update rate
        ubox_wait();
        // update sprites on screen
        spman_update();
    }

    // stop the in game music
    mplayer_init(SONG, SONG_IN_GAME);
    // hide all the sprites before going back to the menu
    spman_hide_all_sprites();
}