#include "Player.h"

// Global player object.
Player player;

// The position of the player body colour in the palette.
u8 playerPalettePosition_ = PALETTE_POSITION_BRIGHT_WHITE;

// The current environment flags set in the player.
u8 currentEnvironmentFlags = 0;

void initPlayer(u8 x, u8 y)
{
    // Initialise actor variables.
    initActor((Actor*)&player, x, y);

    // The current room is the room that the player is in.
    currentRoom = player.object.position.room;
    currentRoomX = currentRoom % 2;
    currentRoomY = currentRoom / 2;

    // Default to health set by difficulty.
    player.health = playerStartingHealth;

    // Default the player palette position to bright white with the possibility of using the secondary
    // bright white with the player at a later time.
    playerPalettePosition_ = PALETTE_POSITION_BRIGHT_WHITE;

    // By default the first set of sprites are drawn.
    player.paletteOffset = 0;

    // Reset environment flags used to determine if player is on spikes.
    currentEnvironmentFlags = 0;
}

void drawPlayer()
{
    if (player.redraw)
    {
#ifdef CPC
        // Draws the player as an actor using the backbuffer.
        drawActor((Actor*)&player, playerSprites[player.direction + player.paletteOffset]);
#else
        drawActorAllegro((Actor*)&player, allegroPlayerSprites[player.direction + player.paletteOffset]);
#endif

        // Set redraw to false ready for the next change when it will be set to true.
        player.redraw = FALSE;
    }
}

void movePlayer()
{
    // Update the player velocity based on controls currently down.
    if (gameControls.controlFlags & CONTROL_LEFT_FLAG)
    {
        player.object.velocity.x = -1;
        player.direction = DIRECTION_LEFT;
    }

    if (gameControls.controlFlags & CONTROL_RIGHT_FLAG)
    {
        player.object.velocity.x = 1;
        player.direction = DIRECTION_RIGHT;
    }

    if (gameControls.controlFlags & CONTROL_UP_FLAG)
    {
        player.object.velocity.y = -2;
        player.direction = DIRECTION_UP;
    }

    if (gameControls.controlFlags & CONTROL_DOWN_FLAG)
    {
        player.object.velocity.y = 2;
        player.direction = DIRECTION_DOWN;
    }
}

