local Event = require 'utils.event'
local Token = require 'utils.token'
local Task = require 'utils.task'
local Global = require 'utils.global'
local Command = require 'utils.command'
local Debug = require 'utils.debug'
local Gui = require 'utils.gui'

local set_timeout_in_ticks = Task.set_timeout_in_ticks
local debug_print = Debug.print

local skip_btn_name = Gui.uid_name()
local backward_btn_name = Gui.uid_name()
local forward_btn_name = Gui.uid_name()

local Public = {}
local handler

local cutscene_functions = {}
local running_cutscenes = {}
local replay = {
    identifier = nil,
    final_transition_time = nil
}
Global.register(
    {
        cutscene_functions = cutscene_functions,
        running_cutscenes = running_cutscenes,
        replay = replay
    },
    function(tbl)
        cutscene_functions = tbl.cutscene_functions
        running_cutscenes = tbl.running_cutscenes
        replay = tbl.replay
    end
)

local function valid(entity)
    return entity and entity.valid
end

local function waypoint_still_active(tick, player_index)
    local running_cutscene = running_cutscenes[player_index]
    tick = tick or -1
    if tick == -1 then
        debug_print('Tick was nil', 5)
    end
    if not running_cutscene or tick < running_cutscene.start_tick then
        return false
    end
    return true
end

local toggle_gui_delayed =
    Token.register(
    function(params)
        local player = params.player
        if (not valid(player)) then
            return
        end
        if not waypoint_still_active(params.tick, player.index) then
            debug_print('Cutscene is no longer active. Skipping toggle_gui')
            return
        end
        local event = {player = player}
        local clear = params.clear
        if clear == 'left' then
            player.gui.left.clear()
        elseif clear == 'top' then
            player.gui.top.clear()
        elseif clear == 'center' then
            player.gui.center.clear()
        end
        params.gui.toggle(event)
    end
)

function Public.toggle_gui(tick, player, gui, initial_delay, clear)
    --[[if type(gui) == 'table' then
        debug_print('Provided GUI is invalid.')
        return
    end]]
    if (not valid(player)) then
        return
    end
    set_timeout_in_ticks(initial_delay, toggle_gui_delayed, {tick = tick, player = player, gui = gui, clear = clear})
end

local play_sound_delayed =
    Token.register(
    function(params)
        local player = params.player
        if (not valid(player)) then
            return
        end
        if not waypoint_still_active(params.tick, player.index) then
            debug_print('Cutscene is no longer active. Skipping play_sound')
            return
        end
        player.play_sound {path = params.path}
    end
)

function Public.play_sound(tick, player, path, times, delay, initial_delay)
    if (not valid(player)) then
        return
    end
    if not game.is_valid_sound_path(path) then
        debug_print('Provided SoundPath is invalid. Try opening /radio and browse for a valid path')
        return
    end

    if not waypoint_still_active(tick, player.index) then
        debug_print('Cutscene is no longer active. Skipping play_sound')
        return
    end

    times = times or 1
    if times == 1 and not delay and initial_delay then
        delay = initial_delay
    end
    if times > 1 or delay then
        delay = delay or 20
        initial_delay = initial_delay or 0
        for i = 1, times, 1 do
            set_timeout_in_ticks(initial_delay + delay * i, play_sound_delayed, {tick = tick, player = player, path = path})
        end
    else
        player.play_sound {path = path}
    end
end

local remove_renderings =
    Token.register(
    function(renderings)
        for _, v in pairs(renderings) do
            if rendering.is_valid(v) then
                rendering.destroy(v)
                debug_print('Deleted rendering with id: ' .. v)
            end
        end
    end
)

---Asserts if a given variable is of the expected type using type().
---
---@param expected_type string
---@param given any
---@param variable_reference_message string displayed when the expectation is not met
local function assert_type(expected_type, given, variable_reference_message, allow_nil)
    local given_type = type(given)
    if given_type ~= expected_type and (allow_nil and given_type ~= 'nil') then
        error('Argument ' .. variable_reference_message .. " must be of type '" .. expected_type .. "', given '" .. given_type .. "'")
    end
end

