local Event = require 'utils.event'
local Global = require 'utils.global'
local Task = require 'utils.task_token'
local Public = {}

local math_floor = math.floor
local table_shuffle_table = table.shuffle_table

local this =
{
    debug = false,
    disabled = false,
    reverse_disabled = false
}
Global.register(
    this,
    function (tbl)
        this = tbl
    end
)

local direction_reverse =
{
    ['north'] = 'south',
    ['south'] = 'north',
    ['west'] = 'east',
    ['east'] = 'west'
}

local directions =
{
    ['north'] = function (position, reverse)
        local surface_index = this.surface_index
        if not surface_index then
            return
        end

        local surface = game.get_surface(surface_index)
        if not surface or not surface.valid then
            return
        end
        local width = surface.map_gen_settings.width
        if width > this.max_line_size then
            width = this.max_line_size
        end
        if this.max_line_size_force then
            width = this.max_line_size
        end
        local a = width * 0.5 + 4

        if not reverse then
            this.vector = { 0, -1 }
            this.area = { { position.x - a, position.y - 1 }, { position.x + a, position.y } }
        else
            this.reverse_vector = { 0, -1 }
            this.reverse_area = { { position.x - a, position.y - 1 }, { position.x + a, position.y } }
        end
    end,
    ['south'] = function (position, reverse)
        local surface_index = this.surface_index
        if not surface_index then
            return
        end

        local surface = game.get_surface(surface_index)
        if not surface or not surface.valid then
            return
        end
        local width = surface.map_gen_settings.width
        if width > this.max_line_size then
            width = this.max_line_size
        end
        if this.max_line_size_force then
            width = this.max_line_size
        end
        local a = width * 0.5
        if not reverse then
            this.vector = { 0, 1 }
            this.area = { { position.x - a, position.y }, { position.x + a, position.y + 1 } }
        else
            this.reverse_vector = { 0, 1 }
            this.reverse_area = { { position.x - a, position.y }, { position.x + a, position.y + 1 } }
        end
    end,
    ['west'] = function (position, reverse)
        local surface_index = this.surface_index
        if not surface_index then
            return
        end

        local surface = game.get_surface(surface_index)
        if not surface or not surface.valid then
            return
        end
        local width = surface.map_gen_settings.height
        if width > this.max_line_size then
            width = this.max_line_size
        end
        if this.max_line_size_force then
            width = this.max_line_size
        end
        local a = width * 0.5 + 1
        if not reverse then
            this.vector = { -1, 0 }
            this.area = { { position.x - 1, position.y - a }, { position.x, position.y + a } }
        else
            this.reverse_vector = { -1, 0 }
            this.reverse_area = { { position.x - 1, position.y - a }, { position.x, position.y + a } }
        end
    end,
    ['east'] = function (position, reverse)
        local surface_index = this.surface_index
        if not surface_index then
            return
        end

        local surface = game.get_surface(surface_index)
        if not surface or not surface.valid then
            return
        end
        local width = surface.map_gen_settings.height
        if width > this.max_line_size then
            width = this.max_line_size
        end
        if this.max_line_size_force then
            width = this.max_line_size
        end
        local a = width * 0.5 + 1
        if not reverse then
            this.vector = { 1, 0 }
            this.area = { { position.x, position.y - a }, { position.x + 1, position.y + a } }
        else
            this.reverse_vector = { 1, 0 }
            this.reverse_area = { { position.x, position.y - a }, { position.x + 1, position.y + a } }
        end
    end
}

local function print_debug(a)
    if not this.debug then
        return
    end
    print('Collapse error #' .. a)
end

local function set_collapse_tiles(surface)
    if not surface or surface.valid then
        print_debug(45)
    end
    game.forces.player.chart(surface, this.area)
    this.tiles = surface.find_tiles_filtered({ area = this.area, name = 'out-of-map', invert = true })

    if not this.tiles then
        return
    end

    this.size_of_tiles = #this.tiles

    if this.size_of_tiles > 0 then
        table_shuffle_table(this.tiles)
    end
    this.position = { x = this.position.x + this.vector[1], y = this.position.y + this.vector[2] }
    local v = this.vector
    if not v then
        return
    end
    local area = this.area
    this.area = { { area[1][1] + v[1], area[1][2] + v[2] }, { area[2][1] + v[1], area[2][2] + v[2] } }
    local chart_area = { { area[1][1] + v[1] - 4, area[1][2] + v[2] - 4 }, { area[2][1] + v[1] + 4, area[2][2] + v[2] + 4 } }
    game.forces.player.chart(surface, chart_area)
