--luacheck: ignore 143
local Gui = require 'utils.gui'
local Global = require 'utils.global'
local Event = require 'utils.event'
local Server = require 'utils.server'
local Session = require 'utils.datastore.session_data'
local Config = require 'utils.gui.config'
local SpamProtection = require 'utils.spam_protection'
local Math = require 'utils.math.math'
local Discord = require 'utils.discord_handler'
local Color = require 'utils.color_presets'
local CustomEvents = require 'utils.created_events'

local Public = {}

local insert = table.insert
local contains = table.contains
local remove_element = table.remove_element

local default_poll_duration = 300 * 60 -- in ticks
local duration_max = 3600 -- in seconds
local duration_step = 15 -- in seconds

local duration_slider_max = duration_max / duration_step
local tick_duration_step = duration_step * 60
local inv_tick_duration_step = 1 / tick_duration_step

local polls = {}
local polls_counter = { 0 }
local running_polls = {}
local no_notify_players = {}
local player_poll_index = {}
local player_create_poll_data = {}
local settings =
{
    allow_non_trusted_to_vote = false,
    allow_non_trusted_to_create = false,
}

Global.register(
    {
        polls = polls,
        polls_counter = polls_counter,
        running_polls = running_polls,
        no_notify_players = no_notify_players,
        player_poll_index = player_poll_index,
        player_create_poll_data = player_create_poll_data,
        settings = settings
    },
    function (tbl)
        polls = tbl.polls
        polls_counter = tbl.polls_counter
        running_polls = tbl.running_polls
        no_notify_players = tbl.no_notify_players
        player_poll_index = tbl.player_poll_index
        player_create_poll_data = tbl.player_create_poll_data
        settings = tbl.settings
    end
)

local main_button_name = Gui.uid_name()
local main_frame_name = Gui.uid_name()
local create_poll_button_name = Gui.uid_name()
local notify_checkbox_name = Gui.uid_name()

local poll_view_back_name = Gui.uid_name()
local poll_view_forward_name = Gui.uid_name()
local poll_view_vote_name = Gui.uid_name()
local poll_view_edit_name = Gui.uid_name()

local create_poll_frame_name = Gui.uid_name()
local create_poll_duration_name = Gui.uid_name()
local create_poll_label_name = Gui.uid_name()
local create_poll_question_name = Gui.uid_name()
local create_poll_answer_name = Gui.uid_name()
local create_poll_add_answer_name = Gui.uid_name()
local create_poll_delete_answer_name = Gui.uid_name()
local create_poll_close_name = Gui.uid_name()
local create_poll_clear_name = Gui.uid_name()
local create_poll_edit_name = Gui.uid_name()
local create_poll_confirm_name = Gui.uid_name()
local create_poll_delete_name = Gui.uid_name()

Config.register_scenario_module(
    {
        id = "poll",
        admin_only = false,
        gui_rows = Config.register_token(
            function (player, frame)
                local poll_table = Public.get_no_notify_players()
                local switch_state = 'left'
                if poll_table[player.index] then
                    switch_state = 'right'
                end
                Config.add_switch(frame, switch_state, 'poll_no_notify_toggle', { 'gui.notify_on_polls' }, { 'gui-description.notify_on_polls' })
                frame.add({ type = 'line' })
            end),
        handlers =
        {
            ['poll_no_notify_toggle'] = Config.register_token(
                function (player, event)
                    local poll_table = Public.get_no_notify_players()
                    if event.element.switch_state == 'left' then
                        poll_table[player.index] = nil
                        player.print('You will now be notified when polls are created.', { color = Color.green })
                    else
                        poll_table[player.index] = true
                        player.print('You will no longer be notified when polls are created.', { color = Color.red })
                    end
                end),
        }
    })

Config.register_scenario_module(
    {
        id = "poll_admin_settings",
        admin_only = true,
        gui_rows = Config.register_token(
            function (_, frame)
                local switch_state = 'right'
                if settings.allow_non_trusted_to_create then
                    switch_state = 'left'
                end
                Config.add_switch(frame, switch_state, 'poll_trusted_create_toggle', 'Poll create', 'Disables non-trusted plebs to create polls.')
                frame.add({ type = 'line' })
                switch_state = 'right'
                if settings.allow_non_trusted_to_vote then
                    switch_state = 'left'
                end
                Config.add_switch(frame, switch_state, 'poll_trusted_vote_toggle', 'Poll vote', 'Disables non-trusted plebs to vote on polls.')
                frame.add({ type = 'line' })
            end),
        handlers =
        {
            ['poll_trusted_create_toggle'] = Config.register_token(
                function (_, event)
                    if event.element.switch_state == 'left' then
                        settings.allow_non_trusted_to_create = true
                        Config.get_actor(event, '[Poll create]', 'has disabled non-trusted people to do polls.')
                    else
                        settings.allow_non_trusted_to_create = false
                        Config.get_actor(event, '[Poll create]', 'has allowed non-trusted people to do polls.')
                    end
                end),
            ['poll_trusted_vote_toggle'] = Config.register_token(
                function (_, event)
                    if event.element.switch_state == 'left' then
                        settings.allow_non_trusted_to_vote = true
                        Config.get_actor(event, '[Poll vote]', 'has disabled non-trusted people to vote on polls.')
                    else
                        settings.allow_non_trusted_to_vote = false
                        Config.get_actor(event, '[Poll vote]', 'has allowed non-trusted people to vote on polls.')
                    end
                end)
        }
    })

