local Public = {}
local Event = require 'utils.event'
local Global = require 'utils.global'
local Collapse = require 'modules.collapse'
local this = {
    explosives = {},
    settings = {
        disabled = false,
        slow_explode = false,
        slow_explode_tick = 300,
        check_growth_below_void = false,
        valid_items = {
            ['explosives'] = 500,
            ['cliff-explosives'] = 750
        }
    }
}
Global.register(
    this,
    function (tbl)
        this = tbl
    end
)

local math_abs = math.abs
local math_floor = math.floor
local math_sqrt = math.sqrt
local math_round = math.round
local math_random = math.random
local shuffle_table = table.shuffle_table
local speed = 6
local density = 1
local density_r = density * 0.5
local valid_container_types = {
    ['container'] = true,
    ['logistic-container'] = true,
    ['car'] = true,
    ['cargo-wagon'] = true
}

local disabled_container_names = {
    ['buffer-chest'] = true
}

local function pos_to_key(position)
    return tostring(position.x .. '_' .. position.y)
end

local function check_y_pos(position)
    if not this.settings.check_growth_below_void then
        return false
    end
    if not position or not position.y then
        return false
    end
    local collapse_pos = Collapse.get_position()

    local radius = 10

    local dy = position.y - collapse_pos.y
    if dy ^ 2 < radius ^ 2 then
        return true
    end

    if position.y >= collapse_pos.y then
        return true
    else
        return false
    end
end

local function get_explosion_name(health)
    if health < 2500 then
        return 'explosion'
    end
    if health < 25000 then
        return 'big-explosion'
    end
    return 'big-artillery-explosion'
end

local function cell_birth(surface_index, origin_position, origin_tick, position, health, atomic)
    local key = pos_to_key(position)

    --Merge cells that are overlapping.
    if this.explosives.cells[key] then
        this.explosives.cells[key].health = this.explosives.cells[key].health + health
        return
    end

    if not atomic then
        atomic = false
    end

    --Spawn new cell.
    this.explosives.cells[key] = {
        surface_index = surface_index,
        origin_position = origin_position,
        origin_tick = origin_tick,
        position = { x = position.x, y = position.y },
        spawn_tick = game.tick + speed,
        health = health,
        atomic = atomic
    }
end