void logicPlayer()
{
    // The tiles that the player currently occupies.
    u8 occupiedXTileLeft = (player.object.position.x) >> 2;
    u8 occupiedXTileRight = (player.object.position.x + TILE_WIDTH_BYTES - 1);
    u8 occupiedYTileBottom = (player.object.position.y + TILE_HEIGHT - 1);
    u8 occupiedYTileTop = (player.object.position.y) >> 4;
    occupiedXTileRight >>= 2;
    occupiedYTileBottom >>= 4;

    // Clear environment flags before it is set again in the updatePlayerEnvironmentFlags function.
    player.environmentFlags = 0;

    // The action key is activated by the player when they want to use an interactable.
    if (!actionKeyFlag && gameControls.controlFlags & CONTROL_ACTION_FLAG)
    {
        // Activate any interactible in one of the squares the player currently occupies.
        u8 interactableActivated = activateInteractable(occupiedXTileLeft, occupiedYTileTop, currentRoomX, currentRoomY, 0);

        // Set action key flag to ensure the action key cannot be held down.
        // This prevents an interactable being activated continuously with the action key.
        actionKeyFlag = TRUE;

        // Check that the cells are not the same otherwise we get multiple activations.
        // Check that the player has not changed rooms (teleported) since the interactable in the previous cell was activated.
        // If this is not done you could activate an interactable where the player was instead of where the player now is.
        if (occupiedYTileTop != occupiedYTileBottom && !roomChange)
        {
            interactableActivated += activateInteractable(occupiedXTileLeft, occupiedYTileBottom, currentRoomX, currentRoomY, 0);
        }

        if (occupiedXTileLeft != occupiedXTileRight && !roomChange)
        {
            interactableActivated += activateInteractable(occupiedXTileRight, occupiedYTileTop, currentRoomX, currentRoomY, 0);

            if (occupiedYTileTop != occupiedYTileBottom && !roomChange)
            {
                interactableActivated += activateInteractable(occupiedXTileRight, occupiedYTileBottom, currentRoomX, currentRoomY, 0);
            }
        }
    }

    // Save player current position before updating position based on velocity.
    // If you do this after checking velocity it results in the player only being partially drawn (or not at all) on screen.
    player.object.previousPosition.x = player.object.position.x;
    player.object.previousPosition.y = player.object.position.y;

    // Check the player is moving (the block could set velocity to 0).
    if (player.object.velocity.y || player.object.velocity.x)
    {
        // The tiles the player occupies following a change in velocity.
        u8 futureXTileLeft = (player.object.position.x + player.object.velocity.x) >> 2;
        u8 futureXTileRight = (player.object.position.x + TILE_WIDTH_BYTES - 1 + player.object.velocity.x);
        u8 futureYTileBottom = (player.object.position.y + TILE_HEIGHT - 1 + player.object.velocity.y);
        u8 futureYTileTop = (player.object.position.y + player.object.velocity.y) >> 4;
        futureXTileRight >>= 2;
        futureYTileBottom >>= 4;

        if (player.object.velocity.x > 0)
        {
            // Player going right.
            if (player.object.position.x + player.object.velocity.x <= SCREEN_X - TILE_WIDTH_BYTES)
            {
                if (processCell(futureXTileRight, occupiedYTileTop) == CELL_EMPTY &&
                    processCell(futureXTileRight, occupiedYTileBottom) == CELL_EMPTY)
                {
                    player.object.position.x += player.object.velocity.x;
                }
            }
            else
            {
                // Check that there is a room on the right (not in room 1).
                if (currentRoomX < 1)
                {
                    // Check that the first cell on the room to the right is empty.
                    if (processCellInRoom(0, occupiedYTileTop, currentRoomX + 1, currentRoomY) == CELL_EMPTY &&
                        processCellInRoom(0, occupiedYTileBottom, currentRoomX + 1, currentRoomY) == CELL_EMPTY)
                    {
                        // Update player position for the new room.
                        moveRoomX(0, 1);
                    }
                }
            }
        }
        else if (player.object.velocity.x < 0)
        {
            // Player going left.
            if (player.object.position.x + player.object.velocity.x >= 0)
            {
                if (processCell(futureXTileLeft, occupiedYTileTop) == CELL_EMPTY &&
                    processCell(futureXTileLeft, occupiedYTileBottom) == CELL_EMPTY)
                {
                    player.object.position.x += player.object.velocity.x;
                }
            }
            else
            {
                // Check that there is a room on the left (not in room 0).
                if (currentRoomX)
                {
                    // Check that the last cell on the room to the left is empty.
                    if (processCellInRoom(SCREEN_TILES_X - 1, occupiedYTileTop, currentRoomX - 1, currentRoomY) == CELL_EMPTY &&
                        processCellInRoom(SCREEN_TILES_X - 1, occupiedYTileBottom, currentRoomX - 1, currentRoomY) == CELL_EMPTY)
                    {
                        // Update player position for the new room.
                        moveRoomX(SCREEN_X - TILE_WIDTH_BYTES, -1);
                    }
                }
            }
        }

        // Update the occupied position with the new x position values.
        // This prevents the player from clipping through solid cells.
        occupiedXTileLeft = (player.object.position.x) >> 2;
        occupiedXTileRight = (player.object.position.x + TILE_WIDTH_BYTES - 1);
        occupiedXTileRight >>= 2;

        if (player.object.velocity.y > 0)
        {
            // Player going down.
            // Check whether player next position below is on screen.
            if (player.object.position.y + player.object.velocity.y <= SCREEN_Y_NO_HUD - TILE_HEIGHT)
            {
                if (processCell(occupiedXTileLeft, futureYTileBottom) == CELL_EMPTY &&
                    processCell(occupiedXTileRight, futureYTileBottom) == CELL_EMPTY)
                {
                    player.object.position.y += player.object.velocity.y;
                }
            }
            else
            {
                // Check that the first cell in the room below is empty.
                if (currentRoomY != 1 &&
                    processCellInRoom(occupiedXTileLeft, 0, currentRoomX, currentRoomY + 1) == CELL_EMPTY &&
                    processCellInRoom(occupiedXTileRight, 0, currentRoomX, currentRoomY + 1) == CELL_EMPTY)
                {
                    // Move the player to the room below the current one.
                    moveRoomBelow();
                }
            }
        }
        else if (player.object.velocity.y < 0)
        {
            // Going up.
            // Check whether player next position above is on screen.
            if (player.object.position.y + player.object.velocity.y >= 0)
            {
                if (processCell(occupiedXTileLeft, futureYTileTop) == CELL_EMPTY && processCell(occupiedXTileRight, futureYTileTop) == CELL_EMPTY)
                {
                    player.object.position.y += player.object.velocity.y;
                }
            }
            else
            {
                // Check that the last cell in the room above is empty.
                if (currentRoomY != 0 &&
                    processCellInRoom(occupiedXTileLeft, SCREEN_TILES_Y - 1, currentRoomX, currentRoomY - 1) == CELL_EMPTY &&
                    processCellInRoom(occupiedXTileRight, SCREEN_TILES_Y - 1, currentRoomX, currentRoomY - 1) == CELL_EMPTY)
                {
                    // Moves the player to the room above the current room.
                    moveRoomAbove();
                }
            }
        }

        // Reset velocity after movement.
        player.object.velocity.y = 0;
        player.object.velocity.x = 0;
    }

    // Redraw the player at the new position if they have moved.
    if (player.object.position.x != player.object.previousPosition.x || player.object.position.y != player.object.previousPosition.y)
    {
        player.redraw = TRUE;

        // Applies the environment flags from the cells (if they are environment cells) that the player is currently occupying.
        // Performing this here instead of in processCell prevents environment flags being incorrectly applied to the player.
        // The problem there was that one cell could apply a flag only for that cell to be impassable due to a different cell blocking the player.
        // The consequence of this was that the player could be unfairly damaged\killed by a cell that they physically could not touch.
        // This also handles mine detection and triggering.
        updatePlayerEnvironmentFlags();

        // Check if the player has moved onto a spike (previously on a safe surface).
        // As a side effect of this you can continue on the spike as long as you want without taking more damage.
        // This means that the invulnerability timer is not required.
        if ((currentEnvironmentFlags ^ player.environmentFlags) & PLAYER_ENVIRONMENT_SPIKE)
        {
            if (player.environmentFlags & PLAYER_ENVIRONMENT_SPIKE)
            {
                // Player touching a spike so will lose health.
                playerLoseHealth(spikeDamageAmount);
            }
        }

        // Save the environment flag from the player for the next iteration of the logic.
        // The currentEnvironmentFlag_ must only be set when the player is moved otherwise
        // it will incorrectly detect that the player has moved off a surface.
        currentEnvironmentFlags = player.environmentFlags;
    }

    // Player occupies only chasm cells then they die.
    // If there is a room change we want the map to redraw before the player dies.
    // If the player moves fast they might get over the chasm but this is an acceptable compromise.
    if (!roomChange && player.environmentFlags)
    {
        if (actorOccuppyingCells((Actor*)&player, CELL_ENVIRONMENT_FLAG | ENVIRONMENT_CHASM) == 4)
        {
            // Player has fallen down a chasm and loses all health.
            playerLoseHealth(playerStartingHealth);
        }

        // Reset the environment cells flag once we are done.
        player.environmentFlags &= ~PLAYER_ENVIRONMENT_CHASM;
    }

    if (roomChange)
    {
        // Map needs to be redrawn on room change.
        mapRedraw = TRUE;

        // Player needs to be redrawn on room change.
        player.redraw = TRUE;

        roomChange = FALSE;
    }
}