end

---@param surface LuaSurface
local function set_reverse_collapse_tiles(surface)
    if not surface or surface.valid then
        print_debug(45)
    end
    this.reverse_tiles = surface.find_tiles_filtered({ area = this.reverse_area, name = 'out-of-map', invert = true })

    if not this.reverse_tiles then
        return
    end

    this.reverse_size_of_tiles = #this.reverse_tiles
    if this.reverse_size_of_tiles > 0 then
        table_shuffle_table(this.reverse_tiles)
    end
    this.reverse_position = { x = this.reverse_position.x + this.reverse_vector[1], y = this.reverse_position.y + this.reverse_vector[2] }
    local v = this.reverse_vector
    if not v then return end
    local area = this.reverse_area
    this.reverse_area = { { area[1][1] + v[1], area[1][2] + v[2] }, { area[2][1] + v[1], area[2][2] + v[2] } }
    local chart_area = { { area[1][1] + v[1] - 4, area[1][2] + v[2] - 10 }, { area[2][1] + v[1] + 4, area[2][2] + v[2] + 10 } }
    local force = game.forces.player

    force.chart(surface, chart_area)
end

local function custom_callback(entity)
    if this.specific_entities and this.specific_entities.callback then
        local callback_token = this.specific_entities.callback
        local callback = Task.get(callback_token)
        if not entity or not entity.valid then
            return -- entity is invalid, don't continue
        end

        local event =
        {
            entity = entity
        }

        callback(event)
    end
end

local function progress()
    if not this.start_now then
        this.tiles = nil
        return
    end

    local surface_index = this.surface_index
    if not surface_index then
        return
    end

    local surface = game.get_surface(surface_index)
    if not surface or not surface.valid then
        return
    end

    local tiles = this.tiles
    if not tiles then
        set_collapse_tiles(surface)
        tiles = this.tiles
    end
    if not tiles then
        return
    end

    for _ = 1, this.amount, 1 do
        local tile = tiles[this.size_of_tiles]
        if not tile then
            this.tiles = nil
            return
        end
        this.size_of_tiles = this.size_of_tiles - 1
        if not tile.valid then
            return
        end
        if this.specific_entities.enabled then
            local position = { tile.position.x + 0.5, tile.position.y + 0.5 }
            local entities = this.specific_entities.entities
            for _, e in pairs(surface.find_entities_filtered({ area = { { position[1] - 4, position[2] - 2 }, { position[1] + 4, position[2] + 2 } } })) do
                custom_callback(e)
                if entities[e.name] and e.valid and e.health then
                    e.die()
                elseif e.valid then
                    e.destroy()
                end
            end
        end
        surface.set_tiles({ { name = 'out-of-map', position = tile.position } }, true)
    end
end

local function progress_reverse()
    if not this.reverse_start_now then
        this.reverse_tiles = nil
        return
    end
    local surface_index = this.surface_index
    if not surface_index then
        return
    end

    local surface = game.get_surface(surface_index)
    if not surface or not surface.valid then
        return
    end

    local tiles = this.reverse_tiles
    if not tiles then
        set_reverse_collapse_tiles(surface)
        tiles = this.reverse_tiles
    end
    if not tiles then
        return
    end

    for _ = 1, this.amount + 20, 1 do
        local tile = tiles[this.reverse_size_of_tiles]
        if not tile then
            this.reverse_tiles = nil
            return
        end
        this.reverse_size_of_tiles = this.reverse_size_of_tiles - 1
        if not tile.valid then
            return
        end
        if this.specific_entities.enabled then
            local position = { tile.position.x + 0.5, tile.position.y + 0.5 }
            local entities = this.specific_entities.entities
            for _, e in pairs(surface.find_entities_filtered({ area = { { position[1] - 4, position[2] - 2 }, { position[1] + 4, position[2] + 2 } } })) do
                custom_callback(e)
                if entities[e.name] and e.valid and e.health then
                    e.die()
                elseif e.valid then
                    e.destroy()
                end
            end
        end
        surface.set_tiles({ { name = 'out-of-map', position = tile.position } }, true)
    end
end

