local Global = require 'utils.global'
local Session = require 'utils.datastore.session_data'
local Game = require 'utils.game'
local Token = require 'utils.token'
local Task = require 'utils.task'
local Server = require 'utils.server'
local Event = require 'utils.event'
local Utils = require 'utils.core'

local jailed_data_set = 'jailed'
local jailed = {}
local player_data = {}
local terms_tbl = {}
local votejail = {}
local votefree = {}
local settings = {
    playtime_for_vote = 25920000, -- 5 days
    playtime_for_instant_jail = 103680000, -- 20 days
    clear_voted_player = 36000, -- remove player from vote-tbl after 10 minutes
    clear_terms_tbl = 3600,
    votejail_count = 5,
    valid_surface = 'nauvis'
}

local set_data = Server.set_data
local try_get_data = Server.try_get_data
local concat = table.concat

local valid_commands = {
    ['free'] = true,
    ['jail'] = true
}

Global.register(
    {
        jailed = jailed,
        votejail = votejail,
        votefree = votefree,
        settings = settings,
        player_data = player_data,
        terms_tbl = terms_tbl
    },
    function(t)
        jailed = t.jailed
        votejail = t.votejail
        votefree = t.votefree
        settings = t.settings
        player_data = t.player_data
        terms_tbl = t.terms_tbl
    end
)

local Public = {}

local validate_entity = function(entity)
    if not (entity and entity.valid) then
        return false
    end

    return true
end

local clear_terms_tbl =
    Token.register(
    function(data)
        local player = data.player
        if not player then
            return
        end

        if terms_tbl[player] then
            terms_tbl[player] = nil
            return
        end
    end
)

local play_alert_sound =
    Token.register(
    function(data)
        local name = data.name
        if not name then
            return
        end
        local player = game.get_player(name)
        if not player or not player.valid then
            return
        end

        player.play_sound {path = 'utility/scenario_message', volume_modifier = 1}
    end
)

local clear_jail_data_token =
    Token.register(
    function(data)
        local griefer = data.griefer
        if not griefer then
            return
        end
        if votejail[griefer] and votejail[griefer].jailed then
            return
        end

        local msg_two = 'You have been cleared of all accusations because not enough players voted against you.'
        Utils.print_to(griefer, msg_two)
        votejail[griefer] = nil
        votefree[griefer] = nil
    end
)

local clear_gui =
    Token.register(
    function(data)
        local player = data.player
        if player and player.valid then
            for _, child in pairs(player.gui.center.children) do
                child.destroy()
            end
            for _, child in pairs(player.gui.left.children) do
                child.destroy()
            end
        end
    end
)

local validate_playtime = function(player)
    local tracker = Session.get_session_table()

    local playtime = player.online_time

    if tracker[player.name] then
        playtime = player.online_time + tracker[player.name]
    end

    return playtime
end

local validate_trusted = function(player)
    local trusted = Session.get_trusted_table()

    local is_trusted = false

    if trusted[player.name] then
        is_trusted = true
    end

    return is_trusted
end

local get_player_data = function(player, remove)
    if remove and player_data[player.name] then
        player_data[player.name] = nil
        return
    end
    if not player_data[player.name] then
        player_data[player.name] = {}
    end
    return player_data[player.name]
end

local get_gulag_permission_group = function()
    local gulag = game.permissions.get_group('gulag')
    if not gulag then
        gulag = game.permissions.create_group('gulag')
        for action_name, _ in pairs(defines.input_action) do
            gulag.set_allows_action(defines.input_action[action_name], false)
        end
        gulag.set_allows_action(defines.input_action.write_to_console, true)
    end

    return gulag
end