function Public.register_cutscene_function(identifier, waypoints, func, terminate_func)
    assert_type('string', identifier, 'identifier of function cutscene_controller.register_cutscene_function')
    assert_type('table', waypoints, 'waypoints of function cutscene_controller.register_cutscene_function')
    assert_type('number', func, 'func of function cutscene_controller.register_cutscene_function')
    assert_type('number', terminate_func, 'func of function cutscene_controller.register_cutscene_function', true)

    cutscene_functions[identifier] = {func = func, waypoints = waypoints, update = false, terminate_func = terminate_func}
end

function Public.register_running_cutscene(player_index, identifier, final_transition_time)
    assert_type('number', player_index, 'player_index of function cutscene_controller.register_running_cutscene')
    assert_type('string', identifier, 'identifier of function cutscene_controller.register_running_cutscene')
    assert_type('number', final_transition_time, 'identifier of function cutscene_controller.register_running_cutscene', true)

    local player = game.get_player(player_index)
    if not valid(player) then
        return
    end
    if player.controller_type ~= 1 then
        player.print('Cannot start cutscene without a character')
        return
    end
    local cutscene_function = cutscene_functions[identifier]
    if not cutscene_function then
        return
    end

    local waypoints = cutscene_function.waypoints
    if not waypoints then
        return
    end

    if running_cutscenes[player_index] then
        player.print({'cutscene_controller.cannot_start_new'})
        return
    end

    running_cutscenes[player_index] = {
        func = cutscene_function.func,
        waypoints = waypoints,
        update = cutscene_function.update,
        final_transition_time = final_transition_time,
        character = player.character,
        terminate_func = cutscene_function.terminate_func,
        rendering = {},
        current_index = -1,
        start_tick = 0
    }
    local running_cutscene = running_cutscenes[player_index]

    if player.controller_type == defines.controllers.cutscene then
        debug_print('' .. player.name .. ' was already in another cutscene not controlled by this module. It has been stopped')
        player.exit_cutscene()
    end
    player.set_controller {type = defines.controllers.ghost}

    final_transition_time = final_transition_time >= 0 and final_transition_time or 60
    running_cutscene.final_transition_time = final_transition_time
    running_cutscene.identifier = identifier
    player.set_controller {
        type = defines.controllers.cutscene,
        waypoints = waypoints,
        final_transition_time = final_transition_time
    }

    local flow = player.gui.top.add {type = 'flow'}
    running_cutscene.btn = flow

    local btn = flow.add {type = 'sprite-button', name = skip_btn_name, caption = 'Skip cutscene'}
    btn.style.minimal_height = 28
    btn.style.minimal_width = 150
    btn.style.font = 'default-large-bold'
    btn.style.font_color = {r = 255, g = 215, b = 0}

    local back_btn = flow.add {type = 'sprite-button', name = backward_btn_name, caption = 'Go back'}
    back_btn.style.minimal_height = 28
    back_btn.style.minimal_width = 100
    back_btn.style.font = 'default-large-bold'
    back_btn.style.font_color = {r = 255, g = 215, b = 0}

    local forward_btn = flow.add {type = 'sprite-button', name = forward_btn_name, caption = 'Go forward'}
    forward_btn.style.minimal_height = 28
    forward_btn.style.minimal_width = 100
    forward_btn.style.font = 'default-large-bold'
    forward_btn.style.font_color = {r = 255, g = 215, b = 0}

    handler({player_index = player_index, waypoint_index = -1, tick = game.tick})
end

