-- Tetris Game using GameEngine

--- region dependencies
local vector2 = require("vector2")
local button = require("button")

--- endregion

--- region Constants
local GRID_WIDTH = 10
local GRID_HEIGHT = 19
local BLOCK_SIZE = 30
local FALL_SPEED = 0.5

local screenWidth = 800
local screenHeight = 600

--- endregion

--- region Tetrominoes
local tetrominoes = {
    { shape = {{1, 1, 1, 1}}, color = Color.new(0, 255, 255) }, -- I
    { shape = {{1, 1}, {1, 1}}, color = Color.new(255, 255, 0) }, -- O
    { shape = {{0, 1, 0}, {1, 1, 1}}, color = Color.new(128, 0, 128) }, -- T
    { shape = {{1, 1, 0}, {0, 1, 1}}, color = Color.new(0, 255, 0) }, -- S
    { shape = {{0, 1, 1}, {1, 1, 0}}, color = Color.new(255, 0, 0) }, -- Z
    { shape = {{1, 1, 1}, {1, 0, 0}}, color = Color.new(255, 165, 0) }, -- L
    { shape = {{1, 1, 1}, {0, 0, 1}}, color = Color.new(0, 0, 255) }  -- J
}
--- endregion

-- Game State
local grid = {}
local currentPiece = {}
local pieceX, pieceY = 4, 0
local fallTimer = 0
local lastKeyState = { Left = false, Right = false, Down = false, Rotate = false, Space = true, Shift = false }
local hasGottenLeaderBoard = false
local leaderboard = {}
local netError = false
local nextPiece = {}
local pieceBag = {}
local heldPiece = nil
local canHold = true
-- enum of gameState, Main Menu, Playing, Submitting Name, Game Over

local MAIN_MENU = 0
local PLAYING = 1
local GAME_OVER = 2
local SUBMITTING_NAME = 3

local gameState = MAIN_MENU



local nameTextBox
local score = 0

-- list of bitmap frames that should be animated
local leaderboardFrames = {}

local leaderboardFrameTimer = 0
local leaderboardFrameIndex = 1
local leaderboardFreamSpeed = 0.05

--- region Bitmaps

local titleScreenBitmap
local gameOverBitmap
local pressRBitmap

local enterNameBitmap
local boardBitmap
local controlsBitmap
--- endregion

local yesButton = button.new(350, 400, 100, 50, "Yes", Color.new(0, 255, 0), function() 
    print("Sending Post")
    local playerName = nameTextBox:GetText()
    
    if playerName == "" then playerName = "Anonymous" end

    local data = "{ \"player\": \"" .. playerName .. "\", \"score\": " .. score .. " }"
    local response = GameEngine:postRequest("https://api.brammie15.dev/leaderboard/tetris", data)
    if(response == "Error: Request timed out") then 
        netError = true
    end
    print(response)
    gameState = GAME_OVER
end)
local noButton = button.new(350, 500, 100, 50, "No", Color.new(255, 0, 0), function() gameState = GAME_OVER end)

--- region BagStuff
local function shuffleBag()
    pieceBag = {}
    local indices = {1, 2, 3, 4, 5, 6, 7}
    for i = #indices, 2, -1 do
        local j = math.random(i)
        indices[i], indices[j] = indices[j], indices[i]
    end
    for _, index in ipairs(indices) do
        table.insert(pieceBag, tetrominoes[index])
    end
end

local function getNextPiece()
    if #pieceBag == 0 then
        shuffleBag()
    end
    local pieceData = table.remove(pieceBag, 1)
    return { shape = pieceData.shape, color = pieceData.color }
end