local create_gulag_surface = function()
    local surface = game.surfaces['gulag']
    if not surface then
        local walls = {}
        local tiles = {}
        pcall(
            function()
                surface =
                    game.create_surface(
                    'gulag',
                    {
                        autoplace_controls = {
                            ['coal'] = {frequency = 23, size = 3, richness = 3},
                            ['stone'] = {frequency = 20, size = 3, richness = 3},
                            ['copper-ore'] = {frequency = 25, size = 3, richness = 3},
                            ['iron-ore'] = {frequency = 35, size = 3, richness = 3},
                            ['uranium-ore'] = {frequency = 20, size = 3, richness = 3},
                            ['crude-oil'] = {frequency = 80, size = 3, richness = 1},
                            ['trees'] = {frequency = 0.75, size = 2, richness = 0.1},
                            ['enemy-base'] = {frequency = 15, size = 0, richness = 1}
                        },
                        cliff_settings = {cliff_elevation_0 = 1024, cliff_elevation_interval = 10, name = 'cliff'},
                        height = 64,
                        width = 256,
                        peaceful_mode = false,
                        seed = 1337,
                        starting_area = 'very-low',
                        starting_points = {{x = 0, y = 0}},
                        terrain_segmentation = 'normal',
                        water = 'normal'
                    }
                )
            end
        )
        if not surface then
            surface = game.create_surface('gulag', {width = 40, height = 40})
        end
        surface.always_day = true
        surface.request_to_generate_chunks({0, 0}, 9)
        surface.force_generate_chunk_requests()
        local area = {left_top = {x = -128, y = -32}, right_bottom = {x = 128, y = 32}}
        for x = area.left_top.x, area.right_bottom.x, 1 do
            for y = area.left_top.y, area.right_bottom.y, 1 do
                tiles[#tiles + 1] = {name = 'black-refined-concrete', position = {x = x, y = y}}
                if x == area.left_top.x or x == area.right_bottom.x or y == area.left_top.y or y == area.right_bottom.y then
                    walls[#walls + 1] = {name = 'stone-wall', force = 'neutral', position = {x = x, y = y}}
                end
            end
        end
        surface.set_tiles(tiles)
        for _, entity in pairs(walls) do
            local e = surface.create_entity(entity)
            e.destructible = false
            e.minable = false
        end

        rendering.draw_text {
            text = 'The pit of despair ☹',
            surface = surface,
            target = {0, -50},
            color = {r = 0.98, g = 0.66, b = 0.22},
            scale = 10,
            font = 'heading-1',
            alignment = 'center',
            scale_with_zoom = false
        }
    end
    surface = game.surfaces['gulag']
    return surface
end

local teleport_player_to_gulag = function(player, action)
    local p_data = get_player_data(player)

    if action == 'jail' then
        local gulag = game.surfaces['gulag']
        if p_data and not p_data.locked then
            p_data.fallback_surface_index = player.surface.index
            p_data.position = player.position
            p_data.p_group_id = player.permission_group.group_id
            p_data.locked = true
        end
        player.teleport(gulag.find_non_colliding_position('character', {0, 0}, 128, 1), gulag.name)
        local data = {
            player = player
        }
        Task.set_timeout_in_ticks(5, clear_gui, data)
    elseif action == 'free' then
        jailed[player.name] = nil
        if votejail[player.name] then
            votejail[player.name] = nil
        end
        if votefree[player.name] then
            votefree[player.name] = nil
        end

        local surface = game.surfaces[p_data.fallback_surface_index]
        if not surface or not surface.valid then
            if settings.valid_surface then
                surface = game.surfaces[settings.valid_surface]
            end
        end

        local p = p_data.position
        local p_group = game.permissions.get_group(p_data.p_group_id)
        p_group.add_player(player)
        local pos = {x = p.x, y = p.y}
        local get_tile = surface.get_tile(pos)
        if get_tile.valid and get_tile.name == 'out-of-map' then
            player.teleport(surface.find_non_colliding_position('character', game.forces.player.get_spawn_position(surface), 128, 1), surface.name)
        else
            player.teleport(surface.find_non_colliding_position('character', p, 128, 1), surface.name)
        end

        get_player_data(player, true)
    end
end

local validate_args = function(data)
    local player = data.player
    local griefer = data.griefer
    local trusted = data.trusted
    local playtime = data.playtime
    local message = data.message
    local cmd = data.cmd

    if not griefer then
        return
    end

    if not type(griefer) == 'string' then
        Utils.print_to(player, 'Invalid name.')
        return false
    end

    local get_griefer_player = game.get_player(griefer)

    if not validate_entity(get_griefer_player) then
        Utils.print_to(player, 'Invalid name.')
        return false
    end

    if not griefer or not get_griefer_player then
        Utils.print_to(player, 'Invalid name.')
        return false
    end

    if votejail[player.name] and not player.admin then
        Utils.print_to(player, 'You are currently being investigated since you have griefed.')
        return false
    end

    if votefree[player.name] and not player.admin then
        Utils.print_to(player, 'You are currently being investigated since you have griefed.')
        return false
    end

    if jailed[player.name] and not player.admin then
        Utils.print_to(player, 'You are jailed, you can´t run this command.')
        return false
    end

    if player.name == griefer and not player.admin then
        Utils.print_to(player, 'You can´t select yourself.')
        return false
    end

    if get_griefer_player.admin and not player.admin then
        Utils.print_to(player, 'You can´t select an admin.')
        return false
    end

    if not trusted and not player.admin or playtime <= settings.playtime_for_vote and not player.admin then
        Utils.print_to(player, 'You are not trusted enough to run this command.')
        return false
    end

    if not message then
        Utils.print_to(player, 'No valid reason was given.')
        return false
    end

    if cmd == 'jail' and message and string.len(message) <= 0 then
        Utils.print_to(player, 'No valid reason was given.')
        return false
    end

    if cmd == 'jail' and message and string.len(message) <= 10 then
        Utils.print_to(player, 'Reason is too short.')
        return false
    end

    return true
end

local vote_to_jail = function(player, griefer, msg)
    if not griefer then
        return
    end

    if type(griefer) == 'table' then
        griefer = griefer.name
    end

    if not votejail[griefer] then
        votejail[griefer] = {index = 0, actor = player.name}
        local message = player.name .. ' has started a vote to jail player ' .. griefer
        Utils.print_to(nil, message)
        Task.set_timeout_in_ticks(settings.clear_voted_player, clear_jail_data_token, {griefer = griefer})
    end

    if not votejail[griefer][player.name] then
        votejail[griefer][player.name] = true
        votejail[griefer].index = votejail[griefer].index + 1
        Utils.print_to(player, 'You have voted to jail player ' .. griefer .. '.')
        if votejail[griefer].index >= settings.votejail_count or (votejail[griefer].index == #game.connected_players - 1 and #game.connected_players > votejail[griefer].index) then
            Public.try_ul_data(griefer, true, votejail[griefer].actor, msg)
        end
    else
        Utils.print_to(player, 'You have already voted to kick ' .. griefer .. '.')
    end
end

local vote_to_free = function(player, griefer)
    if not griefer then
        return
    end

    if type(griefer) == 'table' then
        griefer = griefer.name
    end

    if not votefree[griefer] then
        votefree[griefer] = {index = 0, actor = player.name}
        local message = player.name .. ' has started a vote to free player ' .. griefer
        Utils.print_to(nil, message)
    end

    if not votefree[griefer][player.name] then
        votefree[griefer][player.name] = true
        votefree[griefer].index = votefree[griefer].index + 1

        Utils.print_to(player, 'You have voted to free player ' .. griefer .. '.')
        if votefree[griefer].index >= settings.votejail_count or (votefree[griefer].index == #game.connected_players - 1 and #game.connected_players > votefree[griefer].index) then
            Public.try_ul_data(griefer, false, votefree[griefer].actor)
            votejail[griefer] = nil
            votefree[griefer] = nil
        end
    else
        Utils.print_to(player, 'You have already voted to free ' .. griefer .. '.')
    end
    return
end

local jail = function(player, griefer, msg, raised)
    player = player or 'script'

    if jailed[griefer] then
        return false
    end

    if not msg then
        return
    end

    if not game.players[griefer] then
        return
    end

    local g = game.players[griefer]

    teleport_player_to_gulag(g, 'jail')

    local gulag = get_gulag_permission_group()
    gulag.add_player(griefer)

    local message = griefer .. ' has been jailed by ' .. player .. '. Cause: ' .. msg

    if game.players[griefer].character and game.players[griefer].character.valid and game.players[griefer].character.driving then
        game.players[griefer].character.driving = false
    end

    jailed[griefer] = {jailed = true, actor = player, reason = msg}
    if not raised then
        set_data(jailed_data_set, griefer, {jailed = true, actor = player, reason = msg})
    end

    Utils.print_to(nil, message)
    Utils.action_warning_embed('[Jailed]', message)

    if votejail[griefer] then
        votejail[griefer].jailed = true
    end

    game.players[griefer].clear_console()
    Utils.print_to(griefer, message)
    return true
end

local free = function(player, griefer)
    player = player or 'script'
    if not jailed[griefer] then
        return false
    end

    if not game.players[griefer] then
        return
    end

    local g = game.players[griefer]
    teleport_player_to_gulag(g, 'free')

    local message = griefer .. ' was set free from jail by ' .. player .. '.'

    set_data(jailed_data_set, griefer, nil)

    Utils.print_to(nil, message)
    Utils.action_warning_embed('[Jailed]', message)
    return true
end

local is_jailed =
    Token.register(
    function(data)
        local key = data.key
        local value = data.value
        if value and value.jailed and value.reason then
            jail('script', key, value.reason, true)
        else
            free('script', key)
        end
    end
)

local update_jailed =
    Token.register(
    function(data)
        local key = data.key
        local value = data.value or false
        local player = data.player or 'script'
        local message = data.message
        if value then
            jail(player, key, message)
        else
            free(player, key)
        end
    end
)

--- Tries to get data from the webpanel and updates the local table with values.
-- @param data_set player token
function Public.try_dl_data(key)
    key = tostring(key)

    local secs = Server.get_current_time()

    if not secs then
        return
    else
        try_get_data(jailed_data_set, key, is_jailed)
    end
end

--- Tries to get data from the webpanel and updates the local table with values.
-- @param data_set player token
function Public.try_ul_data(key, value, player, message)
    if type(key) == 'table' then
        key = key.name
    end

    key = tostring(key)

    local data = {
        key = key,
        value = value,
        player = player,
        message = message
    }

    Task.set_timeout_in_ticks(1, update_jailed, data)
end

--- Checks if a player exists within the table
-- @param player_name <string>
-- @return <boolean>
function Public.exists(player_name)
    return jailed[player_name] ~= nil
end

--- Prints a list of all players in the player_jailed table.
function Public.print_jailed()
    local result = {}

    for k, _ in pairs(jailed) do
        result[#result + 1] = k
    end

    result = concat(result, ', ')
    Game.player_print(result)
end

--- Returns the table of jailed
-- @return <table>
function Public.get_jailed_table()
    return jailed
end

Server.on_data_set_changed(
    jailed_data_set,
    function(data)
        if not data then
            return
        end

        local v = data.value
        if v and v.actor then
            if v.jailed then
                jail(v.actor, data.key, v.reason, true)
            elseif not v.jailed then
                free('script', data.key)
            end
        end
    end
)

commands.add_command(
    'jail',
    'Sends the player to gulag! Valid arguments are:\n/jail <LuaPlayer> <reason>',
    function()
        return
    end
)

commands.add_command(
    'free',
    'Brings back the player from gulag.',
    function()
        return
    end
)

function Public.required_playtime_for_instant_jail(value)
    if value then
        settings.playtime_for_instant_jail = value
    end
    return settings.playtime_for_instant_jail
end

function Public.set_valid_surface(value)
    settings.valid_surface = value or 'nauvis'
    return settings.valid_surface
end

function Public.required_playtime_for_vote(value)
    if value then
        settings.playtime_for_vote = value
    end
    return settings.playtime_for_vote
end

function Public.reset_vote_table()
    for k, _ in pairs(votejail) do
        votejail[k] = nil
    end
    for k, _ in pairs(votefree) do
        votefree[k] = nil
    end
end

Event.on_init(
    function()
        get_gulag_permission_group()
        create_gulag_surface()
    end
)

Event.add(
    defines.events.on_console_command,
    function(event)
        local cmd = event.command
        if not valid_commands[cmd] then
            return
        end

        local param = event.parameters

        if event.player_index then
            local player = game.players[event.player_index]
            local playtime = validate_playtime(player)
            local trusted = validate_trusted(player)

            if not param then
                return Utils.print_to(player, 'No valid reason given.')
            end

            local message
            local t = {}

            for i in string.gmatch(param, '%S+') do
                t[#t + 1] = i
            end

            local griefer = t[1]
            table.remove(t, 1)

            message = concat(t, ' ')

            local data = {
                player = player,
                griefer = griefer,
                trusted = trusted,
                playtime = playtime,
                message = message,
                cmd = cmd
            }

            local success = validate_args(data)

            if not success then
                return
            end

            local delay = 30

            if game.players[griefer] then
                griefer = game.players[griefer].name
            end

            if trusted and playtime >= settings.playtime_for_vote and playtime < settings.playtime_for_instant_jail and not player.admin then
                if cmd == 'jail' then
                    if not terms_tbl[player.name] then
                        Utils.warning(player, 'Abusing the jail command will lead to revoked permissions. Jailing someone in case of disagreement is _NEVER_ OK!')
                        Utils.warning(player, 'Run this command again to if you really want to do this!')
                        for i = 1, 4 do
                            Task.set_timeout_in_ticks(delay, play_alert_sound, {name = player.name})
                            delay = delay + 30
                        end
                        terms_tbl[player.name] = true
                        Task.set_timeout_in_ticks(settings.clear_terms_tbl, clear_terms_tbl, {player = player.name})
                        return
                    end
                    Utils.warning(player, 'Logging your actions.')
                    vote_to_jail(player, griefer, message)
                    return
                elseif cmd == 'free' then
                    vote_to_free(player, griefer)
                    return
                end
            end

            if player.admin or playtime >= settings.playtime_for_instant_jail then
                if cmd == 'jail' then
                    if not terms_tbl[player.name] then
                        Utils.warning(player, 'Abusing the jail command will lead to revoked permissions. Jailing someone in case of disagreement is _NEVER_ OK!')
                        Utils.warning(player, 'Run this command again to if you really want to do this!')
                        for i = 1, 4 do
                            Task.set_timeout_in_ticks(delay, play_alert_sound, {name = player.name})
                            delay = delay + 30
                        end
                        terms_tbl[player.name] = true
                        Task.set_timeout_in_ticks(settings.clear_terms_tbl, clear_terms_tbl, {player = player.name})
                        return
                    end
                    Utils.warning(player, 'Logging your actions.')
                    Public.try_ul_data(griefer, true, player.name, message)
                    return
                elseif cmd == 'free' then
                    Public.try_ul_data(griefer, false, player.name)
                    return
                end
            end
        end
    end
)

Event.add(
    defines.events.on_player_joined_game,
    function(event)
        local player = game.get_player(event.player_index)
        if not player or not player.valid then
            return
        end

        Public.try_dl_data(player.name)

        if not jailed[player.name] then
            return
        end

        local surface = game.surfaces['gulag']

        if player.surface.index ~= surface.index then
            local p_data = get_player_data(player)
            if jailed[player.name] and p_data and p_data.locked then
                teleport_player_to_gulag(player, 'jail')
            end
        end

        local gulag = get_gulag_permission_group()
        gulag.add_player(player)

        if player.character and player.character.valid and player.character.driving then
            player.character.driving = false
        end
    end
)

Event.add(
    defines.events.on_player_changed_surface,
    function(event)
        local player = game.players[event.player_index]
        if not player or not player.valid then
            return
        end

        if not jailed[player.name] then
            return
        end

        local surface = game.surfaces['gulag']
        if player.surface.index ~= surface.index then
            local p_data = get_player_data(player)
            if jailed[player.name] and p_data and p_data.locked then
                teleport_player_to_gulag(player, 'jail')
            end
        end
    end
)

return Public
