local gui = require("gui")
local color = require("gui.core.color")
local loader = require("utils/loader")
local theme = require("gui.core.theme")
local fmt = require("utils/fmt")
local timer = require("utils/utils")
local multi, thread = require("multi"):init()
local menu = require("gui.core.menus")

local timesup = love.audio.newSource("assets/sounds/timesup.mp3", "static")
local double = love.audio.newSource("assets/sounds/double.mp3", "static")

local boardUpdater = gui:getProcessor():newProcessor("board-updater")
local scoreUpdater = gui:getProcessor():newProcessor("score-updater")
local qUpdater = gui:getProcessor():newProcessor("question-updater")

local activePlayer
local playerList = {}
local playerStaticList = {}
local scoreboard = {}

-- Create a table to manage GUI elements
local manage = {}

local boardLog = log:child("board")

-- Function to resize fonts for all managed elements
boardLog:debug("Creating resize fonts handler")
local function resizeFonts()
    -- Use multi-threading to improve performance
    multi:newThread(function()
        -- Introduce a small delay to avoid too many iterations at once
        thread.skip(2)
        -- Iterate over each element in the 'manage' table
        for i = 1, #manage do
            local elem = manage[i]

            -- Check if the current element has a centerFont property
            if elem.centerFont then
                -- Adjust font size and re-center the text
                elem:fitFont(nil, nil, {scale = 2/3})
                elem:centerFont()
            end
        end
    end)
end

gui.Events.OnResized(resizeFonts)

local completed_questions = 0
local min_questions = 0
local dd_count = 0
local dd_enabled = false
local applied_dd = false
local board, question