local function getRandomPiece()
    local pieceData = tetrominoes[math.random(#tetrominoes)]
    return { shape = pieceData.shape, color = pieceData.color }
end

--- endregion

local function checkCollision(px, py, piece)
    for y = 1, #piece.shape do
        for x = 1, #piece.shape[y] do
            if piece.shape[y][x] == 1 then
                if px + x < 1 or px + x > GRID_WIDTH or py + y > GRID_HEIGHT or (grid[py + y] and grid[py + y][px + x].value ~= 0) then
                    return true
                end
            end
        end
    end
    return false
end

local function newPiece()
    currentPiece = nextPiece or getNextPiece()
    nextPiece = getNextPiece()
    pieceX, pieceY = 4, 0

    if checkCollision(pieceX, pieceY, currentPiece) then
        gameState = SUBMITTING_NAME
    end
end

--- region GhostPieces
local function getGhostPieceY()
    local ghostY = pieceY
    while not checkCollision(pieceX, ghostY + 1, currentPiece) do
        ghostY = ghostY + 1
    end
    return ghostY
end

local function drawGhostPiece()
    local ghostY = getGhostPieceY()
    GameEngine:setColor(Color.new(200, 200, 200)) -- Semi-transparent gray

    for y = 1, #currentPiece.shape do
        for x = 1, #currentPiece.shape[y] do
            if currentPiece.shape[y][x] == 1 then
                GameEngine:fillRect((pieceX + x) * BLOCK_SIZE, (ghostY + y) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
            end
        end
    end
end

--- endregion

local function drawNextPiece()
    GameEngine:setColor(Color.new(255, 255, 255))
    GameEngine:drawText("Next:", 650, 100)
    
    if nextPiece then
        for y = 1, #nextPiece.shape do
            for x = 1, #nextPiece.shape[y] do
                if nextPiece.shape[y][x] == 1 then
                    GameEngine:setColor(nextPiece.color)
                    GameEngine:fillRect(650 + x * BLOCK_SIZE, 120 + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
                end
            end
        end
    end
end

local function clearLines()
    local linesCleared = 0
    local newGrid = {}

    -- Build a new grid without full lines
    for y = 1, GRID_HEIGHT do
        local full = true
        for x = 1, GRID_WIDTH do
            if grid[y][x].value == 0 then
                full = false
                break
            end
        end
        if not full then
            table.insert(newGrid, grid[y])  -- Keep non-full rows
        else
            linesCleared = linesCleared + 1
        end
    end

    -- Add empty rows at the top
    while #newGrid < GRID_HEIGHT do
        local emptyRow = {}
        for x = 1, GRID_WIDTH do
            emptyRow[x] = { value = 0, color = Color.new(255, 255, 255) }
        end
        table.insert(newGrid, 1, emptyRow)  -- Add empty row at the top
    end

    -- Update the grid reference
    grid = newGrid

    -- Score calculation based on cleared lines
    local scoreTable = { 100, 300, 500, 800 }
    if linesCleared > 0 then
        score = score + (scoreTable[linesCleared] or 0)
    end
end

local function freezePiece()
    for y = 1, #currentPiece.shape do
        for x = 1, #currentPiece.shape[y] do
            if currentPiece.shape[y][x] == 1 then
                grid[pieceY + y][pieceX + x] = { value = 1, color = currentPiece.color }
            end
        end
    end
    clearLines()
    newPiece()
    canHold = true
end

local function rotatePiece()
    local newShape = {}
    for x = 1, #currentPiece.shape[1] do
        newShape[x] = {}
        for y = 1, #currentPiece.shape do
            newShape[x][#currentPiece.shape - y + 1] = currentPiece.shape[y][x]
        end
    end
    if not checkCollision(pieceX, pieceY, { shape = newShape }) then
        currentPiece.shape = newShape
    end
end

local function drawGrid()
    for y = 1, GRID_HEIGHT do
        for x = 1, GRID_WIDTH do
            if grid[y][x].value ~= 0 then
                GameEngine:setColor(grid[y][x].color)
                GameEngine:fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
            end
            GameEngine:setColor(Color.new(50, 50, 50))
            GameEngine:drawRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
        end
    end
    GameEngine:setColor(Color.new(255, 255, 255))
    GameEngine:drawRect(BLOCK_SIZE, BLOCK_SIZE, GRID_WIDTH * BLOCK_SIZE, GRID_HEIGHT * BLOCK_SIZE)
end

local function drawPiece()
    GameEngine:setColor(currentPiece.color)
    for y = 1, #currentPiece.shape do
        for x = 1, #currentPiece.shape[y] do
            if currentPiece.shape[y][x] == 1 then
                GameEngine:fillRect((pieceX + x) * BLOCK_SIZE, (pieceY + y) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
            end
        end
    end
end
local function drawHeldPiece()
    GameEngine:setColor(Color.new(255, 255, 255))
    GameEngine:drawText("Hold:", 650, 300)

    if heldPiece then
        for y = 1, #heldPiece.shape do
            for x = 1, #heldPiece.shape[y] do
                if heldPiece.shape[y][x] == 1 then
                    GameEngine:setColor(heldPiece.color)
                    GameEngine:fillRect(650 + x * BLOCK_SIZE, 320 + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
                end
            end
        end
    end
end

function setup_window()
    GameEngine:setTitle("Tetris")
    GameEngine:setWidth(screenWidth)
    GameEngine:setHeight(screenHeight)
    GameEngine:setFrameRate(60)
end

--- the set_keylist function
--- @return string
function set_keylist()
    return "WASD "
end

function start()
    -- Initialize grid
    for y = 1, GRID_HEIGHT do
        grid[y] = {}
        for x = 1, GRID_WIDTH do
            grid[y][x] = { value = 0, color = Color.new(255, 255, 255) }
        end
    end

    -- Don't blame me for logging :p
    local name = GameEngine:getName()
    local data = "{ \"name\": \"" .. name .. "\" }"
    GameEngine:getRequest("https://api.brammie15.dev/game-open", data)

    nextPiece = getRandomPiece()
    newPiece()

    nameTextBox = Textbox.new("")
    -- Center on screen
    local width = 175
    local height = 25
    nameTextBox:SetBounds(math.floor((screenWidth - width) / 2) - 10, 300, width, height)
    nameTextBox:Hide()

    titleScreenBitmap = Bitmap.new("resources/tetrisLogo.bmp", true)

    -- load frame0 - 4
    for i = 0, 4 do
        print("loading frame" .. i)
        local bmp = Bitmap.new("resources/leaderboard/frame" .. i .. ".bmp", true)
        bmp:SetTransparencyColor(Color.new(255, 0, 255))
        table.insert(leaderboardFrames, bmp)
    end

    gameOverBitmap = Bitmap.new("resources/leaderboard/game_over.bmp", true)
    gameOverBitmap:SetTransparencyColor(Color.new(255, 0, 255))

    pressRBitmap = Bitmap.new("resources/leaderboard/press_r.bmp", true)
    pressRBitmap:SetTransparencyColor(Color.new(255, 0, 255))

    enterNameBitmap = Bitmap.new("resources/leaderboard/enter_name.bmp", true)
    enterNameBitmap:SetTransparencyColor(Color.new(255, 0, 255))

    boardBitmap = Bitmap.new("resources/board.bmp", true)
    boardBitmap:SetTransparencyColor(Color.new(255, 0, 255))

    controlsBitmap = Bitmap.new("resources/controls.bmp", true)
    controlsBitmap:SetTransparencyColor(Color.new(255, 0, 255))
end

function update()
    -- print(GameEngine:getMouseX(), GameEngine:getMouseY())
    if GameEngine:isKeyDown("ESCAPE") then
        GameEngine:quit()
    end
    if gameState == PLAYING then
        
        fallTimer = fallTimer + 1 / 60
        if fallTimer >= FALL_SPEED then
            if not checkCollision(pieceX, pieceY + 1, currentPiece) then
                pieceY = pieceY + 1
            else
                freezePiece()
            end
            fallTimer = 0
        end

        local leftPressed = GameEngine:isKeyDown("A") or GameEngine:isKeyDown("LEFT")
        local rightPressed = GameEngine:isKeyDown("D") or GameEngine:isKeyDown("RIGHT")
        local downPressed = GameEngine:isKeyDown("S") or GameEngine:isKeyDown("DOWN")
        local rotatePressed = GameEngine:isKeyDown("W") or GameEngine:isKeyDown("UP")
        local spacePressed = GameEngine:isKeyDown(" ")
        local shiftPressed = GameEngine:isKeyDown("SHIFT")

        if leftPressed and not lastKeyState.Left and not checkCollision(pieceX - 1, pieceY, currentPiece) then
            pieceX = pieceX - 1
        end
        if rightPressed and not lastKeyState.Right and not checkCollision(pieceX + 1, pieceY, currentPiece) then
            pieceX = pieceX + 1
        end
        if downPressed and not lastKeyState.Down and not checkCollision(pieceX, pieceY + 1, currentPiece) then
            pieceY = pieceY + 1
        end

        if shiftPressed and canHold then
            if heldPiece then
                -- Swap current piece with held piece
                currentPiece, heldPiece = heldPiece, currentPiece
            else
                -- Store the current piece and generate a new one
                heldPiece = currentPiece
                newPiece()
            end
            canHold = false  -- Prevent multiple swaps until next piece is placed
        end

        if spacePressed and not lastKeyState.Space then
            while not checkCollision(pieceX, pieceY + 1, currentPiece) do
                pieceY = pieceY + 1
            end
            freezePiece()
        end

        if rotatePressed and not lastKeyState.Rotate then
            rotatePiece()
        end

        lastKeyState.Left = leftPressed
        lastKeyState.Right = rightPressed
        lastKeyState.Down = downPressed
        lastKeyState.Rotate = rotatePressed
        lastKeyState.Space = spacePressed
        lastKeyState.Shift = shiftPressed
    end

    if gameState == GAME_OVER then

        --Update leaderboard gif
        leaderboardFrameTimer = leaderboardFrameTimer + 1 / 60
        if leaderboardFrameTimer >= leaderboardFreamSpeed then
            leaderboardFrameIndex = leaderboardFrameIndex + 1
            if leaderboardFrameIndex > #leaderboardFrames then
                leaderboardFrameIndex = 1
            end
            leaderboardFrameTimer = 0
        end

        if not hasGottenLeaderBoard then 

            local response = GameEngine:getRequest("https://api.brammie15.dev/leaderboard/tetris", "")
            if response == "Error: Request timed out" then 
                netError = true
            end
    
            print(response)
    
            -- format is
            -- NAME SCORE
    
            if not netError then
                for line in response:gmatch("[^\r\n]+") do
                    local name, score = line:match("([^%s]+)%s+(%d+)")
                    table.insert(leaderboard, { name = name, score = tonumber(score) })
                end
                table.sort(leaderboard, function(a, b) return a.score > b.score end)
            end
            hasGottenLeaderBoard = true
        end

        if GameEngine:isKeyDown("R") then
            gameState = PLAYING
            score = 0
            netError = false
            hasGottenLeaderBoard = false
            leaderboard = {}
            nameTextBox:SetText("")
            grid = {}
            for y = 1, GRID_HEIGHT do
                grid[y] = {}
                for x = 1, GRID_WIDTH do
                    grid[y][x] = { value = 0, color = Color.new(255, 255, 255) }
                end
            end
            newPiece()
        end
    end

    if gameState == SUBMITTING_NAME then
        yesButton:update(GameEngine)
        noButton:update(GameEngine)
    end

    if gameState == MAIN_MENU then
        if GameEngine:isKeyDown(" ") then
            gameState = PLAYING
        end
    end
end

function drawScoreBoard()
    GameEngine:setColor(Color.new(255, 0, 0))
    GameEngine:drawText("Score: " .. score, 350, 400)

    if hasGottenLeaderBoard then
        local NamesX = 475
        local ScoresX = 625
        local Y = 150

        GameEngine:setColor(Color.new(255, 255, 0))
        -- GameEngine:drawText("Leaderboard", 600, 60)
        GameEngine:drawBitmap(leaderboardFrames[leaderboardFrameIndex], 450, 60)

        if netError then 
            GameEngine:setColor(Color.new(255, 0, 0))
            GameEngine:drawText("Error: Request timed out", 600, 100)
            GameEngine:drawText("Or server down", 600, 120)
            GameEngine:drawText("Not gonna check :p", 600, 140)
            return
        end

        for i = 1, math.min(#leaderboard, 20) do
            GameEngine:setColor(Color.new(255, 255, 255))
            GameEngine:drawText(leaderboard[i].name, NamesX, Y)
            GameEngine:drawText(leaderboard[i].score, ScoresX, Y)
            Y = Y + 20
        end
    else
        GameEngine:setColor(Color.new(255, 255, 255))
        GameEngine:drawText("Loading Leaderboard...", 600, 100) 
    end
end

function drawGame()
    GameEngine:fillScreen(Color.new(0, 0, 0))
    drawGrid()
    GameEngine:drawBitmap(boardBitmap, 0, 0)
    drawGhostPiece()
    drawPiece()
    drawHeldPiece()
    drawNextPiece()  -- Draw the next piece preview
    GameEngine:setColor(Color.new(255, 255, 255))
    GameEngine:drawText("Score: " .. score, 650, 50)
end

function drawGameOver()
    nameTextBox:Hide()
    GameEngine:fillScreen(Color.new(0, 0, 0))
    GameEngine:drawBitmap(gameOverBitmap, 0, 0)
    GameEngine:drawBitmap(pressRBitmap, 0, 0)

    drawScoreBoard()
end

function drawSubmitName() 
    GameEngine:fillScreen(Color.new(0, 0, 0))
    GameEngine:drawBitmap(enterNameBitmap, 0, 0)
    nameTextBox:Show()
    yesButton:draw(GameEngine)
    noButton:draw(GameEngine)
end

function drawMainMenu() 
    GameEngine:drawBitmap(titleScreenBitmap, 0, 0)
    GameEngine:drawBitmap(controlsBitmap, 0, 0)
end

function draw()

    if gameState == PLAYING then
        drawGame()
    end

    if gameState == GAME_OVER then
        drawGameOver()
    end

    if gameState == SUBMITTING_NAME then
        drawSubmitName()
    end

    if gameState == MAIN_MENU then
        drawMainMenu()
    end
end

function quit()
    print("bye")
    local name = GameEngine:getName()
    local data = "{ \"name\": \"" .. name .. "\" }"
    print(data)
    GameEngine:getRequest("https://api.brammie15.dev/game-close", data)
end
