local Price_raffle = require 'maps.expanse.price_raffle'
local BiterRaffle = require 'utils.functions.biter_raffle'
local SpaceMissions = require 'maps.expanse.space_missions'
local Task = require 'utils.task'
local Token = require 'utils.token'
local Public = {}

local ores = { 'copper-ore', 'iron-ore', 'stone', 'coal' }
local price_modifiers = {
    ['unit-spawner'] = -256,
    ['unit'] = -16,
    ['turret'] = -128,
    ['tree'] = -8,
    ['simple-entity'] = 2,
    ['cliff'] = -128,
    ['water'] = -5,
    ['water-green'] = -5,
    ['deepwater'] = -5,
    ['deepwater-green'] = -5,
    ['water-mud'] = -6,
    ['water-shallow'] = -6
}

local qualities = {
    ['normal'] = 0,
    ['uncommon'] = 1,
    ['rare'] = 2,
    ['epic'] = 3,
    ['legendary'] = 5
}

--- Some mods like to destroy the infini tree.
--- So we solve it by delaying the creation.
local delay_infini_tree_token =
    Token.register(
        function (event)
            local surface = event.surface
            local position = event.position

            local newtree = surface.create_entity({ name = 'tree-0' .. math.random(1, 9), position = position })
            event.expanse.tree = script.register_on_object_destroyed(newtree)
        end
    )

local function reward_tokens(expanse, entity)
    local chance = expanse.token_chance % 1
    local count = math.floor(expanse.token_chance)

    if chance > 0 then
        chance = math.floor(chance * 1000)
        if math.random(1, 1000) <= chance then
            entity.surface.spill_item_stack({ position = entity.position, stack = { name = 'coin', count = 1 }, enable_looted = true, allow_belts = false })
        end
    end
    if count > 0 then
        for _ = 1, count, 1 do
            entity.surface.spill_item_stack({ position = entity.position, stack = { name = 'coin', count = 1 }, enable_looted = true, allow_belts = false })
        end
    end
end

local function get_cell_value(expanse, left_top)
    local square_size = expanse.square_size
    local value = square_size ^ 2
    value = value * 8

    local source_surface = game.surfaces[expanse.source_surface]
    local area = { { left_top.x, left_top.y }, { left_top.x + square_size, left_top.y + square_size } }
    local entities = source_surface.find_entities(area)
    local tiles = source_surface.find_tiles_filtered({ area = area })

    for _, tile in pairs(tiles) do
        if price_modifiers[tile.name] then
            value = value + price_modifiers[tile.name]
        end
    end
    for _, entity in pairs(entities) do
        if price_modifiers[entity.type] then
            value = value + price_modifiers[entity.type]
        end
    end

    local distance = math.sqrt(left_top.x ^ 2 + left_top.y ^ 2)
    value = value * ((distance ^ 1.15) * expanse.price_distance_modifier)
    local ore_modifier = distance * (expanse.price_distance_modifier / 20)
    if ore_modifier > expanse.max_ore_price_modifier then
        ore_modifier = expanse.max_ore_price_modifier
    end

    for _, entity in pairs(entities) do
        if entity.type == 'resource' then
            if entity.prototype.resource_category == 'basic-fluid' then
                value = value + (entity.amount * ore_modifier * 0.01)
            else
                value = value + (entity.amount * ore_modifier)
            end
        end
    end

    value = math.floor(value)
    if value < 16 then
        value = 16
    end

    return value
end

local function get_left_top(expanse, position)
    local vectors = { { -1, 0 }, { 1, 0 }, { 0, 1 }, { 0, -1 } }
    table.shuffle_table(vectors)

    local surface = game.surfaces[expanse.active_surface_index]

    for _, v in pairs(vectors) do
        local tile = surface.get_tile(position.x + v[1], position.y + v[2])
        if tile.name == 'out-of-map' then
            local left_top = tile.position
            left_top.x = left_top.x - left_top.x % expanse.square_size
            left_top.y = left_top.y - left_top.y % expanse.square_size
            if not expanse.grid[tostring(left_top.x .. '_' .. left_top.y)] then
                return left_top
            end
        end
    end

    return false