local function pickUniqueIndices(t, count)
    assert(#t >= count, "Not enough elements to pick " .. count .. " unique items")

    local indices = {}
    for i = 1, #t do indices[i] = i end

    -- partial Fisher-Yates shuffle
    for i = 1, count do
        local j = math.random(i, #indices)
        indices[i], indices[j] = indices[j], indices[i]
    end

    return unpack(indices, 1, count)
end

local function pickFiltered(items, count)
    -- Filter to only elements whose text contains "$"
    local filtered = {}
    for _, item in ipairs(items) do
        if item.text:find("%$") then
            filtered[#filtered + 1] = item
        end
    end

    -- Use pickUniqueIndices to select from filtered results
    local picks = { pickUniqueIndices(filtered, count) }
    local results = {}
    for _, idx in ipairs(picks) do
        results[#results + 1] = filtered[idx]
    end

    return unpack(results)
end

function applyDD()
    if not applied_dd and completed_questions > min_questions then
        boardLog:debug("Creating double jeopardy questions: %d",dd_count)
        local dd = 0
        local dd_list = {}
        local dds = {pickFiltered(board.children, dd_count)}
        for i,v in pairs(dds) do
            v.isDouble = true
        end
        applied_dd = true
    end
end

local templateLog = log:child("template")
function LoadTemplate(name, path)
    boardLog:debug("Loading Template: %s", name)
    if love.filesystem.getInfo(path .."/templates/" .. name .. ".lua") then
        data = love.filesystem.read(path .."/templates/" .. name .. ".lua")
    elseif love.filesystem.getInfo("templates/" .. name .. ".lua") then
        data = love.filesystem.read("templates/" .. name .. ".lua")
    else
        boardLog:error("Error Loading Template: %s", name)
        gui:newMessageBox({
            title    = "Error!",
            message  = "Unable to load template: " .. name,
            buttons  = { "Yes", "No" },
            onChoice = function(label, idx)
                if label == "Yes" then 
                    doCallback(selected)
                    pickerWindow.visible = false
                    pickerWindow:setTag("visual")
                end
            end,
        })
        return 
    end

    local template, err = loadstring(data)

    if err ~= nil then
        error(err)
    end

    local timer = function(wait, callback)
        multi:newAlarm(wait):OnRing(callback)
    end

    local env = { -- lua built-ins except io/os code execution
        pairs=pairs,
        print=print,
        tostring=tostring,
        tonumber=tonumber,
        type=type,
        assert=assert,
        ipairs=ipairs,
        table=table,
        math=math,
        string=string,
        next=next,
        pcall=pcall,
        select=select,
        xpcall=xpcall,
        utf8=utf8,
        clock = require("socket").gettime,
        ALIGN_CENTER = gui.ALIGN_CENTER,
        ALIGN_LEFT = gui.ALIGN_LEFT,
        ALIGN_RIGHT = gui.ALIGN_RIGHT,
        ALIGN_JUSTIFY = gui.ALIGN_JUSTIFY,
        -- Global vars
        color = color,
        error = multi.error,
        theme = theme,
        gui = gui,
        timer = timer,
        -- love stuff
        newSource = love.audio.newSource,
        log = templateLog
    }
    
    env._G = env
    local isoTemplate = multi.isolateFunction(template, env)

    return isoTemplate()
end

local function manageCosmetics(frame, cosmetics)
    boardLog:debug("Managing cosmetics")
    if cosmetics["bg-img"] then
        frame:setImage(cosmetics["bg-img"])
    else
        frame:setImage("assets/images/background.png")
    end
    if type(cosmetics.colors) == "table" then
        for i,v in pairs(cosmetics.colors) do
            color.reindexColor((i:gsub("-","_")),v)
        end
    end
end

function gui:cleanup()
    for i = #self.children, 1, -1 do
        self.children[i]:destroy()
    end
    self.children = {}
    completed_questions = completed_questions + 1
end

local reset = boardUpdater:newConnection()

local default_text = "Pick a Category"

function Menu(frame, path)
    boardLog:info("Building board")
    if path:match(".zip") then
        -- local basename = path:match("([^/\\]+)$")
        local success = love.filesystem.mount(path, "quiz", true)
        if not success then
            gui:newMessageBox({
                title    = "Return to the main menu?",
                message  = "Couldn't load file: " .. path,
                buttons  = { "Return", "Quit" },
                onChoice = function(label, idx)
                    if label == "Return" then 
                        return menu.getMenu("title"):visible(true)
                    else
                        os.exit()
                    end
                end,
            })
            return
        end
        path = "quiz"
    end

    -- Start the processors now
    boardLog:debug("Starting processors")
    boardUpdater.Start()
    qUpdater.Start()
    scoreUpdater.Start()

    local scoreboard = ScoreBoard(frame, 0, 0, 0, 0, .06, .1, .170, .8)

    local qframe = frame:newFrame(0, 0, 0, 0, .24, .1, .70, .8)

    local label = frame:newTextLabel(default_text, 0, 10, 0, -20, .29, 0, .6, .1)
    label.visibility = .85
    label.drawBorder = false
    label.align = gui.ALIGN_CENTER
    label.color = color.bg
    label.textColor = color.status_font
    label:setRoundness(20,20,60)

    boardUpdater:newThread(function()
        local test = function()
            return label.text ~= ""
        end
        while true do
            thread.hold(test)
            label:centerFont()
            label:fitFont(nil, nil, {scale = 2/3})
        end
    end)

    local data = loader:new(path)
    board, question, dailydouble = qframe:newFrame(), qframe:newFrame(), qframe:newImageLabel("assets/images/double.jpg")
    index = data.index

    local cosmetics = index.cosmetics or {}
    manageCosmetics(frame, cosmetics)
    
    board:fullFrame()
    question:fullFrame()
    dailydouble:fullFrame()

    dailydouble.visible = false
    question.visible = false
    question.color = color.question_bg

    local tiers = index.settings.tiers or 5
    local start = index.settings.start or 100
    local inc   = index.settings.increment or 100

    if index.settings.dailyDouble and index.settings.dailyDouble.enabled then
        local dd = index.settings.dailyDouble
        dd_enabled = dd.enabled
        min_questions = dd.minQuestions
        dd_count = dd.count
    end

    local autoreveal = false
    if index.dev and index.dev.enabled then
        if index.dev["auto-reveal"] then
            autoreveal = true
        end
        if type(index.dev.players) == "table" then
            for i,v in ipairs(index.dev.players) do
                if type(v) == "string" then
                    scoreboard:AddPlayer(v, 0)
                elseif type(v) == "table" then
                    scoreboard:AddPlayer(v.name, v.score, v.icon)
                end
            end
        end
    end

    reset(function()
        question:cleanup()
        manageCosmetics(frame, cosmetics, true)
        label.text = default_text
    end)

    for cat,v in pairs(index.categories) do
        local c
        if v.image then
            c = board:newImageLabel(v.image, 0, 0, 0, 0,(1/#index.categories)*(cat-1),0,1/#index.categories,1/(tiers+1))
        else
            c = board:newTextLabel(v.displayName or v.name,0,0,0,0,(1/#index.categories)*(cat-1),0,1/#index.categories,1/(tiers+1))
            c.align = gui.ALIGN_CENTER
            c.textColor = color.header_font
            c.color = color.header_tile
        end

        c.UUID = multi.generate_uuid7() -- Each otpion gets a unique UUID

        if not autoreveal then
            img = c:newImageButton("assets/images/placeholder.jpg")
            img.visibility = 0

            img:OnReleased(function(self)
                self.visible = false
            end)

            img:fullFrame()
        end

        table.insert(manage,c)
        
        for tier = 1,tiers do
            local t = board:newTextButton("$" .. start + inc*(tier-1),0,0,0,0,(1/#index.categories)*(cat-1),1/(tiers+1)*tier,(1/#index.categories),1/(tiers+1))
            t.textColor = color.tile_font
            t.align = gui.ALIGN_CENTER
            t.color = color.tile_bg
            t.category = v.name
            t.index = tier
            t.price = start + inc*(tier-1)
            t:OnReleased(boardUpdater:newFunction(function(self)
                if self.text == "" or GetActivePlayer() == nil then return end
                if dd_enabled then
                    applyDD() -- check and run DD if conditions meet
                end
                if index.categories[cat].questions == nil then boardLog:info("Question not defined: File: %s Category: %s - %s\n",path,index.categories[cat].name,start + inc*(tier-1)) return end
                local globals = index.categories[cat].global
                if not globals then
                    globals = {}
                end
                local q = index.categories[cat].questions[self.index]
                if not q then return end
                label.text = t.category .. " - " .. t.price
                boardLog:debug("Displaying question for: %s - %s", t.category, t.price)
                local mt = debug.getmetatable(q)
                if mt then
                    mt.__metatable = nil
                end
                setmetatable(q, {__index = globals})
                if q == nil then boardLog:info("Question contains no data: File: %s Category: %s Tier: %s\n",path,index.categories[cat].name,start + inc*(tier-1)) return end
                if q.cosmetics then
                    manageCosmetics(frame, q.cosmetics)
                end
                self.textVisibility = 0
                local template = LoadTemplate(q.template, path)
                question.visible = true
                local player = GetActivePlayer()
                local tm
                local stop
                boardLog:info("Question: '%s' Answer: '%s'",q["title"],q["answer"])
                local mul = 1
                boardUpdater:newThread(function()
                    if self.isDouble then
                        mul = 2
                        double:play()
                        dailydouble.visible = true
                        thread.hold(function()
                            return not double:isPlaying()
                        end)
                        dailydouble.visible = false
                    end
                    if q["time-limit"] then
                        tm = timer.startTimer({duration = q["time-limit"]})
                        tm.OnStop(function()
                            -- Make sound? Subtract if daily double
                            if stop then return end
                            timesup:play()
                        end)
                    end
                    local finished = false
                    template.index(question, q, function(ans, m)
                        local m = m or 1
                        if finished then return end
                        player = GetActivePlayer()
                        if tm then
                            tm:Cleanup()
                        end
                        stop = true
                        if ans then
                            player:Add((self.price*mul)*m)
                            finished = true
                            question.visible = false
                            reset:Fire()
                        elseif ans == false then
                            player:Add(-self.price*mul)
                            player = GetNextPlayer()
                        else
                            finished = true
                            question.visible = false
                            reset:Fire()
                        end -- nil is a valid option where you weren't right or wrong, you just skipped
                        self.text = ""
                    end)
                    boardUpdater:newThread("QuestionUpdater",function()
                        while true do
                            template.update(dt)
                            thread.yield()
                            if self.text == "" then return end
                        end
                    end)
                end)
            end))
            table.insert(manage,t)
        end
    end

    resizeFonts()
    resizeFonts()

    return background
end



function GetActivePlayer()
    if not activePlayer then return end
    return activePlayer.link
end

local function GetPlayerPos()
    for i,v in pairs(playerStaticList) do
        if v == GetActivePlayer() then
            return i
        end
    end
end

function GetNextPlayer()
    local pos = GetPlayerPos()

    if pos >= #playerStaticList then
        activePlayer = playerStaticList[1].Ref.Frame
    else
        activePlayer = playerStaticList[pos + 1].Ref.Frame
    end
    scoreboard:RenderPlayer(playerList)
    return GetActivePlayer()
end

function love.filedropped(file)
	file:open("r")
	local data = file:read()
	print("Load file? " .. file:getFilename())
end

function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh)
    -- Colors
    local C_BG_PANEL   = color.new("#1a1a2e")
    local C_BG_HEADER  = color.new("#16213e")
    local C_ACCENT     = color.new("#e94560")
    local C_ROW_TOP    = color.new("#1c2641")
    local C_ROW_NORM   = color.new("#121226")
    local C_BORDER_TOP = color.new("#323c6e")
    local C_BORDER_NRM = color.new("#1c1c37")
    local C_BAR_EMPTY  = color.new("#232341")
    local C_TEXT_MUTED = color.new("#786e96")
    local C_WHITE      = color.new("#ffffff")
    local C_GOLD       = color.new("#ffd700")
    local C_SILVER     = color.new("#C0C0C0")
    local C_BRONZE     = color.new("#cd7f32")

    -- Config stuff
    local LEADER_HEIGHT_SCALE = .06
    local HEIGHT_SCALE = .03
    local PLAYER_HEIGHT = .05
    --

    local rankColors = { C_GOLD, C_SILVER, C_BRONZE }

    local leaderboard = frame:newFrame(x, y, w, h, sx, sy, sw, sh)
    leaderboard.color = C_BORDER_NRM
    
    local headernum = leaderboard:newTextLabel("#",0,0,0,0,0,LEADER_HEIGHT_SCALE,1/6,HEIGHT_SCALE)
    headernum.align = gui.ALIGN_CENTER

    local headerplayer = leaderboard:newTextLabel("PLAYER",0,0,0,0,1/6,LEADER_HEIGHT_SCALE,3/6,HEIGHT_SCALE)
    headerplayer.align = gui.ALIGN_LEFT

    local headerscore = leaderboard:newTextLabel("SCORE",0,0,0,0,4/6,LEADER_HEIGHT_SCALE,2/6,HEIGHT_SCALE)
    headerscore.align = gui.ALIGN_RIGHT

    local header = leaderboard:newTextLabel("LEADERBOARD",0,0,0,0,0,0,1,LEADER_HEIGHT_SCALE)
    header.borderColor = C_ACCENT
    header.textColor = C_ACCENT
    header.color = C_BG_HEADER
    header.align = gui.ALIGN_CENTER

    local BASE_HEIGHT = LEADER_HEIGHT_SCALE + HEIGHT_SCALE

    gui.apply({
        drawBorder = false,
        color = color.new("#0f2a60"),
        textColor = C_TEXT_MUTED,
    }, headernum, headerplayer, headerscore)

    local updateList = {header, headernum, headerplayer, headerscore}
   
    local function ScoreResize()
        scoreUpdater:newThread(function()
            thread.skip(2)

            for _,object in pairs(updateList) do
                object:fitFont(nil, nil, {scale = 5/6})
                object:centerFont()
            end

            for _,object in pairs(playerList) do
                if type(object) == "table" and object.Ref then
                    for i, player in pairs(object.Ref) do
                        if player:hasType(gui.TYPE_TEXT) then
                            player:fitFont(nil, nil, {scale = 5/6})
                            player:centerFont()
                        end
                    end
                end
            end
        end)
    end
    
    function scoreboard:AddPlayer(name, score, icon)
        local player = {
            Name = name, 
            Score = score or 0, 
            Icon = icon,
            UUID = multi.generate_uuid7(),
            Add = function(self, amt)
                self.Score = tostring(tonumber(self.Score) + amt)
                table.sort(playerList, function(a, b)
                    if a.Score == b.Score then
                        return a.Name < b.Name
                    end
                    return tonumber(a.Score) > tonumber(b.Score)
                end)
                scoreboard:RenderPlayer(playerList)
                ScoreResize()
            end,
        }
        table.insert(playerList, player)
        table.insert(playerStaticList, player)
        scoreboard:RenderPlayer(playerList)
        ScoreResize()
        return player
    end

    local colors = {C_GOLD, C_SILVER, C_BRONZE}
    local function MapColor(index)
        if index <=3 and index > 0 then
            return colors[index]
        else
            return C_WHITE
        end
    end

    local add_player = leaderboard:newFrame(5,-5,-10,0,0,1-PLAYER_HEIGHT,1,PLAYER_HEIGHT)
    local edit_player = leaderboard:newFrame(5,-10,-10,0,0,1-2*PLAYER_HEIGHT,1,PLAYER_HEIGHT)
    local remove_player = leaderboard:newTextButton("Remove Selected",5,-15,-10,0,0,1-3*PLAYER_HEIGHT,1,PLAYER_HEIGHT)
    remove_player:setFont(20)
    remove_player.align = gui.ALIGN_CENTER
    local embededWatch = {remove_player}
    scoreUpdater:newThread(function()
        while true do
            thread.sleep(.01)
            for i,v in pairs(embededWatch) do
                v:centerFont()
            end
        end
    end)

    local function embedTextEdit(reference, default, but_text, callback)
        reference.color = C_BORDER_NRM
        local textbox = reference:newTextBox(default,0,0,0,0,.015,.1,.8,.8)
        textbox.textColor = C_GOLD
        textbox.blink = false
        textbox.color = C_BORDER_TOP
        textbox.textColor = C_WHITE
        textbox:OnPressed(function()
            textbox.text = ""
        end)

        local button = reference:newTextButton(but_text,5,0,-10,0,.815,.1,.185,.8)
        button.color = color.new("#7eae5b")
        button:OnReleased(function()
            callback(textbox)
        end)
        gui.apply({
            setFont = {20},
            align = gui.ALIGN_CENTER
        },textbox,button,reference)
        table.insert(embededWatch,textbox)
        table.insert(embededWatch,button)
    end

    remove_player.color = color.new("#a13a3a")
    remove_player:OnReleased(function()
        local player = GetActivePlayer()
        uuid = player.UUID
        for i = 1, #playerList do
            if playerList[i].UUID == uuid then
                table.remove(playerList,i)
                break
            end
        end
        for i = 1, #playerStaticList do
            if playerStaticList[i].UUID == uuid then
                table.remove(playerStaticList,i)
                break
            end
        end
        scoreboard:RenderPlayer(playerList)
        player.Ref.Frame:destroy()
    end)

    embedTextEdit(add_player, "Player Name", "Add", function(self)
        scoreboard:AddPlayer(self.text, "0")
    end)

    embedTextEdit(edit_player, "Modify Score", "Edit", function(self)
        local player = GetActivePlayer()
        if player then
            player.Score = self.text
            scoreboard:RenderPlayer(playerList)
        end
    end)

    function scoreboard:RenderPlayer(list)
        for index, player in ipairs(list) do
            if player.Ref then
                player.Ref[1].text = player.Name or ""
                player.Ref[2].text = player.Score or ""
                player.Ref[3].text = tostring(index)
                player.Ref.Frame:setDualDim(nil,5*index,nil,nil,nil,BASE_HEIGHT + (index-1) * PLAYER_HEIGHT)
                player.Ref.Frame.link = player

                gui.apply({
                    visibility = 0,
                    drawBorder = false,
                    textColor = MapColor(index)
                }, unpack(player.Ref))

                if activePlayer == nil then
                    activePlayer = player.Ref.Frame
                end

                if player.Ref.Frame == activePlayer then
                    player.Ref.Frame.borderColor = C_BORDER_NRM
                    player.Ref.Frame.color = C_ROW_NORM
                else
                    player.Ref.Frame.borderColor = C_BORDER_TOP
                    player.Ref.Frame.color = C_ROW_TOP
                end
            else
                local playernum, playerName, playerIcon, playerScore, playerLine
                local playerFrame = leaderboard:newFrame(5,5*index,-10,0,0,BASE_HEIGHT + (index-1) * PLAYER_HEIGHT,1,PLAYER_HEIGHT)
                
                playerFrame:OnReleased(function(self)
                    activePlayer = self
                    scoreboard:RenderPlayer(playerList)
                end)

                playerFrame:respectHierarchy(false)
                
                playerFrame.borderColor = C_BORDER_TOP
                playerFrame.color = C_ROW_TOP

                playernum = playerFrame:newTextLabel(index,0,0,0,0,.015,.1,1/8,.8)
                playernum.align = gui.ALIGN_CENTER

                if player.Icon ~= nil then
                    playerIcon = playerFrame:newImageLabel(player.Icon,0,0,0,0,.16,.1,.1,.8)
                    playerIcon.square = "h" -- When working with scales squaring is trickier. (h/w) to switch on width or height
                    
                    playerName = playerFrame:newTextLabel(player.Name,0,0,0,0,.3,0,2/5,.8)
                    playerName.align = gui.ALIGN_LEFT

                    playerLine = playerFrame:newFrame(0,0,0,0,.3,.8,.69,.07)
                    playerLine.color = C_GOLD
                else
                    playerName = playerFrame:newTextLabel(player.Name,0,0,0,0,.16,0,7/13,.8)
                    playerName.align = gui.ALIGN_LEFT

                    playerLine = playerFrame:newFrame(0,0,0,0,.16,.8,7/13 + 2/7,.07)
                    playerLine.color = C_GOLD
                end

                playerScore = playerFrame:newTextLabel(player.Score,0,0,0,0,.71,0,2/7,.8)
                playerScore.align = gui.ALIGN_CENTER
                playerLine.drawBorder = false
                
                gui.apply({
                    visibility = 0,
                    drawBorder = false,
                    textColor = MapColor(index)
                },playernum, playerName, playerScore, playerIcon, playerLine)

                player.Ref = {playerName, playerScore, playernum, playerIcon, playerLine, Frame = playerFrame}
            end
        end
    end

    gui.Events.OnResized(ScoreResize)
    ScoreResize()
    return scoreboard
end

return menu.registerMenu("board", Menu)