local Event = require 'utils.event'
local Gui = require 'utils.gui'
local Server = require 'utils.server'
local Global = require 'utils.global'
local SpamProtection = require 'utils.spam_protection'

local max = math.max
local round = math.round

local main_frame_name = Gui.uid_name()
local selection_button_name = Gui.uid_name()
local close_main_frame = Gui.uid_name()
local top_button_name = Gui.uid_name()

local this =
{
    difficulties =
    {
        [1] =
        {
            name = "I'm too young to die",
            index = 1,
            value = 0.75,
            color = { r = 0.00, g = 0.25, b = 0.00 },
            print_color = { r = 0.00, g = 0.4, b = 0.00 },
            count = 0,
            strength_modifier = 1.00,
            boss_modifier = 6.0
        },
        [2] =
        {
            name = 'Hurt me plenty',
            index = 2,
            value = 1,
            color = { r = 0.00, g = 0.00, b = 0.25 },
            print_color = { r = 0.0, g = 0.0, b = 0.5 },
            count = 0,
            strength_modifier = 1.25,
            boss_modifier = 7.0
        },
        [3] =
        {
            name = 'Ultra-violence',
            index = 3,
            value = 1.5,
            color = { r = 255, g = 128, b = 0.00 },
            print_color = { r = 255, g = 128, b = 0.00 },
            count = 0,
            strength_modifier = 1.75,
            boss_modifier = 8.0
        }
    },
    tooltip =
    {
        [1] = '',
        [2] = '',
        [3] = ''
    },
    show_gui = true,
    button_height = 37,
    value = 0.75,
    index = 1,
    fair_vote = false,
    closing_timeout = 54000,
    all_votes = {},
    gui_width = 108,
    name = "I'm too young to die",
    strength_modifier = 1.00,
    boss_modifier = 6.0,
    button_tooltip = nil
}

local Public = {}

Global.register(
    this,
    function (t)
        this = t
    end
)

local function clear_main_frame(player)
    local screen = player.gui.center
    if screen[main_frame_name] and screen[main_frame_name].valid then
        screen[main_frame_name].destroy()
    end
end

local function clear_top_frame(player)
    local top = player.gui.top
    if top[top_button_name] and top[top_button_name].valid then
        top[top_button_name].destroy()
    end
end

function Public.difficulty_gui()
    if not this.show_gui then
        return
    end

    local tooltip = 'Current difficulty of the map is ' .. this.difficulties[this.index].name .. '.'

    for _, player in pairs(game.connected_players) do
        local top = player.gui.top
        if top[top_button_name] then
            top[top_button_name].caption = this.difficulties[this.index].name
            top[top_button_name].tooltip = this.button_tooltip or tooltip
            top[top_button_name].style.font_color = this.difficulties[this.index].print_color
        else
            local b =
                top.add
                {
                    type = 'button',
                    caption = this.difficulties[this.index].name,
                    tooltip = tooltip,
                    name = top_button_name
                }
            b.style.font = 'heading-2'
            b.style.font_color = this.difficulties[this.index].print_color
            b.style.minimal_height = this.button_height
            b.style.maximal_height = this.button_height
            b.style.minimal_width = this.gui_width
        end
    end
end