end

local function is_container_position_valid(expanse, position)
    if game.tick == 0 then
        return true
    end

    local left_top = get_left_top(expanse, position)
    if not left_top then
        return false
    end

    if
        game.surfaces[expanse.active_surface_index].count_entities_filtered(
            {
                name = 'requester-chest',
                force = 'neutral',
                area = { { left_top.x - 1, left_top.y - 1 }, { left_top.x + expanse.square_size + 1, left_top.y + expanse.square_size + 1 } }
            }
        ) > 0
    then
        return false
    end

    return true
end

local function create_costs_render(entity, name, offset, quality)
    local id = rendering.draw_sprite {
        sprite = 'virtual-signal/signal-grey',
        surface = entity.surface,
        target = {
            entity = entity,
            offset = { offset, -1.5 }
        },
        x_scale = 1.1,
        y_scale = 1.1,
        render_layer = '190',
        only_in_alt_mode = true
    }
    local id2 = rendering.draw_sprite {
        sprite = 'item/' .. name,
        surface = entity.surface,
        target = {
            entity = entity,
            offset = { offset, -1.5 }
        },
        x_scale = 0.75,
        y_scale = 0.75,
        render_layer = '191',
        only_in_alt_mode = true
    }
    local Q = script.active_mods['quality']
    local id3 = rendering.draw_sprite {
        sprite = 'quality/' .. quality,
        surface = entity.surface,
        target = {
            entity = entity,
            offset = { offset - 0.25, -1.25 }
        },
        x_scale = 0.25,
        y_scale = 0.25,
        render_layer = Q and '192' or '189',
        only_in_alt_mode = true
    }

    return { id, id2, id3}
end

local function remove_one_render(container, key)
    if container.price[key].render[1].valid then
        container.price[key].render[1].destroy()
    end
    if container.price[key].render[2].valid then
        container.price[key].render[2].destroy()
    end
    if container.price[key].render[3].valid then
        container.price[key].render[3].destroy()
    end
end

local function remove_old_renders(container)
    for key, _ in pairs(container.price) do
        remove_one_render(container, key)
    end
end

function Public.spawn_units(spawner)
    local evolution = game.forces.enemy.get_evolution_factor(spawner.surface)
    local position = spawner.position
    for i = 1, 4 + math.floor(8 * evolution), 1 do
        local biter_roll = BiterRaffle.roll('mixed', evolution)
        local free_pos = spawner.surface.find_non_colliding_position(biter_roll, { x = position.x + math.random(-8, 8), y = position.y + math.random(-8, 8) }, 12, 0.05)
        spawner.surface.create_entity({ name = biter_roll, position = free_pos or position, force = 'enemy' })
    end
end

function Public.get_item_tooltip(name, quality, include_value)
    local value = include_value and Price_raffle.get_item_worth(name, qualities[quality]) or ''
    local desc = include_value and 'expanse.stats_item_tooltip' or 'expanse.stats_item_tooltip_nv'
    if quality == 'normal' then
        return { desc, prototypes.item[name].localised_name, '', value }
    end
    return { desc, prototypes.item[name].localised_name, {'expanse.stats_quality', prototypes.quality[quality].localised_name or ''}, value }
end

function Public.invasion_numbers(expanse)
    local evo = game.forces.enemy.get_evolution_factor(game.surfaces[expanse.active_surface_index])
    return { candidates = 3 + math.floor(evo * 10), groups = 1 + math.floor(evo * 4) }
end

function Public.invasion_warn(event)
    local seconds = (120 * 60 - (event.delay or 0)) / 60
    game.print({ 'expanse.biters_invasion_warning', seconds, event.size }, { r = 0.88, g = 0.22, b = 0.22 })
end