function Public.set_surface_index(surface_index)
    if not surface_index then
        print_debug(1)
        return
    end

    local surface = game.get_surface(surface_index)
    if not surface or not surface.valid then
        print_debug(2)
        return
    end

    this.surface_index = surface_index
end

function Public.set_direction(direction)
    if not directions[direction] then
        print_debug(11)
        return
    end
    directions[direction](this.position)
    this.direction = direction
end

function Public.set_reverse_position(position)
    if not position then
        print_debug(4)
        return
    end
    if not position.x and not position[1] then
        print_debug(5)
        return
    end
    if not position.y and not position[2] then
        print_debug(6)
        return
    end
    local x = 0
    local y = 0
    if position[1] then
        x = position[1]
    end
    if position[2] then
        y = position[2]
    end
    if position.x then
        x = position.x
    end
    if position.y then
        y = position.y
    end
    this.reverse_position = { x = x, y = y }
end

function Public.set_reverse_direction()
    if not directions[direction_reverse[this.direction]] then
        print_debug(11)
        return
    end
    if not this.reverse_position then
        print_debug(13)
        return
    end

    directions[direction_reverse[this.direction]](this.reverse_position, true)
    this.direction = direction_reverse[this.direction]
end

function Public.set_force_mode(force_mode)
    this.force_mode = force_mode
end

function Public.set_speed(speed)
    if this.force_mode then
        return
    end

    if not speed then
        print_debug(8)
        return
    end
    speed = math_floor(speed)
    if speed < 1 then
        speed = 1
    end
    this.speed = speed
end

function Public.set_amount(amount)
    if this.force_mode then
        return
    end

    if not amount then
        print_debug(9)
        return
    end
    amount = math_floor(amount)
    if amount < 0 then
        amount = 0
    end
    this.amount = amount
end

function Public.set_position(position)
    if not position then
        print_debug(4)
        return
    end
    if not position.x and not position[1] then
        print_debug(5)
        return
    end
    if not position.y and not position[2] then
        print_debug(6)
        return
    end
    local x = 0
    local y = 0
    if position[1] then
        x = position[1]
    end
    if position[2] then
        y = position[2]
    end
    if position.x then
        x = position.x
    end
    if position.y then
        y = position.y
    end
    this.position = { x = x, y = y }
end

function Public.get_position()
    return this.position
end

function Public.get_reverse_position()
    return this.reverse_position
end

function Public.get_amount()
    return this.amount
end

function Public.get_speed()
    return this.speed
end

--- Set the collapse to start or not, accepts a second argument to disable the collapse completely.
---@param state boolean
---@param disabled? boolean
---@return boolean
function Public.start_now(state, disabled)
    this.start_now = state or false

    this.disabled = disabled or false

    return this.start_now
end

--- Set the reverse collapse to start or not, accepts a second argument to disable the collapse completely.
---@param state boolean
---@param disabled? boolean
---@return boolean
function Public.reverse_start_now(state, disabled)
    this.reverse_start_now = state or false

    this.reverse_disabled = disabled or false

    return this.reverse_start_now
end

function Public.has_collapse_started()
    if this.disabled then
        return true
    end

    return this.start_now
end

function Public.has_reverse_collapse_started()
    return this.reverse_start_now
end

function Public.set_max_line_size(size, force)
    if not size then
        print_debug(22)
        return
    end
    size = math_floor(size)
    if size <= 0 then
        print_debug(21)
        return
    end
    this.max_line_size = size
    this.max_line_size_force = force or false
end

function Public.set_kill_specific_entities(tbl)
    if tbl then
        this.specific_entities = tbl
    else
        this.specific_entities =
        {
            enabled = false
        }
    end
end

local function on_init()
    Public.set_surface_index(game.surfaces.nauvis.index)
    Public.set_position({ 0, 32 })
    Public.set_max_line_size(256)
    Public.set_direction('north')
    Public.set_kill_specific_entities()
    this.tiles = nil
    this.reverse_tiles = nil
    this.speed = 1
    this.force_mode = false
    this.disabled = false
    this.reverse_disabled = false
    this.reverse_start_now = false
    this.amount = 8
    this.start_now = false
end

local function on_tick()
    local tick = game.tick
    if tick % this.speed ~= 0 then
        return
    end

    if not this.reverse_disabled then
        progress_reverse()
    end

    if not this.disabled then
        progress()
    end
end

if not Public.read_tables_only then
    Event.on_init(on_init)
    Event.add(defines.events.on_tick, on_tick)
end

return Public