local function poll_id()
    local count = polls_counter[1] + 1
    polls_counter[1] = count
    return count
end

local function apply_direction_button_style(button)
    local button_style = button.style
    button_style.width = 24
    button_style.height = 24
    button_style.top_padding = 0
    button_style.bottom_padding = 0
    button_style.left_padding = 0
    button_style.right_padding = 0
    button_style.font = 'default-listbox'
end

local function apply_button_style(button)
    local button_style = button.style
    button_style.font = 'default-semibold'
    button_style.height = 26
    button_style.minimal_width = 26
    button_style.top_padding = 0
    button_style.bottom_padding = 0
    button_style.left_padding = 2
    button_style.right_padding = 2
end

local function do_remaining_time(poll, remaining_time_label)
    local end_tick = poll.end_tick
    if end_tick == -1 then
        remaining_time_label.caption = 'Endless Poll.'
        return true
    end

    local ticks = end_tick - game.tick
    if ticks < 0 then
        remaining_time_label.caption = 'Poll Finished.'
        return false
    else
        local time = math.ceil(ticks / 60)
        remaining_time_label.caption = 'Remaining Time: ' .. time
        return true
    end
end

local function send_poll_result_to_discord(poll, complete)
    local fields = {}

    -- Created by
    if poll.created_by then
        table.insert(fields,
            {
                title = "Created by",
                description = poll.created_by,
                inline = "false"
            })
    end

    -- Edited by
    local edited_by_players = poll.edited_by
    if next(edited_by_players) then
        local editors = {}
        for pi, _ in pairs(edited_by_players) do
            local p = game.players[pi]
            if p and p.valid then
                table.insert(editors, p.name)
            end
        end

        if #editors > 0 then
            table.insert(fields,
                {
                    title = "Edited by",
                    description = table.concat(editors, ", "),
                    inline = "false"
                })
        end
    end

    -- Answers (each as inline field)
    local answers = poll.answers
    if answers and #answers > 0 then
        for _, a in ipairs(answers) do
            table.insert(fields,
                {
                    title = a.text,
                    description = "**" .. a.voted_count .. "** votes",
                    inline = "true"
                })
        end
    end

    -- Send to Discord
    Discord.send_notification_connected_channel(
        {
            title = complete and "Poll #" .. poll.id .. " has finished" or "Poll #" .. poll.id,
            description = "**Question:** " .. poll.question,
            color = complete and "success" or "info",
            fields = fields,
            tick = game.tick
        })
end


local function redraw_poll_viewer_content(data)
    local poll_viewer_content = data.poll_viewer_content
    local remaining_time_label = data.remaining_time_label
    local poll_index = data.poll_index
    local player = poll_viewer_content.gui.player

    data.vote_buttons = nil
    Gui.remove_data_recursively(poll_viewer_content)
    poll_viewer_content.clear()

    local poll = polls[poll_index]
    if not poll then
        return
    end

    local answers = poll.answers

    local created_by_player = poll.created_by
    local created_by_text
    if created_by_player then
        created_by_text = ' Created by ' .. created_by_player
    else
        created_by_text = ''
    end

    local top_flow = poll_viewer_content.add { type = 'flow', direction = 'vertical' }
    top_flow.add { type = 'label', caption = table.concat { 'Poll #', poll.id, created_by_text } }

    local edited_by_players = poll.edited_by
    if next(edited_by_players) then
        local edit_names = { 'Edited by ' }
        for pi, _ in pairs(edited_by_players) do
            local p = game.get_player(pi)
            if p and p.valid then
                insert(edit_names, p.name)
                insert(edit_names, ', ')
            end
        end

        table.remove(edit_names)
        local edit_text = table.concat(edit_names)

        local top_flow_label = top_flow.add { type = 'label', caption = edit_text, tooltip = edit_text }
        top_flow_label.style.single_line = false
        top_flow_label.style.horizontally_stretchable = false
    end

    local poll_enabled = do_remaining_time(poll, remaining_time_label)

    local question_flow = poll_viewer_content.add { type = 'table', column_count = 2 }
    if player.admin and not poll.created_by_script then
        local edit_button =
            question_flow.add
            {
                type = 'sprite-button',
                name = poll_view_edit_name,
                sprite = 'utility/rename_icon',
                tooltip = 'Edit Poll.'
            }

        local edit_button_style = edit_button.style
        edit_button_style.width = 26
        edit_button_style.height = 26
    end

    local question_label = question_flow.add { type = 'label', caption = poll.question }
    question_label.style.minimal_height = 32
    question_label.style.single_line = false
    question_label.style.font = 'heading-2'
    question_label.style.font_color = { r = 0.98, g = 0.66, b = 0.22 }
    question_label.style.top_padding = 4
    question_label.style.left_padding = 4
    question_label.style.right_padding = 4
    question_label.style.bottom_padding = 4

    local grid = poll_viewer_content.add { type = 'table', column_count = 2 }

    local vote_buttons = {}
    for i, a in pairs(answers) do
        local vote_button_flow = grid.add { type = 'flow' }
        local vote_button =
            vote_button_flow.add
            {
                type = 'button',
                name = poll_view_vote_name,
                caption = a.voted_count,
                enabled = poll_enabled
            }

        local vote_button_style = vote_button.style
        vote_button_style.height = 24
        vote_button_style.width = 26
        vote_button_style.font = 'default-small'
        vote_button_style.top_padding = 0
        vote_button_style.bottom_padding = 0
        vote_button_style.left_padding = 0
        vote_button_style.right_padding = 0

        Gui.set_data(vote_button, { answer = a, data = data })
        vote_buttons[i] = vote_button

        local label = grid.add { type = 'label', caption = a.text }
        label.style.single_line = false
        label.style.minimal_height = 24
        label.style.font = 'default-semibold'
        label.style.font_color = { r = 0.95, g = 0.95, b = 0.95 }
        label.style.left_padding = 4
        label.style.right_padding = 4
        label.style.bottom_padding = 4
    end

    data.vote_buttons = vote_buttons