u8 processCell(u8 x, u8 y)
{
    return processCellInRoom(x, y, currentRoomX, currentRoomY);
}

u8 processCellInRoom(u8 x, u8 y, u8 roomX, u8 roomY)
{
    u8 cell = mapCell(x, y, roomX, roomY);
    u8 cellType = cell & CELL_GFX_RANGE;

    // Check what type of cell it is.
    if (cell < CELL_ITEM_FLAG)
    {
        // Check if the wall tile type matches the player current body colour.
        if (matchWallToBodyColourPalette(cellType))
        {
            return 0;
        }
    }
    else if (cell & CELL_ITEM_FLAG)
    {
        // Check if the player is already carrying the item.
        if (inventoryContainsItem(cellType))
        {
            // Returning a 1 is equivalent to a tile (removing item flag) so that the player cannot pickup the item matching the one in the inventory.
            return 1;
        }

        if (cellType == ITEM_PAINT_BLUE)
        {
            paintPlayer(PALETTE_POSITION_BRIGHT_BLUE);
        }
        else if (cellType == ITEM_PAINT_YELLOW)
        {
            paintPlayer(PALETTE_POSITION_BRIGHT_YELLOW);
        }
        else if (cellType == ITEM_PAINT_GREEN)
        {
            paintPlayer(PALETTE_POSITION_GREEN);
        }
        else if (cellType == ITEM_PAINT_RED)
        {
            paintPlayer(PALETTE_POSITION_BRIGHT_RED);
        }
        else if (cellType == ITEM_PAINT_WHITE)
        {
            paintPlayer(PALETTE_POSITION_BRIGHT_WHITE);
        }
        else if (cellType == ITEM_PAINT_RESET)
        {
            // Restore offset back to default.
            player.paletteOffset = 0;

            // Restore palette position back to default.
            playerPalettePosition_ = PALETTE_POSITION_BRIGHT_WHITE;
            player.redraw = TRUE;

            // Whatever is set at the bright white position is what the player will get.
            updatePalette();
        }

        // Set map cell to empty when the item is removed from the map grid.
        // Draw the cell after updating to ensure that the item is always removed after pickup.
        updateMapCellRedraw(x, y, roomX, roomY, defaultEnvironmentCell);

        // Add the item to the inventory for all supported items.
        addItemToInventory(cellType);

        // Found an item so add it to the score.
        levelScore += itemScore(cellType);

        // Redraw the hud with the updated score.
        hudRedraw = TRUE;
    }
    else if (cell & CELL_INTERACTABLE_FLAG)
    {
        if (isInteractableReady(cellType))
        {
            if (cellType == INTERACTABLE_DOOR_RED ||
                cellType == INTERACTABLE_DOOR_GREEN ||
                cellType == INTERACTABLE_DOOR_YELLOW)
            {
                // Set map cell to empty so the door interactable is removed from the map grid.
                // Redraw the cell without the door interactable.
                // As soon as the player enters the door the door will disappear
                // if they are carrying a matching key.
                updateMapCellRedraw(x, y, roomX, roomY, defaultEnvironmentCell);

                // Redraw the hud for any inventory changes (such as keys used).
                hudRedraw = TRUE;
            }

            // Most interactables are not solid to enable the player to use them.
            return 0;
        }
    }
    else if (cell & CELL_ENVIRONMENT_FLAG)
    {
        // Detect if the player is standing on a chasm cell.
        if (cellType == ENVIRONMENT_CHASM)
        {
            player.environmentFlags |= PLAYER_ENVIRONMENT_CHASM;
        }
        else if (cellType == ENVIRONMENT_SPIKE_RED)
        {
            player.environmentFlags |= matchSpikeToBodyColourPalette(PALETTE_POSITION_BRIGHT_RED);
        }

        // Environment cells are all non solid by default.
        return 0;
    }

    return cell;
}