local function restart_cutscene(player_index, waypoints, start_index)
    local current_running = running_cutscenes[player_index]
    local final_transition_time = current_running.final_transition_time
    current_running.update = false
    local character = current_running.character

    if not character then
        log('Player index: ' .. player_index .. ' managed to lose their character in a cutscene')
    end

    local end_waypoint = {
        -- end waypoint
        position = character.position,
        transition_time = final_transition_time,
        time_to_wait = 1,
        zoom = 1,
        terminate = true
    }

    table.insert(waypoints, end_waypoint)

    running_cutscenes[player_index] = {
        func = current_running.func,
        waypoints = waypoints,
        update = false,
        final_transition_time = final_transition_time,
        character = character,
        terminate_func = current_running.terminate_func,
        rendering = current_running.rendering,
        btn = current_running.btn,
        current_index = current_running.current_index,
        start_tick = current_running.start_tick
    }

    debug_print('Updating cutscene for player_index ' .. player_index)
    debug_print(running_cutscenes[player_index])

    local player = game.get_player(player_index)
    if not valid(player) then
        return
    end

    if player.controller_type == defines.controllers.cutscene then
        player.exit_cutscene()
        player.set_controller {type = defines.controllers.ghost}
    end

    player.set_controller {
        type = defines.controllers.cutscene,
        waypoints = waypoints,
        final_transition_time = final_transition_time
    }

    if start_index then
        player.jump_to_cutscene_waypoint(start_index + 1)
    else
        start_index = -1
    end

    handler({player_index = player_index, waypoint_index = start_index, tick = game.tick})
end

function Public.inject_waypoint(player_index, waypoint, waypoint_index, override)
    local running_cutscene = running_cutscenes[player_index]
    if not running_cutscene then
        return
    end
    local waypoints = running_cutscene.waypoints
    if not waypoints then
        return
    end
    local copy_waypoints = {}
    for i = 1, #waypoints do
        table.insert(copy_waypoints, waypoints[i])
    end
    if override then
        copy_waypoints[waypoint_index] = waypoint
    else
        table.insert(copy_waypoints, waypoint_index, waypoint)
    end
    running_cutscene.update = copy_waypoints
end

local callback_function =
    Token.register(
    function(params)
        local player_index = params.player_index
        local func_params = params.params
        if waypoint_still_active(func_params.tick, player_index) then
            Token.get(params.func)(player_index, params.waypoint_index, func_params)
        else
            debug_print('Skipping callback function. Cutscene got terminated!')
        end
    end
)

local reconnect_character =
    Token.register(
    function(params)
        local player_index = params.player_index
        local player = game.get_player(player_index)
        local running_cutscene = params.running_cutscene
        local character = running_cutscene.character
        local func = running_cutscene.terminate_func
        if valid(player) then
            player.exit_cutscene()
            if valid(character) then
                player.set_controller {type = defines.controllers.character, character = character}
            else
                params.skip_btn_flag = true
                player.ticks_to_respawn = 3600
                player.print('[color=red][font=compi]Oops - You Died! Please go to a safe location and restart the cutscene[/font][/color]')
            end
            if func then
                Token.get(func)(player_index, params.skip_btn_flag)
            end
            Token.get(remove_renderings)(running_cutscene.rendering)
            running_cutscene.btn.destroy()
            running_cutscenes[player_index] = nil
        end
    end
)

function Public.terminate_cutscene(player_index, ticks,skip_btn_flag)
    local running_cutscene = running_cutscenes[player_index]
    if not running_cutscene then
        return
    end
    ticks = ticks and ticks or 1
    debug_print('Terminating cutscene in ' .. ticks .. ' Ticks')
    if skip_btn_flag == nil then
        skip_btn_flag = false
    end
    debug_print('Is terminate_func ignored = ' .. tostring(skip_btn_flag))

    set_timeout_in_ticks(
        ticks,
        reconnect_character,
        {
            player_index = player_index,
            running_cutscene = running_cutscene,
            skip_btn_flag = skip_btn_flag
        }
    )
end

function Public.register_rendering_id(player_index, tick, render_id)
    if type(render_id) ~= 'table' then
        render_id = {render_id}
    end
    local running_cutscene = running_cutscenes[player_index]
    for _, id in pairs(render_id) do
        if rendering.is_valid(id) then
            if not waypoint_still_active(tick, player_index) then
                debug_print('The rendering with id ' .. id .. ' was not added. Destroying it instead')
                rendering.destroy(id)
            else
                table.insert(running_cutscene.rendering, id)
            end
        end
    end
end

function Public.register_replay(identifier, final_transition_time)
    replay.identifier = identifier
    replay.final_transition_time = final_transition_time
    debug_print('Identifier ' .. identifier .. ' registered as replay cutscene')