local function highest_count(tbl)
    local init =
    {
        count = {},
        index = {}
    }
    for i = 1, #tbl do
        init.count[#init.count + 1] = tbl[i].count
        init.index[#init.index + 1] = tbl[i].index
    end

    local highest = max(unpack(init.count))

    local pre = {}

    if this.fair_vote then
        for x = 1, #init.count do
            if init.count[x] == highest then
                pre[#pre + 1] = { i = init.index[x], c = init.count[x] }
            end
        end
    else
        for x = 1, #init.count do
            if init.count[x] ~= 0 then
                if round(init.count[x] / highest) == 1 then
                    pre[#pre + 1] = { i = init.index[x], c = init.count[x] }
                end
            end
        end
    end

    local post = {}
    for i = 1, #pre do
        post[#post + 1] = pre[i].i
    end

    highest = round(table.mean(post))

    return highest
end

local function poll_difficulty(player)
    if player.gui.center[main_frame_name] then
        clear_main_frame(player)
    end

    if not this.show_gui then
        if player.gui.center[main_frame_name] then
            clear_main_frame(player)
        end
        return
    end

    if game.tick > this.closing_timeout then
        if player.online_time ~= 0 then
            local t = math.abs(math.floor((this.closing_timeout - game.tick) / 3600))
            local str = 'Votes have closed ' .. t
            str = str .. ' minute'
            if t > 1 then
                str = str .. 's'
            end
            str = str .. ' ago.'
            player.print(str)
        end
        return
    end

    local _, inside_frame = Gui.add_main_frame_with_toolbar(player, 'center', main_frame_name, nil, close_main_frame, 'Difficulty')
    if not inside_frame then
        return
    end

    for i = 1, #this.difficulties, 1 do
        local button_flow =
            inside_frame.add
            {
                type = 'flow',
                name = tostring(i)
            }
        local b = button_flow.add({ type = 'button', name = selection_button_name, caption = this.difficulties[i].name })
        b.style.font_color = this.difficulties[i].color
        b.style.font = 'heading-2'
        b.style.minimal_width = 160
        b.tooltip = this.tooltip[i]
    end

    local label_flow =
        inside_frame.add
        {
            type = 'flow'
        }

    inside_frame.add({ type = 'line' })

    label_flow.style.horizontal_align = 'center'
    label_flow.style.horizontally_stretchable = true

    local timeleft_flow =
        inside_frame.add
        {
            type = 'flow'
        }
    timeleft_flow.style.horizontal_align = 'center'
    timeleft_flow.style.horizontally_stretchable = true

    local b =
        timeleft_flow.add(
            {
                type = 'button',
                caption = math.floor((this.closing_timeout - game.tick) / 3600) .. ' minutes left.'
            }
        )
    b.style.font_color = { r = 0.66, g = 0.0, b = 0.66 }
    b.style.font = 'default-semibold'
    b.style.minimal_width = 96
    b.enabled = false

    inside_frame.add({ type = 'line' })
end

local function set_difficulty()
    local index = highest_count(this.difficulties)
    if not index or not this.difficulties[index] then
        return
    end

    if this.index ~= index then
        local message = table.concat({ '*** Map difficulty has changed to ', this.difficulties[index].name, ' difficulty! ***' })
        game.print(message, this.difficulties[index].print_color)
        Server.to_discord_embed(message)
    end
    this.index = index
    this.name = this.difficulties[index].name
    this.value = this.difficulties[index].value
    this.boss_modifier = this.difficulties[index].boss_modifier
    this.strength_modifier = this.difficulties[index].strength_modifier
    this.button_tooltip = this.tooltip[index]
end

function Public.reset_difficulty_poll(tbl)
    if tbl then
        this.value = tbl.value or 0.75
        this.index = tbl.index or 1
        this.all_votes = {}
        this.closing_timeout = tbl.closing_timeout or game.tick + 54000
        for _, p in pairs(game.connected_players) do
            if p.gui.center[main_frame_name] then
                clear_main_frame(p)
            end
            poll_difficulty(p)
        end
        for _, vote in pairs(this.difficulties) do
            vote.count = 0
        end
        Public.difficulty_gui()
    else
        this.value = 0.75
        this.index = 1
        this.all_votes = {}
        this.closing_timeout = game.tick + 54000
        for _, p in pairs(game.connected_players) do
            if p.gui.center[main_frame_name] then
                clear_main_frame(p)
            end
            poll_difficulty(p)
        end
        for _, vote in pairs(this.difficulties) do
            vote.count = 0
        end
        Public.difficulty_gui()
    end
end

local function on_player_joined_game(event)
    if not this.show_gui then
        return
    end
    local player = game.get_player(event.player_index)
    if game.tick < this.closing_timeout then
        if not this.all_votes[player.name] then
            poll_difficulty(player)
        end
    else
        clear_main_frame(player)
    end
    Public.difficulty_gui()
end

local function on_player_left_game(event)
    if game.tick > this.closing_timeout then
        return
    end
    local player = game.get_player(event.player_index)
    if not this.all_votes[player.name] then
        return
    end
    local index = this.all_votes[player.name].index
    this.difficulties[index].count = this.difficulties[index].count - 1
    if this.difficulties[index].count <= 0 then
        this.difficulties[index].count = 0
    end
    this.all_votes[player.name] = nil
    set_difficulty()
    Public.difficulty_gui()
end

function Public.set_tooltip(...)
    if type(...) == 'table' then
        this.tooltip = ...
    end
end

function Public.set_difficulties(...)
    if type(...) == 'table' then
        this.difficulties = ...
    end
end

function Public.set_poll_closing_timeout(...)
    this.closing_timeout = ...
end

--- Sets gui width
---@param number number
function Public.set_gui_width(number)
    this.gui_width = number
end

function Public.get_fair_vote()
    return this.fair_vote
end

function Public.set_fair_vote(value)
    this.fair_vote = value or false
end

function Public.show_gui(state)
    this.show_gui = state or false
end

function Public.get(key)
    if key then
        return this[key]
    else
        return this
    end
end

function Public.set(key, value)
    if key and (value or value == false) then
        this[key] = value
        return this[key]
    elseif key then
        return this[key]
    else
        return this
    end
end

function Public.has_votes_ended()
    return game.tick > this.closing_timeout
end

Gui.on_click(
    selection_button_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Poll difficulty selection frame name')
        if is_spamming then
            return
        end
        local element = event.element
        if not element or not element.valid then
            return
        end

        local player = event.player
        if not player or not player.valid then
            return
        end

        local i = tonumber(element.parent.name)

        if game.tick > this.closing_timeout then
            clear_main_frame(player)
            return
        end

        if this.all_votes[player.name] and this.all_votes[player.name].index == i then
            player.print('You have already voted for ' .. this.difficulties[i].name .. '.', this.difficulties[i].print_color)
            clear_main_frame(player)
            return
        end

        if this.all_votes[player.name] then
            local index = this.all_votes[player.name].index
            this.difficulties[index].count = this.difficulties[index].count - 1
            if this.difficulties[index].count <= 0 then
                this.difficulties[index].count = 0
            end
        end

        this.difficulties[i].count = this.difficulties[i].count + 1
        this.all_votes[player.name] = { voted = true, index = i }

        set_difficulty()
        Public.difficulty_gui()
        clear_main_frame(player)
        local message = '*** ' .. player.name .. ' has voted for ' .. this.difficulties[i].name .. ' difficulty! ***'
        game.print(message, this.difficulties[i].print_color)
        Server.to_discord_embed(message)
    end
)

Gui.on_click(
    top_button_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Poll difficulty top button')
        if is_spamming then
            return
        end
        local player = event.player
        if not player or not player.valid then
            return
        end
        if game.tick > this.closing_timeout then
            clear_main_frame(player)
            return
        end

        local screen = player.gui.center
        if screen[main_frame_name] and screen[main_frame_name].valid then
            clear_main_frame(player)
        else
            poll_difficulty(player)
        end
    end
)

Gui.on_click(
    close_main_frame,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Poll difficulty close button')
        if is_spamming then
            return
        end
        local player = event.player
        if not player or not player.valid then
            return
        end
        clear_main_frame(player)
    end
)

Event.add(defines.events.on_player_created, on_player_joined_game)
Event.add(defines.events.on_player_joined_game, on_player_joined_game)
Event.add(defines.events.on_player_left_game, on_player_left_game)

Public.top_button_name = top_button_name
Public.clear_main_frame = clear_main_frame
Public.clear_top_frame = clear_top_frame

return Public