void moveRoomX(u8 newX, i8 roomDeltaX)
{
    // Update player position for the new room.
    player.object.position.x = newX;
    player.object.previousPosition.x = newX;

    // Update the current room.
    currentRoom += roomDeltaX;
    currentRoomX += roomDeltaX;
    player.object.position.room = currentRoom;
    player.object.previousPosition.room = currentRoom;
    roomChange = TRUE;
}

void moveRoomBelow()
{
    // Update player position for the new room.
    player.object.position.y = 0;
    player.object.previousPosition.y = 0;

    // Update the current room.
    currentRoom += 2;
    ++currentRoomY;
    player.object.position.room = currentRoom;
    player.object.previousPosition.room = currentRoom;
    roomChange = TRUE;
}

void moveRoomAbove()
{
    // Update player position for the new room above.
    // Using -24 instead of -16 prevents the hud being drawn over by the player sprite.
    player.object.position.y = SCREEN_Y_NO_HUD - TILE_HEIGHT;
    player.object.previousPosition.y = SCREEN_Y_NO_HUD - TILE_HEIGHT;

    // Update the current room.
    currentRoom -= 2;
    --currentRoomY;
    player.object.position.room = currentRoom;
    player.object.previousPosition.room = currentRoom;
    roomChange = TRUE;
}