local function grow_cell(cell)
    shuffle_table(this.explosives.vectors)
    local radius = math_floor((game.tick - cell.origin_tick) / 9) + 2
    local positions = {}
    for i = 1, 4, 1 do
        local position = {
            x = cell.position.x + this.explosives.vectors[i][1],
            y = cell.position.y + this.explosives.vectors[i][2]
        }
        if not this.explosives.cells[pos_to_key(position)] then
            local distance = math_sqrt((cell.origin_position.x - position.x) ^ 2 + (cell.origin_position.y - position.y) ^ 2)
            if distance < radius then
                positions[#positions + 1] = position
            end
        end
    end

    if #positions == 0 then
        positions[#positions + 1] = {
            x = cell.position.x + this.explosives.vectors[1][1],
            y = cell.position.y + this.explosives.vectors[1][2]
        }
    end

    local new_cell_health = math_round(cell.health / #positions, 3) - this.explosives.damage_decay

    if new_cell_health <= 0 then
        return
    end

    if not cell.atomic then
        cell.atomic = false
    end

    for _, p in pairs(positions) do
        cell_birth(cell.surface_index, cell.origin_position, cell.origin_tick, p, new_cell_health, cell.atomic)
    end
end

local function damage_entity(entity, cell)
    if not entity.valid then
        return true
    end
    if not entity.health then
        return true
    end
    if entity.health <= 0 then
        return true
    end
    if not entity.destructible then
        return true
    end

    if this.explosives.whitelist_entity[entity.name] then
        return true
    end

    local damage_required = entity.health
    for _ = 1, 4, 1 do
        if damage_required > cell.health then
            entity.damage(cell.health, 'player', 'explosion')
            return false
        end
        local damage_dealt = entity.damage(damage_required, 'player', 'explosion')
        cell.health = cell.health - damage_required
        if not entity then
            return true
        end
        if not entity.valid then
            return true
        end
        if entity.health <= 0 then
            return true
        end
        damage_required = math_floor(entity.health * (damage_required / damage_dealt)) + 1
    end
end

local function damage_area(cell)
    local surface = game.surfaces[cell.surface_index]
    if not surface then
        return
    end
    if not surface.valid then
        return
    end

    if math_random(1, 4) == 1 then
        if cell.atomic then
            surface.create_entity({ name = 'nuke-explosion', position = cell.position })
        else
            surface.create_entity({ name = get_explosion_name(cell.health), position = cell.position })
        end
    end

    for _, entity in pairs(
        surface.find_entities(
            {
                { cell.position.x - density_r, cell.position.y - density_r },
                { cell.position.x + density_r, cell.position.y + density_r }
            }
        )
    ) do
        if not damage_entity(entity, cell) then
            return
        end
    end

    local tile = surface.get_tile(cell.position.x, cell.position.y)
    if not tile or not tile.valid then return end
    if this.explosives.destructible_tiles[tile.name] then
        local key = pos_to_key(tile.position)
        if not this.explosives.tiles[key] then
            this.explosives.tiles[key] = this.explosives.destructible_tiles[tile.name]
        end

        if cell.health > this.explosives.tiles[key] then
            cell.health = cell.health - this.explosives.tiles[key]
            this.explosives.tiles[key] = nil
            if math_abs(tile.position.y) < surface.map_gen_settings.height * 0.5 and math_abs(tile.position.x) < surface.map_gen_settings.width * 0.5 then
                if not check_y_pos(tile.position) then
                    surface.set_tiles({ { name = 'nuclear-ground', position = tile.position } }, true)
                end
            end
        else
            this.explosives.tiles[key] = this.explosives.tiles[key] - cell.health
            return
        end
    end

    return true
end

local function life_cycle(cell)
    if not damage_area(cell) then
        return
    end
    grow_cell(cell)
end

local function tick()
    if this.settings.disabled then
        return
    end

    if this.settings.slow_explode then
        local count = 0
        for key, cell in pairs(this.explosives.cells) do
            if cell.spawn_tick < game.tick then
                count = count + 1
                if count < this.settings.slow_explode_tick then
                    life_cycle(cell)
                    this.explosives.cells[key] = nil
                else
                    cell.spawn_tick = game.tick + speed
                end
            end
        end
    else
        for key, cell in pairs(this.explosives.cells) do
            if cell.spawn_tick < game.tick then
                life_cycle(cell)
                this.explosives.cells[key] = nil
            end
        end
    end

    if game.tick % 216000 == 0 then
        this.explosives.tiles = {}
    end
end

local function check_entity_for_items(item)
    local items = this.settings.valid_items
    for name, damage in pairs(items) do
        local amount = item.get_item_count(name)
        if amount and amount > 1 then
            return amount, damage
        end
    end
    return false
end

local function on_entity_died(event)
    if this.settings.disabled then
        return false
    end

    local entity = event.entity
    if not entity.valid then
        return
    end

    if disabled_container_names[entity.name] then
        return
    end

    if not valid_container_types[entity.type] then
        return
    end
    if this.explosives.surface_whitelist then
        if not this.explosives.surface_whitelist[entity.surface.name] then
            return
        end
    end

    local inventory = defines.inventory.chest
    if entity.type == 'car' then
        inventory = defines.inventory.car_trunk
    end

    local item = entity.get_inventory(inventory)
    local amount, damage = check_entity_for_items(item)
    if not amount then
        return
    end

    local final_damage = amount * damage

    cell_birth(entity.surface.index, { x = entity.position.x, y = entity.position.y }, game.tick, { x = entity.position.x, y = entity.position.y }, final_damage)
end

function Public.detonate_chest(entity)
    if this.settings.disabled then
        return false
    end

    if not entity or not entity.valid then
        return false
    end
    if not valid_container_types[entity.type] then
        return false
    end
    if this.explosives.surface_whitelist then
        if not this.explosives.surface_whitelist[entity.surface.name] then
            return false
        end
    end

    local inventory = defines.inventory.chest
    if entity.type == 'car' then
        inventory = defines.inventory.car_trunk
    end

    local item = entity.get_inventory(inventory)
    local amount, damage = check_entity_for_items(item)
    if not amount then
        return false
    end
    if amount < 99 then
        return false
    end

    cell_birth(entity.surface.index, { x = entity.position.x, y = entity.position.y }, game.tick, { x = entity.position.x, y = entity.position.y }, amount * damage)
    return true
end

function Public.detonate_entity(entity, amount, damage)
    if this.settings.disabled then
        return false
    end

    if not entity or not entity.valid then
        return false
    end

    if not amount then
        amount = 200
    end

    if not damage then
        damage = 700
    end

    cell_birth(entity.surface.index, { x = entity.position.x, y = entity.position.y }, game.tick, { x = entity.position.x, y = entity.position.y }, amount * damage, true)
    return true
end

function Public.reset()
    this.explosives.cells = {}
    this.explosives.tiles = {}
    if not this.explosives.vectors then
        this.explosives.vectors = { { density, 0 }, { density * -1, 0 }, { 0, density }, { 0, density * -1 } }
    end
    if not this.explosives.damage_decay then
        this.explosives.damage_decay = 10
    end
    if not this.explosives.destructible_tiles then
        this.explosives.destructible_tiles = {}
    end
    if not this.explosives.whitelist_entity then
        this.explosives.whitelist_entity = {}
    end
end

function Public.set_destructible_tile(tile_name, health)
    this.explosives.destructible_tiles[tile_name] = health
end

function Public.set_whitelist_entity(entity)
    if entity then
        this.explosives.whitelist_entity[entity] = true
    end
end

function Public.set_surface_whitelist(list)
    this.explosives.surface_whitelist = list
end

function Public.disable(state)
    this.settings.disabled = state or false
end

function Public.get_table()
    return this.explosives
end

function Public.check_growth_below_void(value)
    this.settings.check_growth_below_void = value or false
end

function Public.slow_explode(value)
    this.settings.slow_explode = value or false
end

function Public.slow_explode_tick(value)
    this.settings.slow_explode_tick = value or 300
end

local function on_init()
    Public.reset()
end

Event.on_init(on_init)
Event.on_nth_tick(speed, tick)
Event.add(defines.events.on_entity_died, on_entity_died)

return Public