end

handler = function(event)
    local player_index = event.player_index
    local waypoint_index = event.waypoint_index
    local tick = event.tick

    debug_print('Waypoint_index ' .. waypoint_index .. ' has finished at tick: ' .. tick)

    local running_cutscene = running_cutscenes[player_index]
    if not running_cutscene then
        return
    end
    running_cutscene.current_index = waypoint_index + 1
    running_cutscene.start_tick = tick

    local update = running_cutscene.update
    if update then
        restart_cutscene(player_index, update, waypoint_index)
        return
    end
    local ticks = running_cutscene.waypoints[waypoint_index + 2]
    if ticks then
        ticks = ticks.transition_time
    else
        ticks = running_cutscene.final_transition_time
    end

    local func = running_cutscene.func
    if not func then
        return
    end
    local current_waypoint = running_cutscene.waypoints[waypoint_index + 2]
    if not current_waypoint or current_waypoint.terminate then
        Public.terminate_cutscene(player_index, ticks)
        return
    end
    local params = {
        position = current_waypoint.position,
        time_to_wait = current_waypoint.time_to_wait,
        transition_time = current_waypoint.transition_time,
        zoom = current_waypoint.zoom,
        name = current_waypoint.name,
        tick = tick
    }

    debug_print('Waypoint_index ' .. waypoint_index + 1 .. ' (waypoint #' .. waypoint_index + 2 .. ') callback in ' .. ticks .. ' ticks')

    set_timeout_in_ticks(ticks, callback_function, {func = running_cutscene.func, player_index = player_index, waypoint_index = waypoint_index, params = params})
end

function Public.goTo(player_index, waypoint_index)
    local running_cutscene = running_cutscenes[player_index]
    if waypoint_index < 0 or waypoint_index > #running_cutscene.waypoints - 2 then
        return false
    end
    Token.get(remove_renderings)(running_cutscene.rendering)
    game.get_player(player_index).jump_to_cutscene_waypoint(waypoint_index)
    handler({player_index = player_index, waypoint_index = waypoint_index - 1, tick = game.tick})
    running_cutscene.current_index = waypoint_index
    return true
end

local function restore(event)
    Public.terminate_cutscene(event.player_index, 1, true)
end

Event.add(defines.events.on_cutscene_waypoint_reached, handler)
Event.add(defines.events.on_pre_player_left_game, restore)
Event.add(defines.events.on_player_joined_game, restore)

local replay_cutscene =
    Token.register(
    function(params)
        Public.register_running_cutscene(params.event.player_index, replay.identifier, replay.final_transition_time)
    end
)

local function replay_handler(_, player)
    if not replay.identifier then
        player.print({'cutscene_controller.cannot_replay'})
        return
    end
    Token.get(replay_cutscene)({event = {player_index = player.index}})
end

Command.add(
    'replay',
    {
        description = {'cutscene_controller.replay'},
        capture_excess_arguments = false,
        allowed_by_server = false
    },
    replay_handler
)

local function skip_cutscene(_, player)
    if not player or not player.valid then
        return
    end
    if player.controller_type == defines.controllers.cutscene then
        Public.terminate_cutscene(player.index, 1, true)
    end
end

Command.add(
    'skip',
    {
        description = {'cutscene_controller.skip'},
        capture_excess_arguments = false,
        allowed_by_server = false
    },
    skip_cutscene
)

Gui.on_click(
    skip_btn_name,
    function(event)
        skip_cutscene(nil, game.get_player(event.player_index))
    end
)

Gui.on_click(
    backward_btn_name,
    function(event)
        local player_index = event.player_index
        if Public.goTo(player_index, running_cutscenes[player_index].current_index - 1) == false then
            game.get_player(player_index).print("Cutscene: You're already at the beginning")
        end
    end
)

Gui.on_click(
    forward_btn_name,
    function(event)
        local player_index = event.player_index
        if Public.goTo(event.player_index, running_cutscenes[player_index].current_index + 1) == false then
            game.get_player(player_index).print("Cutscene: You're already at the end")
        end
    end
)

return Public