u8 matchWallToBodyColourPalette(u8 tileType)
{
    u8 tileColourPosition = 0;

    // Determine the position in the colour palette matching the wall colour.
    if (tileType == TILE_WALL1_YELLOW)
    {
        tileColourPosition = PALETTE_POSITION_BRIGHT_YELLOW;
    }
    else if (tileType == TILE_WALL1_BLUE)
    {
        tileColourPosition = PALETTE_POSITION_BRIGHT_BLUE;
    }
    else if (tileType == TILE_WALL1_GREEN)
    {
        tileColourPosition = PALETTE_POSITION_GREEN;
    }
    else if (tileType == TILE_WALL1_LIME)
    {
        tileColourPosition = PALETTE_POSITION_BRIGHT_GREEN;
    }
    else if (tileType == TILE_WALL1_PURPLE)
    {
        tileColourPosition = PALETTE_POSITION_MAGENTA;
    }
    else if (tileType == TILE_WALL1_BROWN)
    {
        tileColourPosition = PALETTE_POSITION_RED;
    }
    else if (tileType == TILE_WALL1_WHITE)
    {
        tileColourPosition = PALETTE_POSITION_BRIGHT_WHITE;
    }
    else if (tileType == TILE_WALL1_RED)
    {
        tileColourPosition = PALETTE_POSITION_BRIGHT_RED;
    }

    if (tileColourPosition)
    {
        // The single colour wall matches the body colour palette.
        if (gamePalette[tileColourPosition] == gamePalette[playerPalettePosition_])
        {
            return TRUE;
        }
    }
    else if (tileType == TILE_WALL1_YELLOW_GREEN)
    {
        // The yellow and green colour wall matches the body colour palette (this only happens after multiple colour changes happen).
        if (gamePalette[PALETTE_POSITION_BRIGHT_YELLOW] == gamePalette[playerPalettePosition_] &&
            gamePalette[PALETTE_POSITION_GREEN] == gamePalette[playerPalettePosition_])
        {
            return TRUE;
        }
    }
    else if (tileType == TILE_WALL1_YELLOW_BLUE)
    {
        // The yellow and blue colour wall matches the body colour palette (this only happens after multiple colour changes happen).
        if (gamePalette[PALETTE_POSITION_BRIGHT_YELLOW] == gamePalette[playerPalettePosition_] &&
            gamePalette[PALETTE_POSITION_BRIGHT_BLUE] == gamePalette[playerPalettePosition_])
        {
            return TRUE;
        }
    }
    else if (tileType == TILE_WALL1_YELLOW_RED)
    {
        // The yellow and red colour wall matches the body colour palette (this only happens after multiple colour changes happen).
        if (gamePalette[PALETTE_POSITION_BRIGHT_YELLOW] == gamePalette[playerPalettePosition_] &&
            gamePalette[PALETTE_POSITION_BRIGHT_RED] == gamePalette[playerPalettePosition_])
        {
            return TRUE;
        }
    }
    else if (tileType == TILE_WALL1_PURPLE_BROWN)
    {
        // The purple and brown colour wall matches the body colour palette (this only happens after multiple colour changes happen).
        if (gamePalette[PALETTE_POSITION_MAGENTA] == gamePalette[playerPalettePosition_] &&
            gamePalette[PALETTE_POSITION_RED] == gamePalette[playerPalettePosition_])
        {
            return TRUE;
        }
    }
    else if (tileType == TILE_WALL1_YELLOW_RED_GREEN_BLUE)
    {
        // The yellow, red, green, blue colour wall matches the body colour palette (this only happens after multiple colour changes happen).
        if (gamePalette[PALETTE_POSITION_BRIGHT_YELLOW] == gamePalette[playerPalettePosition_] &&
            gamePalette[PALETTE_POSITION_BRIGHT_RED] == gamePalette[playerPalettePosition_] &&
            gamePalette[PALETTE_POSITION_GREEN] == gamePalette[playerPalettePosition_] &&
            gamePalette[PALETTE_POSITION_BRIGHT_BLUE] == gamePalette[playerPalettePosition_])
        {
            return TRUE;
        }
    }

    // No match
    return FALSE;
}