end

local function update_poll_viewer(data)
    local back_button = data.back_button
    local forward_button = data.forward_button
    local poll_index_label = data.poll_index_label
    local poll_index = data.poll_index

    if #polls == 0 then
        poll_index = 0
    else
        poll_index = Math.clamp(poll_index, 1, #polls)
    end

    data.poll_index = poll_index

    if poll_index == 0 then
        poll_index_label.caption = 'No Polls'
    else
        poll_index_label.caption = table.concat { 'Poll ', poll_index, ' / ', #polls }
    end

    back_button.enabled = poll_index > 1
    forward_button.enabled = poll_index < #polls

    redraw_poll_viewer_content(data)
end

local function draw_main_frame(_, player)
    local main_frame, inside_frame = Gui.add_main_frame_with_toolbar(player, 'left', main_frame_name, nil, main_button_name, 'Polls')
    main_frame.style.maximal_width = 300

    local poll_viewer_top_flow = inside_frame.add { type = 'table', column_count = 5 }
    poll_viewer_top_flow.style.horizontal_spacing = 0

    local back_button = poll_viewer_top_flow.add { type = 'button', name = poll_view_back_name, caption = '◀' }
    apply_direction_button_style(back_button)

    local forward_button = poll_viewer_top_flow.add { type = 'button', name = poll_view_forward_name, caption = '▶' }
    apply_direction_button_style(forward_button)

    local poll_index_label = poll_viewer_top_flow.add { type = 'label' }
    poll_index_label.style.left_padding = 8

    local spacer = poll_viewer_top_flow.add { type = 'flow' }
    spacer.style.horizontally_stretchable = true

    local remaining_time_label = poll_viewer_top_flow.add { type = 'label' }

    local poll_viewer_content = inside_frame.add { type = 'scroll-pane' }
    poll_viewer_content.style.maximal_height = 480
    poll_viewer_content.style.width = 274

    local poll_index = player_poll_index[player.index] or #polls

    local data =
    {
        back_button = back_button,
        forward_button = forward_button,
        poll_index_label = poll_index_label,
        poll_viewer_content = poll_viewer_content,
        remaining_time_label = remaining_time_label,
        poll_index = poll_index
    }

    Gui.set_data(main_frame, data)
    Gui.set_data(back_button, data)
    Gui.set_data(forward_button, data)

    update_poll_viewer(data)

    local bottom_flow = inside_frame.add { type = 'flow', direction = 'horizontal' }

    local left_flow = bottom_flow.add { type = 'flow' }
    left_flow.style.horizontal_align = 'left'
    left_flow.style.horizontally_stretchable = true

    local right_flow = bottom_flow.add { type = 'flow' }
    right_flow.style.horizontal_align = 'right'

    if (Session.get_trusted_player(player) or player.admin) or settings.allow_non_trusted_to_create then
        local create_poll_button = right_flow.add { type = 'button', name = create_poll_button_name, caption = 'Create Poll' }
        apply_button_style(create_poll_button)
    else
        local create_poll_button =
            right_flow.add
            {
                type = 'button',
                caption = 'Create Poll',
                enabled = false,
                tooltip = 'Sorry, you need to be trusted to create polls.'
            }
        apply_button_style(create_poll_button)
    end
end

local function remove_create_poll_frame(create_poll_frame, player_index)
    local data = Gui.get_data(create_poll_frame)
    if not data then
        return
    end

    data.edit_mode = nil
    player_create_poll_data[player_index] = data

    Gui.remove_data_recursively(create_poll_frame)
    create_poll_frame.destroy()
end

local function remove_main_frame(main_frame, left, player)
    local player_index = player.index
    local data = Gui.get_data(main_frame)
    player_poll_index[player_index] = data.poll_index

    Gui.remove_data_recursively(main_frame)
    main_frame.destroy()

    local create_poll_frame = left[create_poll_frame_name]
    if create_poll_frame and create_poll_frame.valid then
        remove_create_poll_frame(create_poll_frame, player_index)
    end
end

local function toggle(event)
    local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Toggle Poll')
    if is_spamming then
        return
    end

    local left = event.player.gui.left
    local main_frame = left[main_frame_name]

    if main_frame then
        remove_main_frame(main_frame, left, event.player)
    else
        Gui.clear_all_active_frames(event.player)
        draw_main_frame(left, event.player)
    end
end

local function update_duration(slider)
    local slider_data = Gui.get_data(slider)
    local label = slider_data.duration_label
    local value = slider.slider_value

    value = math.floor(value)

    slider_data.data.duration = value * tick_duration_step

    if value == 0 then
        label.caption = 'Endless Poll.'
    else
        label.caption = value * duration_step .. ' sec.'
    end
end

local function redraw_create_poll_content(data)
    local grid = data.grid
    local answers = data.answers

    Gui.remove_data_recursively(grid)
    grid.clear()

    grid.add { type = 'flow' }
    grid.add
    {
        type = 'label',
        caption = 'Duration:',
        tooltip = 'Pro tip: Use mouse wheel or arrow keys for more fine control.'
    }

    local duration_flow = grid.add { type = 'flow', direction = 'horizontal' }
    local duration_slider =
        duration_flow.add
        {
            type = 'slider',
            name = create_poll_duration_name,
            minimum_value = 0,
            maximum_value = duration_slider_max,
            value = math.floor(data.duration * inv_tick_duration_step)
        }
    duration_slider.style.width = 100

    data.duration_slider = duration_slider

    local duration_label = duration_flow.add { type = 'label' }

    Gui.set_data(duration_slider, { duration_label = duration_label, data = data })

    update_duration(duration_slider)

    grid.add { type = 'flow' }
    local question_label = grid.add({ type = 'flow' }).add { type = 'label', name = create_poll_label_name, caption = 'Question:' }

    local question_textfield = grid.add({ type = 'flow' }).add { type = 'textfield', name = create_poll_question_name, text = data.question }
    question_textfield.style.width = 170

    Gui.set_data(question_label, question_textfield)
    Gui.set_data(question_textfield, data)

    local edit_mode = data.edit_mode
    for count, answer in pairs(answers) do
        local delete_flow = grid.add { type = 'flow' }

        local delete_button
        if edit_mode or count ~= 1 then
            delete_button =
                delete_flow.add
                {
                    type = 'sprite-button',
                    name = create_poll_delete_answer_name,
                    sprite = 'utility/trash',
                    tooltip = 'Delete answer field.'
                }
            delete_button.style.height = 26
            delete_button.style.width = 26
        else
            delete_flow.style.height = 26
            delete_flow.style.width = 26
        end

        local label_flow = grid.add { type = 'flow' }
        local label =
            label_flow.add
            {
                type = 'label',
                name = create_poll_label_name,
                caption = table.concat { 'Answer #', count, ':' }
            }

        local textfield_flow = grid.add { type = 'flow' }

        local textfield = textfield_flow.add { type = 'textfield', name = create_poll_answer_name, text = answer.text }
        textfield.style.width = 170
        Gui.set_data(textfield, { answers = answers, count = count })

        if delete_button then
            Gui.set_data(delete_button, { data = data, count = count })
        end

        Gui.set_data(label, textfield)
    end
end

local function draw_create_poll_frame(parent, player, previous_data)
    previous_data = previous_data or player_create_poll_data[player.index]

    local edit_mode
    local question
    local answers
    local duration
    local title_text
    local confirm_text
    local confirm_name
    if previous_data then
        edit_mode = previous_data.edit_mode

        question = previous_data.question

        answers = {}
        for i, a in pairs(previous_data.answers) do
            answers[i] = { text = a.text, source = a }
        end

        duration = previous_data.duration
    else
        question = ''
        answers = { { text = '' }, { text = '' }, { text = '' } }
        duration = default_poll_duration
    end

    if edit_mode then
        title_text = 'Edit Poll #' .. previous_data.id
        confirm_text = 'Edit Poll'
        confirm_name = create_poll_edit_name
    else
        title_text = 'New Poll'
        confirm_text = 'Create Poll'
        confirm_name = create_poll_confirm_name
    end

    local frame = parent.add { type = 'frame', name = create_poll_frame_name, caption = title_text, direction = 'vertical' }
    frame.style.maximal_width = 320

    local scroll_pane = frame.add { type = 'scroll-pane', vertical_scroll_policy = 'always' }
    scroll_pane.style.maximal_height = 250
    scroll_pane.style.maximal_width = 300
    scroll_pane.style.padding = 3

    local grid = scroll_pane.add { type = 'table', column_count = 3 }

    local data =
    {
        frame = frame,
        grid = grid,
        question = question,
        answers = answers,
        duration = duration,
        previous_data = previous_data,
        edit_mode = edit_mode
    }

    Gui.set_data(frame, data)

    redraw_create_poll_content(data)

    local add_answer_button =
        scroll_pane.add
        {
            type = 'button',
            name = create_poll_add_answer_name,
            caption = 'Add Answer'
        }
    apply_button_style(add_answer_button)
    Gui.set_data(add_answer_button, data)

    local bottom_flow = frame.add { type = 'flow', direction = 'horizontal' }

    local left_flow = bottom_flow.add { type = 'flow' }
    left_flow.style.horizontal_align = 'left'
    left_flow.style.horizontally_stretchable = true

    local close_button = left_flow.add { type = 'button', name = create_poll_close_name, caption = 'Close' }
    apply_button_style(close_button)
    Gui.set_data(close_button, frame)

    local clear_button = left_flow.add { type = 'button', name = create_poll_clear_name, caption = 'Clear' }
    apply_button_style(clear_button)
    Gui.set_data(clear_button, data)

    local right_flow = bottom_flow.add { type = 'flow' }
    right_flow.style.horizontal_align = 'right'

    if edit_mode then
        local delete_button = right_flow.add { type = 'button', name = create_poll_delete_name, caption = 'Delete' }
        apply_button_style(delete_button)
        Gui.set_data(delete_button, data)
    end

    local confirm_button = right_flow.add { type = 'button', name = confirm_name, caption = confirm_text }
    apply_button_style(confirm_button)
    Gui.set_data(confirm_button, data)
end

local function show_new_poll(poll_data)
    local message = table.concat { poll_data.created_by, ' has created a new Poll #', poll_data.id, ': ', poll_data.question }

    for _, p in pairs(game.connected_players) do
        local left = p.gui.left
        local frame = left[main_frame_name]
        if no_notify_players[p.index] then
            if frame and frame.valid then
                local data = Gui.get_data(frame)
                update_poll_viewer(data)
            end
        else
            p.print(message)

            if frame and frame.valid then
                local data = Gui.get_data(frame)
                data.poll_index = #polls
                update_poll_viewer(data)
            else
                player_poll_index[p.index] = nil
                draw_main_frame(left, p)
            end
        end
    end
end

local function create_poll(event)
    local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Create Poll Confirm')
    if is_spamming then
        return
    end
    local player = event.player
    local data = Gui.get_data(event.element)

    if not data then
        return
    end

    local frame = data.frame
    local question = data.question

    if not question:find('%S') then
        event.player.print('Sorry, the poll needs a question.')
        return
    end

    local answers = {}
    for _, a in pairs(data.answers) do
        local text = a.text
        if text:find('%S') then
            local index = #answers + 1
            answers[index] = { text = text, index = index, voted_count = 0 }
        end
    end

    if #answers < 1 then
        player.print('Sorry, the poll needs at least one answer.')
        return
    end

    player_create_poll_data[player.index] = nil

    local tick = game.tick
    local duration = data.duration
    local end_tick

    if duration == 0 then
        end_tick = -1
    else
        end_tick = tick + duration
    end

    local name = ''
    if event.player and event.player.valid then
        name = event.player.name
    end

    local poll_data =
    {
        id = poll_id(),
        question = question,
        answers = answers,
        voters = {},
        start_tick = tick,
        end_tick = end_tick,
        duration = duration,
        created_by = name,
        edited_by = {}
    }

    insert(polls, poll_data)
    insert(running_polls, poll_data)

    show_new_poll(poll_data)
    send_poll_result_to_discord(poll_data)

    Gui.remove_data_recursively(frame)
    frame.destroy()
end

local function update_vote(answer, direction)
    local count = answer.voted_count + direction
    answer.voted_count = count

    return tostring(count)
end

local function vote(event)
    local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Poll Vote')
    if is_spamming then
        return
    end

    local player_index = event.player_index
    local player = game.get_player(player_index)
    if player and player.valid and not settings.allow_non_trusted_to_vote and not player.admin then
        if not Session.get_trusted_player(player) then
            player.print('Sorry, you need to be trusted to vote on polls.')
            player.print('You can become trusted by asking an admin to use the /trust command on you.')
            return
        end
    end
    local voted_button = event.element
    local button_data = Gui.get_data(voted_button)
    local answer = button_data.answer

    local poll_index = button_data.data.poll_index
    local poll = polls[poll_index]

    local voters = poll.voters

    local previous_vote_answer = voters[player_index]
    if previous_vote_answer == answer then
        return
    end

    local vote_index = answer.index

    voters[player_index] = answer

    local previous_vote_button_count
    local previous_vote_index
    if previous_vote_answer then
        previous_vote_button_count = update_vote(previous_vote_answer, -1)
        previous_vote_index = previous_vote_answer.index
    end

    local vote_button_count = update_vote(answer, 1)

    for _, p in pairs(game.connected_players) do
        local frame = p.gui.left[main_frame_name]
        if frame and frame.valid then
            local data = Gui.get_data(frame)

            if data.poll_index == poll_index then
                local vote_buttons = data.vote_buttons
                if previous_vote_answer then
                    local vote_button = vote_buttons[previous_vote_index]
                    vote_button.caption = previous_vote_button_count
                end

                local vote_button = vote_buttons[vote_index]
                vote_button.caption = vote_button_count
            end
        end
    end
end

local function player_joined(event)
    local player = game.get_player(event.player_index)
    if not player or not player.valid then
        return
    end

    if Gui.get_mod_gui_top_frame() then
        local button =
            Gui.add_mod_button(
                player,
                {
                    type = 'sprite-button',
                    name = main_button_name,
                    sprite = 'item/programmable-speaker',
                    tooltip = 'Poll your thoughts',
                    style = Gui.button_style
                }
            )
        if button then
            button.style.font_color = { 165, 165, 165 }
            button.style.font = 'default-semibold'
            button.style.minimal_height = 36
            button.style.maximal_height = 36
            button.style.minimal_width = 40
            button.style.padding = -2
        end
    else
        if player.gui.top[main_button_name] ~= nil then
            local frame = player.gui.top[main_frame_name]
            if frame and frame.valid then
                local data = Gui.get_data(frame)
                update_poll_viewer(data)
            end
        else
            local b =
                player.gui.top.add
                {
                    type = 'sprite-button',
                    name = main_button_name,
                    sprite = 'item/programmable-speaker',
                    tooltip = 'Create a poll',
                    style = Gui.button_style
                }
            b.style.maximal_height = 38
        end
    end
end

local function poll_complete(poll)
    local end_tick = poll.end_tick
    if end_tick == -1 then
        return false
    end

    local ticks = end_tick - game.tick
    return ticks < 0
end

local function tick()
    for _, p in pairs(game.connected_players) do
        local frame = p.gui.left[main_frame_name]
        if frame and frame.valid then
            local data = Gui.get_data(frame)
            local poll = polls[data.poll_index]
            if poll then
                local poll_enabled = do_remaining_time(poll, data.remaining_time_label)

                if not poll_enabled then
                    for _, v in pairs(data.vote_buttons) do
                        v.enabled = poll_enabled
                    end
                end
            end
        end
        local player_index = p.index
        local tbl = player_create_poll_data[player_index]
        if tbl then
            for _, element in pairs(tbl) do
                if type(element) == 'table' then
                    if not element.valid then
                        player_create_poll_data[player_index] = nil
                    end
                end
            end
        end
    end

    if not running_polls or not next(running_polls) then
        return
    end

    for i = #running_polls, 1, -1 do
        local poll = running_polls[i]
        if poll_complete(poll) then
            table.remove(running_polls, i)
            send_poll_result_to_discord(poll, true)

            local poll_result, winning_answer = Public.poll_result(poll.id)

            Event.raise(CustomEvents.events.on_poll_complete, { player_index = poll.player_index, poll_id = poll.id, custom_data = poll.custom_data, poll_result = poll_result, winning_answer = winning_answer })

            local message = table.concat { 'Poll finished: Poll #', poll.id, ': ', poll.question }
            for _, p in pairs(game.connected_players) do
                if not no_notify_players[p.index] then
                    p.print(message)
                end
            end
        end
    end
end

Event.add(CustomEvents.events.on_poll_created, function (event)
    Public.poll(event)
end)

Event.add(defines.events.on_player_joined_game, player_joined)
Event.add(defines.events.on_player_created, player_joined)
Event.on_nth_tick(60, tick)

Gui.on_click(main_button_name, toggle)

Gui.on_click(
    create_poll_button_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Create Poll')
        if is_spamming then
            return
        end
        local player = event.player
        local left = player.gui.left
        local frame = left[create_poll_frame_name]
        if frame and frame.valid then
            remove_create_poll_frame(frame, player.index)
        else
            draw_create_poll_frame(left, player)
        end
    end
)

Gui.on_click(
    poll_view_edit_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Poll View Edit')
        if is_spamming then
            return
        end
        local player = event.player
        local left = player.gui.left
        local frame = left[create_poll_frame_name]

        if frame and frame.valid then
            Gui.remove_data_recursively(frame)
            frame.destroy()
        end

        local main_frame = left[main_frame_name]
        local frame_data = Gui.get_data(main_frame)
        local poll = polls[frame_data.poll_index]

        poll.edit_mode = true
        draw_create_poll_frame(left, player, poll)
    end
)

Gui.on_value_changed(
    create_poll_duration_name,
    function (event)
        update_duration(event.element)
    end
)

Gui.on_click(
    create_poll_delete_answer_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Create Poll Delete Answer')
        if is_spamming then
            return
        end
        local button_data = Gui.get_data(event.element)
        if not button_data then
            return
        end
        local data = button_data.data

        if not data then
            return
        end

        table.remove(data.answers, button_data.count)
        redraw_create_poll_content(data)
    end
)

Gui.on_click(
    create_poll_label_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Create Poll Label Name')
        if is_spamming then
            return
        end
        local textfield = Gui.get_data(event.element)
        if not textfield then
            return
        end

        if textfield and textfield.valid then
            textfield.focus()
        end
    end
)

Gui.on_text_changed(
    create_poll_question_name,
    function (event)
        local textfield = event.element
        local data = Gui.get_data(textfield)

        if not data then
            return
        end

        if textfield and textfield.valid then
            if string.len(textfield.text) >= 50 then
                textfield.text = ''
                return
            end
            data.question = textfield.text
        end
    end
)

Gui.on_text_changed(
    create_poll_answer_name,
    function (event)
        local textfield = event.element
        local data = Gui.get_data(textfield)

        if not data then
            return
        end

        if textfield and textfield.valid then
            if string.len(textfield.text) >= 50 then
                textfield.text = ''
                return
            end
            data.answers[data.count].text = textfield.text
        end
    end
)

Gui.on_click(
    create_poll_add_answer_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Create Poll Add Answer')
        if is_spamming then
            return
        end
        local data = Gui.get_data(event.element)

        if not data then
            return
        end

        if data and #data.answers > 10 then
            return
        end

        insert(data.answers, { text = '' })
        redraw_create_poll_content(data)
    end
)

Gui.on_click(
    create_poll_close_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Create Poll Close')
        if is_spamming then
            return
        end
        local frame = Gui.get_data(event.element)
        if frame and frame.valid then
            remove_create_poll_frame(frame, event.player_index)
        end
    end
)

Gui.on_click(
    create_poll_clear_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Create Poll Clear')
        if is_spamming then
            return
        end
        local data = Gui.get_data(event.element)
        if not data then
            return
        end

        local slider = data.duration_slider
        slider.slider_value = math.floor(default_poll_duration * inv_tick_duration_step)
        update_duration(slider)

        data.question = ''

        local answers = data.answers
        for i = 1, #answers do
            answers[i].text = ''
        end

        redraw_create_poll_content(data)
    end
)

Gui.on_click(create_poll_confirm_name, create_poll)

Gui.on_click(
    create_poll_delete_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Create Poll Delete')
        if is_spamming then
            return
        end
        local player = event.player
        local data = Gui.get_data(event.element)
        if not data then
            return
        end

        local frame = data.frame
        local poll = data.previous_data

        Gui.remove_data_recursively(frame)
        frame.destroy()

        player_create_poll_data[player.index] = nil

        local removed_index
        for i, p in pairs(polls) do
            if p == poll then
                table.remove(polls, i)
                remove_element(running_polls, p)
                removed_index = i
                break
            end
        end

        if not removed_index then
            return
        end

        local message = table.concat { player.name, ' has deleted Poll #', poll.id, ': ', poll.question }

        for _, p in pairs(game.connected_players) do
            if not no_notify_players[p.index] then
                p.print(message)
            end

            local main_frame = p.gui.left[main_frame_name]
            if main_frame and main_frame.valid then
                local main_frame_data = Gui.get_data(main_frame)
                local poll_index = main_frame_data.poll_index

                if removed_index < poll_index then
                    main_frame_data.poll_index = poll_index - 1
                end

                update_poll_viewer(main_frame_data)
                toggle(event)
            end
        end
    end
)

Gui.on_click(
    create_poll_edit_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Create Poll Edit')
        if is_spamming then
            return
        end
        local player = event.player
        local data = Gui.get_data(event.element)
        if not data then
            return
        end

        local frame = data.frame
        local poll = data.previous_data

        local new_question = data.question
        if not new_question:find('%S') then
            player.print('Sorry, the poll needs a question.')
            return
        end

        local new_answer_set = {}
        local new_answers = {}
        for _, a in pairs(data.answers) do
            if a.text:find('%S') then
                local source = a.source
                local index = #new_answers + 1
                if source then
                    new_answer_set[source] = a
                    source.text = a.text
                    source.index = index
                    new_answers[index] = source
                else
                    new_answers[index] = { text = a.text, index = index, voted_count = 0 }
                end
            end
        end

        if not next(new_answers) then
            player.print('Sorry, the poll needs at least one answer.')
            return
        end

        Gui.remove_data_recursively(frame)
        frame.destroy()

        local player_index = player.index

        player_create_poll_data[player_index] = nil

        local old_answers = poll.answers
        local voters = poll.voters
        for _, a in pairs(old_answers) do
            if not new_answer_set[a] then
                for pi, a2 in pairs(voters) do
                    if a == a2 then
                        voters[pi] = nil
                    end
                end
            end
        end

        poll.question = new_question
        poll.answers = new_answers
        poll.edited_by[player_index] = true

        local start_tick = game.tick
        local duration = data.duration
        local end_tick

        if duration == 0 then
            end_tick = -1
        else
            end_tick = start_tick + duration
        end

        poll.start_tick = start_tick
        poll.end_tick = end_tick
        poll.duration = duration

        local poll_index
        for i, p in pairs(polls) do
            if poll == p then
                poll_index = i
                break
            end
        end

        if not poll_index then
            insert(polls, poll)
            insert(running_polls, poll)
            poll_index = #polls
            ---@diagnostic disable-next-line: undefined-global
        elseif not contains(running_polls, poll) then
            insert(running_polls, poll)
        end

        local message = table.concat { player.name, ' has edited Poll #', poll.id, ': ', poll.question }

        for _, p in pairs(game.connected_players) do
            local main_frame = p.gui.left[main_frame_name]

            if no_notify_players[p.index] then
                if main_frame and main_frame.valid then
                    local main_frame_data = Gui.get_data(main_frame)
                    update_poll_viewer(main_frame_data)
                end
            else
                p.print(message)
                if main_frame and main_frame.valid then
                    local main_frame_data = Gui.get_data(main_frame)
                    main_frame_data.poll_index = poll_index
                    update_poll_viewer(main_frame_data)
                else
                    draw_main_frame(p.gui.left, p)
                end
            end
        end
    end
)

Gui.on_checked_state_changed(
    notify_checkbox_name,
    function (event)
        local player_index = event.player_index
        local checkbox = event.element

        local new_state
        if checkbox.state then
            new_state = nil
        else
            new_state = true
        end

        no_notify_players[player_index] = new_state
    end
)

local function do_direction(event, sign)
    local count
    if event.shift then
        count = #polls
    else
        local button = event.button
        if button == defines.mouse_button_type.right then
            count = 5
        else
            count = 1
        end
    end

    count = count * sign

    local data = Gui.get_data(event.element)
    data.poll_index = data.poll_index + count
    update_poll_viewer(data)
end

Gui.on_click(
    poll_view_back_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Poll View Back')
        if is_spamming then
            return
        end
        do_direction(event, -1)
    end
)

Gui.on_click(
    poll_view_forward_name,
    function (event)
        local is_spamming = SpamProtection.is_spamming(event.player, nil, 'Poll View Forward')
        if is_spamming then
            return
        end
        do_direction(event, 1)
    end
)

Gui.on_click(poll_view_vote_name, vote)

function Public.reset()
    for k, _ in pairs(polls) do
        polls[k] = nil
    end
    for k, _ in pairs(player_poll_index) do
        player_poll_index[k] = nil
    end
    for k, _ in pairs(player_create_poll_data) do
        player_create_poll_data[k] = nil
    end
    for _, p in pairs(game.connected_players) do
        local main_frame = p.gui.left[main_frame_name]
        if main_frame and main_frame.valid then
            local main_frame_data = Gui.get_data(main_frame)
            update_poll_viewer(main_frame_data)
            remove_main_frame(main_frame, p.gui.left, p)
        end
    end
end

function Public.get_no_notify_players()
    return no_notify_players
end

function Public.validate(data)
    if type(data) ~= 'table' then
        return false, 'argument must be of type table'
    end

    local question = data.question
    if type(question) ~= 'string' or question == '' then
        return false, 'field question must be a non empty string.'
    end

    local answers = data.answers
    if type(answers) ~= 'table' then
        return false, 'answers field must be an array.'
    end

    if #answers == 0 then
        return false, 'answer array must contain at least one entry.'
    end

    for _, a in pairs(answers) do
        if type(a) ~= 'string' or a == '' then
            return false, 'answers must be a non empty string.'
        end
    end

    local duration = data.duration
    local duration_type = type(duration)
    if duration_type == 'number' then
        if duration < 0 then
            return false, 'duration cannot be negative, set duration to 0 for endless poll.'
        end
    elseif duration_type ~= 'nil' then
        return false, 'duration must be of type number or nil'
    end

    return true
end

--[[ local d = {
    question = 'What is your favorite color?',
    answers = { 'Red', 'Green', 'Blue' },
    duration = 5
}

local Poll = require 'utils.gui.poll' Poll.poll(d) ]]

function Public.poll(data)
    local suc, error = Public.validate(data)
    if not suc then
        return false, error
    end

    local answers = {}
    for index, a in pairs(data.answers) do
        if a ~= '' then
            insert(answers, { text = a, index = index, voted_count = 0 })
        end
    end

    local duration = data.duration
    if duration then
        duration = duration * 60
    else
        duration = default_poll_duration
    end

    local start_tick = game.tick
    local end_tick
    if duration == 0 then
        end_tick = -1
    else
        end_tick = start_tick + duration
    end

    local id = poll_id()

    local name = nil
    if game.player and game.player.valid then
        name = game.player.name
    end

    local poll_data =
    {
        id = id,
        custom_data = data.custom_data or nil,
        question = data.question,
        answers = answers,
        voters = {},
        start_tick = start_tick,
        end_tick = end_tick,
        duration = duration,
        created_by = name or '<server>',
        created_by_script = true,
        edited_by = {}
    }

    insert(polls, poll_data)
    insert(running_polls, poll_data)


    show_new_poll(poll_data)
    send_poll_result_to_discord(poll_data)

    return true, id
end

function Public.poll_result(id)
    if type(id) ~= 'number' then
        return 'poll-id must be a number'
    end

    for _, poll_data in pairs(polls) do
        if poll_data.id == id then
            local result = { 'Question: ', poll_data.question, ' Answers: ' }
            local answers = poll_data.answers
            local answers_count = #answers
            local highest_vote = 0
            local winners = {}

            for i, a in ipairs(answers) do
                insert(result, '( [')
                insert(result, a.voted_count)
                insert(result, '] - ')
                insert(result, a.text)
                insert(result, ' )')

                if a.voted_count > highest_vote then
                    highest_vote = a.voted_count
                    winners = { a }
                elseif a.voted_count == highest_vote then
                    insert(winners, a)
                end

                if i ~= answers_count then
                    insert(result, ', ')
                end
            end

            local winning_answer
            if #winners == 1 then
                winning_answer = winners[1]
            else
                winning_answer = { text = 'Tie', voted_count = highest_vote }
            end

            return table.concat(result), winning_answer
        end
    end

    return table.concat { 'poll #', id, ' not found' }
end

function Public.send_poll_result_to_discord(id)
    if type(id) ~= 'number' then
        Server.output_script_data('poll-id must be a number')
        return
    end

    for _, poll_data in pairs(polls) do
        if poll_data.id == id then
            send_poll_result_to_discord(poll_data)
            return
        end
    end

    local message = table.concat { 'poll #', id, ' not found' }
    Server.output_script_data(message)
end

function Public.poll_complete(id)
    if type(id) ~= 'number' then
        return 'poll-id must be a number'
    end

    for _, poll_data in pairs(polls) do
        if poll_data.id == id then
            return poll_complete(poll_data)
        end
    end
end

function Public.edit_poll(id, question, answers, duration)
    if type(id) ~= 'number' then
        return 'poll-id must be a number'
    end

    local poll_data = polls[id]
    if not poll_data then
        return 'poll not found'
    end

    local new_question = question
    if not new_question:find('%S') then
        return false, 'question must be a non empty string'
    end

    local new_answers = {}
    for _, a in pairs(answers) do
        if a.text:find('%S') then
            local source = a.source
            local index = #new_answers + 1
            if source then
                source.text = a.text
                source.index = index
                source.voted_count = 0
                new_answers[index] = source
            else
                new_answers[index] = { text = a.text, index = index, voted_count = 0 }
            end
        end
    end

    if not next(new_answers) then
        return false, 'answer array must contain at least one entry'
    end

    poll_data.question = question
    poll_data.answers = new_answers
    poll_data.duration = duration
    poll_data.end_tick = game.tick + duration

    for _, p in pairs(game.connected_players) do
        local main_frame = p.gui.left[main_frame_name]

        if no_notify_players[p.index] then
            if main_frame and main_frame.valid then
                local main_frame_data = Gui.get_data(main_frame)
                update_poll_viewer(main_frame_data)
            end
        else
            if main_frame and main_frame.valid then
                local main_frame_data = Gui.get_data(main_frame)
                main_frame_data.poll_index = id
                update_poll_viewer(main_frame_data)
            else
                draw_main_frame(p.gui.left, p)
            end
        end
    end

    return true
end

Public.main_button_name = main_button_name

return Public