function Public.invasion_detonate(event)
    local surface = event.surface
    local position = event.position
    local entities_close = surface.find_entities_filtered { position = position, radius = 8 }
    for _, entity in pairs(entities_close) do
        if entity.valid then
            entity.die('enemy')
        end
    end
    local entities_nearby = surface.find_entities_filtered { position = position, radius = 16 }
    for _, entity in pairs(entities_nearby) do
        if entity.valid and entity.is_entity_with_health then
            entity.damage(entity.max_health * 0.75, 'enemy')
        end
    end
    surface.create_entity({ name = 'nuke-explosion', position = position })
end

function Public.invasion_trigger(event)
    local surface = event.surface
    local position = event.position
    local round = event.round
    local evolution = game.forces.enemy.get_evolution_factor(surface)
    local biters = {}
    for i = 1, 5 + math.floor(30 * evolution) + round * 5, 1 do
        local biter_roll = BiterRaffle.roll('mixed', evolution)
        local free_pos = surface.find_non_colliding_position(biter_roll, { x = position.x + math.random(-8, 8), y = position.y + math.random(-8, 8) }, 12, 0.05)
        biters[#biters + 1] = surface.create_entity({ name = biter_roll, position = free_pos or position, force = 'enemy' })
    end
    local group = surface.create_unit_group { position = position, force = 'enemy' }
    for _, biter in pairs(biters) do
        group.add_member(biter)
    end
    group.set_command({ type = defines.command.attack_area, destination = position, radius = 80, distraction = defines.distraction.by_anything })
    group.start_moving()
    local worm_roll = BiterRaffle.roll('worm', evolution)
    for i = 1, 3 + math.floor(7 * evolution), 1 do
        local worm_pos = surface.find_non_colliding_position(worm_roll, { x = position.x + math.random(-12, 12), y = position.y + math.random(-12, 12) }, 12, 0.1)
        if worm_pos then
            surface.create_entity({ name = worm_roll, position = worm_pos, force = 'enemy' })
        end
    end
    local nest = { 'biter-spawner', 'biter-spawner', 'biter-spawner', 'spitter-spawner' }
    local nest_roll = nest[math.random(1, 4)]
    local nest_pos = surface.find_non_colliding_position(nest_roll, position, 12, 0.1)
    if nest_pos then
        surface.create_entity({ name = nest_roll, position = nest_pos, force = 'enemy' })
    end
end

local function schedule_detonation(expanse, surface, position)
    table.insert(expanse.schedule, { tick = game.tick + 120 * 60, event = 'invasion_detonate', parameters = { surface = surface, position = position } })
end

local function schedule_warning(expanse, size, delay)
    table.insert(expanse.schedule, { tick = game.tick + 2 * 60 + delay, event = 'invasion_warn', parameters = { size = size, delay = delay } })
end

local function schedule_biters(expanse, surface, position, delay, round)
    table.insert(expanse.schedule, { tick = game.tick + delay + 120 * 60, event = 'invasion_trigger', parameters = { surface = surface, position = position, round = round } })
end

local function plan_invasion(expanse, invasion_numbers)
    local candidates = expanse.invasion_candidates
    table.shuffle_table(candidates)
    schedule_warning(expanse, invasion_numbers.groups, 0)
    schedule_warning(expanse, invasion_numbers.groups, 60 * 60)
    schedule_warning(expanse, invasion_numbers.groups, 90 * 60)
    local rounds = 4 + math.random(1, 8)
    for i = 1, invasion_numbers.groups, 1 do
        local surface_index = candidates[i].surface_index
        if not surface_index then break end
        local surface = game.get_surface(surface_index)
        local position = candidates[i].position
        schedule_detonation(expanse, surface, position)
        for ii = 1, rounds, 1 do
            schedule_biters(expanse, surface, position, 120 + (ii - 1) * 300, ii)
        end
        candidates[i].render.time_to_live = (122 * 60 + rounds * 300)
    end
    for j = invasion_numbers.groups + 1, #candidates, 1 do
        candidates[j].render.time_to_live = (122 * 60)
    end
    expanse.invasion_candidates = {}
end

function Public.check_invasion(expanse)
    local invasion_numbers = Public.invasion_numbers(expanse)
    if #expanse.invasion_candidates >= invasion_numbers.candidates then
        plan_invasion(expanse, invasion_numbers)
    end
end

local function calculate_tier(expanse, left_top)
    local distances = {
        30,
        60,
        90,
        150,
        210,
        300
    }
    local tier = 5
    local distance = math.sqrt(left_top.x ^ 2 + left_top.y ^ 2)
    for i = 1, #distances - 1, 1 do
        if distance < distances[i] then
            tier = i
            break
        end
    end
    if script.active_mods['space-age'] then
        local biomes = {
            6, --vulcanus
            7, --fulgora
            8, --gleba
            9, --aquilo
        }
        if distance > distances[5] and distance < distances[6] then
            if left_top.x >= 0 and left_top.y >= 0 then
                tier = biomes[(expanse.biome_offset + 1) % 4 + 1]
            elseif left_top.x >= 0 and left_top.y < 0 then
                tier = biomes[(expanse.biome_offset + 2) % 4 + 1]
            elseif left_top.x < 0 and left_top.y >= 0 then
                tier = biomes[(expanse.biome_offset + 3) % 4 + 1]
            elseif left_top.x < 0 and left_top.y < 0 then
                tier = biomes[(expanse.biome_offset + 4) % 4 + 1]
            end
        elseif distance >= distances[6] then
            tier = 10
        end
    end
    return tier, distance
end

function Public.expand(expanse, left_top)
    expanse.grid[tostring(left_top.x .. '_' .. left_top.y)] = true
    local tier = calculate_tier(expanse, left_top)
    local square_size = expanse.square_size
    -- if tier == 7 then
    --     expanse.lightning_tiles[#expanse.lightning_tiles + 1] = {x = left_top.x + square_size / 2, y = left_top.y + square_size / 2}
    -- end

    local source_surface = game.surfaces[expanse.source_surface]
    if not source_surface then
        return
    end
    source_surface.request_to_generate_chunks(left_top, 2)
    source_surface.force_generate_chunk_requests()

    local area = { { left_top.x, left_top.y }, { left_top.x + square_size, left_top.y + square_size } }
    local surface = game.surfaces[expanse.active_surface_index]

    source_surface.clone_area(
        {
            source_area = area,
            destination_area = area,
            destination_surface = surface,
            clone_tiles = true,
            clone_entities = game.tick ~= expanse.reset_tick,
            clone_decoratives = game.tick ~= expanse.reset_tick and (tier < 6 or tier > 9),
            clear_destination_entities = false,
            clear_destination_decoratives = true,
            expand_map = true
        }
    )

    local positions = {
        { x = left_top.x + math.random(1, square_size - 2), y = left_top.y },
        { x = left_top.x,                                   y = left_top.y + math.random(1, square_size - 2) },
        { x = left_top.x + math.random(1, square_size - 2), y = left_top.y + (square_size - 1) },
        { x = left_top.x + (square_size - 1),               y = left_top.y + math.random(1, square_size - 2) }
    }

    for _, position in pairs(positions) do
        if is_container_position_valid(expanse, position) then
            local e = surface.create_entity({ name = 'requester-chest', position = position, force = 'neutral' })
            e.destructible = false
            e.minable = false
        end
    end
    local custom_tier = tier
    if tier == 10 then
        if expanse.tiered_specials[tier].specials == 0 or math.random(1, 20) ~= 1 then
            custom_tier = math.random(6, 9)
        end
    end

    SpaceMissions.convert_tiles(surface, left_top, custom_tier, square_size)
    SpaceMissions.convert_entities(surface, left_top, custom_tier, square_size)
    SpaceMissions.convert_decoratives(surface, left_top, custom_tier, square_size)
    SpaceMissions.place_special_tiered_object(expanse, tier, surface, left_top, custom_tier)

    if game.tick == expanse.reset_tick then
        local a = math.floor(expanse.square_size * 0.5)
        for x = 1, 3, 1 do
            for y = 1, 3, 1 do
                surface.set_tiles({ { name = 'water', position = { a + x + 2, a + y + 2 } } }, true)
            end
        end
        surface.create_entity({ name = 'crude-oil', position = { a - 4, a - 4 }, amount = 1500000 })
        Task.set_timeout_in_ticks(30, delay_infini_tree_token, { surface = surface, position = { a - 4, a + 4 }, expanse = expanse })
        surface.create_entity({ name = 'big-rock', position = { a + 4, a - 4 } })
        surface.spill_item_stack({ position = { a, a + 2 }, stack = { name = 'coin', count = 1 }, enable_looted = false, allow_belts = false })
        surface.spill_item_stack({ position = { a + 0.5, a + 2.5 }, stack = { name = 'coin', count = 1 }, enable_looted = false, allow_belts = false })
        surface.spill_item_stack({ position = { a - 0.5, a + 2.5 }, stack = { name = 'coin', count = 1 }, enable_looted = false, allow_belts = false })

        for x = 0, square_size, 1 do
            for y = 0, square_size, 1 do
                if surface.can_place_entity({ name = 'wooden-chest', position = { x, y } }) and surface.can_place_entity({ name = 'coal', position = { x, y }, amount = 1 }) then
                    surface.create_entity({ name = ores[(x + y) % 4 + 1], position = { x, y }, amount = 1500 })
                end
            end
        end
    end
end

local function get_tier_locker(tier)
    local SA =  script.active_mods['space-age']
    local lockers = {
        [5] = 'space-science-pack',
        [6] = SA and 'metallurgic-science-pack' or nil,
        [7] = SA and 'electromagnetic-science-pack' or nil,
        [8] = SA and 'agricultural-science-pack' or nil,
        [9] = SA and 'cryogenic-science-pack' or nil,
        [10] = SA and 'promethium-science-pack' or nil
    }
    return lockers[tier]
end

local function init_container(expanse, entity, budget)
    local left_top = get_left_top(expanse, entity.position)
    if not left_top then
        return
    end
    local tier_multi = {
        [1] = 1,
        [2] = 1,
        [3] = 1.5,
        [4] = 1.5,
        [5] = 2,
        [6] = 1,
        [7] = 1,
        [8] = 1,
        [9] = 1,
        [10] = 3
    }
    local tier, distance = calculate_tier(expanse, left_top)
    local cell_value = budget or get_cell_value(expanse, left_top) * tier_multi[tier]
    local item_stacks = {}
    local locker = get_tier_locker(tier)
    if locker then
        item_stacks[locker] = {['normal'] = 10}
    end
    local roll_count = 3
    for _ = 1, roll_count, 1 do
        for _, stack in pairs(Price_raffle.roll(math.floor(cell_value / roll_count), 3, tier, nil)) do
            if not item_stacks[stack.name] then
                item_stacks[stack.name] = {[stack.quality] = stack.count}
            elseif not item_stacks[stack.name][stack.quality] then
                item_stacks[stack.name][stack.quality] = stack.count
            else
                item_stacks[stack.name][stack.quality] = item_stacks[stack.name][stack.quality] + stack.count
            end
        end
    end

    local price = {}
    local offset = -3
    for name, stack in pairs(item_stacks) do
        for quality, count in pairs(stack) do
            table.insert(price, { name = name, count = count, quality = quality, render = create_costs_render(entity, name, offset, quality) })
            offset = offset + 1
        end
    end
    if _DEBUG then
        game.print('distance: ' .. distance .. ', tier: ' .. tier .. ', value: ' .. cell_value)
    end
    local containers = expanse.containers
    containers[entity.unit_number] = { entity = entity, left_top = left_top, price = price }
end

local function get_remaining_budget(container)
    local budget = 0
    for _, item_stack in pairs(container.price) do
        budget = budget + (item_stack.count * Price_raffle.get_item_worth(item_stack.name, qualities[item_stack.quality]))
    end
    return budget
end

function Public.make_key(name, quality)
    return name .. '|' .. quality
end

function Public.split_key(key)
    return string.match(key, '^(.-)|(.+)$')
end

function Public.set_container(expanse, entity)
    if entity.name ~= 'requester-chest' then
        return
    end
    if not expanse.containers[entity.unit_number] then
        init_container(expanse, entity)
    end

    local container = expanse.containers[entity.unit_number]
    if not container or not container.entity or not container.entity.valid then
        expanse.containers[entity.unit_number] = nil
        return
    end

    local inventory = container.entity.get_inventory(defines.inventory.chest)
    local trash_inventory = container.entity.get_inventory(defines.inventory.logistic_container_trash)

    if not inventory.is_empty() then
        if inventory.get_item_count('coin') > 0 then
            local count_removed = inventory.remove({ name = 'coin', count = 1 })
            if count_removed > 0 then
                expanse.cost_stats[Public.make_key('coin', 'normal')] = (expanse.cost_stats[Public.make_key('coin', 'normal')] or 0) + count_removed
                script.raise_event(expanse.events.gui_update, { item = 'coin', quality = 'normal' })
                remove_old_renders(container)
                init_container(expanse, entity, get_remaining_budget(container))
                container = expanse.containers[entity.unit_number]
                game.print({ 'expanse.chest_reset', { 'expanse.gps', math.floor(entity.position.x), math.floor(entity.position.y), game.surfaces[expanse.active_surface_index].name } })
            end
        end
        if _DEBUG and inventory.get_item_count('infinity-chest') > 0 then
            remove_old_renders(container)
            container.price = {}
        end
    end

    for key, item_stack in pairs(container.price) do
        local name = item_stack.name
        local quality = item_stack.quality
        local count_removed = inventory.remove({ name = name, count = item_stack.count, quality = item_stack.quality})
        container.price[key].count = container.price[key].count - count_removed
        expanse.cost_stats[Public.make_key(name, quality)] = (expanse.cost_stats[Public.make_key(name, quality)] or 0) + count_removed
        script.raise_event(expanse.events.gui_update, { item = name, quality = quality})
        if container.price[key].count <= 0 then
            remove_one_render(container, key)
            table.remove(container.price, key)
        end
    end

    if #container.price == 0 then
        Public.expand(expanse, container.left_top)
        local a = math.floor(expanse.square_size * 0.5)
        local expansion_position = { x = expanse.containers[entity.unit_number].left_top.x + a, y = expanse.containers[entity.unit_number].left_top.y + a }
        expanse.containers[entity.unit_number] = nil
        for _, inv in pairs({inventory, trash_inventory}) do
            if not inv.is_empty() then
                for index = 1, #inv, 1 do
                    local slot = inv[index]
                    if slot.valid_for_read then
                        entity.surface.spill_item_stack({ position = entity.position, stack = slot, enable_looted = true, allow_belts = false })
                    end
                end
            end
        end
        reward_tokens(expanse, entity)
        entity.destructible = true
        entity.die()
        return expansion_position
    end
    local logi = container.entity.get_logistic_point(0)
    for i = 1, 256, 1 do
        if logi.get_section(i) then
            logi.remove_section(i)
        end
    end

    logi.add_section()
    local section = logi.get_section(1)

    for slot = 1, #container.price, 1 do
        if #container.price >= slot then
            local item = container.price[slot]
            section.set_slot(slot, { value = {name = item.name, quality = item.quality, comparator = '='}, min = item.count, import_from = 'nauvis' })
        end
    end
end

function Public.chest_value(expanse, player)
    if not player or not player.valid then return end
    local position = player.position
    local chests = player.surface.find_entities_filtered({ name = 'requester-chest', force = 'neutral', position = position, radius = 10})
    for _, chest in pairs(chests) do
        local container = expanse.containers[chest.unit_number]
        local value = container and get_remaining_budget(container) or 0
        player.print({'expanse.chest_value', value, {'expanse.gps', chest.position.x, chest.position.y, chest.surface.name}})
    end
end

return Public