void playerLoseHealth(u8 amount)
{
    if (player.health - amount > 0)
    {
        player.health -= amount;

        // Redraw hud with updated health value.
        hudRedraw = TRUE;
    }
    else
    {
        if (playerLives--)
        {
            levelState = LEVEL_DEATH;
            gameRunning = FALSE;
            player.health = playerStartingHealth;
        }
    }
}

void paintPlayer(u8 paletteIndex)
{
    player.paletteOffset = 4;
    playerPalettePosition_ = PALETTE_POSITION_BRIGHT_WHITE_2;
    player.redraw = TRUE;

    // I am assuming that paint colour can be manipulated by colour switches
    // so we are not going to assign the player colour to the original colour
    // and instead use whatever is currently in the palette at that position.
    gamePalette[playerPalettePosition_] = gamePalette[paletteIndex];

    // Update the palette after changing the player body colour.
    updatePalette();
}

u8 matchSpikeToBodyColourPalette(u8 paletteIndex)
{
    // Check if the player body colour (from palette) does not match the palette for the spike.
    if (gamePalette[playerPalettePosition_] != gamePalette[paletteIndex])
    {
        return PLAYER_ENVIRONMENT_SPIKE;
    }

    // The colour of the player and the spike are the same so return false to ignore the spike.
    return FALSE;
}

void updatePlayerEnvironmentFlags()
{
    // The tiles that the player currently occupies.
    u8 occupiedXTileLeft = (player.object.position.x) >> 2;
    u8 occupiedXTileRight = (player.object.position.x + TILE_WIDTH_BYTES - 1);
    u8 occupiedYTileBottom = (player.object.position.y + TILE_HEIGHT - 1);
    u8 occupiedYTileTop = (player.object.position.y) >> 4;
    occupiedXTileRight >>= 2;
    occupiedYTileBottom >>= 4;

    // Calculates the environment flags for each of the cells the player occupies.
    calculateCellEnvironmentFlags(occupiedXTileLeft, occupiedYTileTop);
    calculateCellEnvironmentFlags(occupiedXTileLeft, occupiedYTileBottom);
    calculateCellEnvironmentFlags(occupiedXTileRight, occupiedYTileTop);
    calculateCellEnvironmentFlags(occupiedXTileRight, occupiedYTileBottom);
}

void calculateCellEnvironmentFlags(u8 x, u8 y)
{
    u8 cell = mapCell(x, y, currentRoomX, currentRoomY);
    u8 cellType = cell & CELL_GFX_RANGE;

    if (cell & CELL_ENVIRONMENT_FLAG)
    {
        switch (cellType)
        {
        case ENVIRONMENT_SPIKE_RED:
            player.environmentFlags |= matchSpikeToBodyColourPalette(PALETTE_POSITION_BRIGHT_RED);
            break;

        case ENVIRONMENT_SPIKE_BLUE:
            player.environmentFlags |= matchSpikeToBodyColourPalette(PALETTE_POSITION_BRIGHT_BLUE);
            break;

        case ENVIRONMENT_SPIKE_GREEN:
            player.environmentFlags |= matchSpikeToBodyColourPalette(PALETTE_POSITION_GREEN);
            break;

        case ENVIRONMENT_SPIKE_YELLOW:
            player.environmentFlags |= matchSpikeToBodyColourPalette(PALETTE_POSITION_BRIGHT_YELLOW);
            break;

        case ENVIRONMENT_SPIKE_WHITE:
            player.environmentFlags |= matchSpikeToBodyColourPalette(PALETTE_POSITION_BRIGHT_WHITE);
            break;
        }
    }
}
