utils/alien_evolution_progress.lua
--[[-- info
    Original (javascript) version: https://hastebin.com/udakacavap.js
    Can be tested against: https://wiki.factorio.com/Enemies#Spawn_chances_by_evolution_factor
]]

-- dependencies
local Global = require 'utils.global'
local Debug = require 'utils.debug'
local table = require 'utils.table'

-- localized functions
local get_random_weighted = table.get_random_weighted
local round = math.round
local ceil = math.ceil
local floor = math.floor
local random = math.random
local pairs = pairs
local format = string.format

-- this
local AlienEvolutionProgress = {}

local memory = {
    spawner_specifications = {},
    spawner_specifications_count = 0,
    evolution_cache = {
        ['biter-spawner'] = {
            evolution = -1,
            weight_table = {},
        },
        ['spitters-spawner'] = {
            evolution = -1,
            weight_table = {},
        },
    },
}

Global.register_init({
    memory = memory,
}, function(tbl)
    for name, prototype in pairs(prototypes.entity) do
        if prototype.type == 'unit-spawner' and prototype.subgroup.name == 'enemies' then
            tbl.memory.spawner_specifications[name] = prototype.result_units
            memory.spawner_specifications_count = memory.spawner_specifications_count + 1
        end
    end
end, function(tbl)
    memory = tbl.memory
end)

local function lerp(low, high, pos)
    local s = high.evolution_factor - low.evolution_factor;
    local l = (pos - low.evolution_factor) / s;
    return (low.weight * (1 - l)) + (high.weight * l)
end

local function get_values(map, evolution_factor)
    local result = {}
    local sum = 0

    for _, spawner_data in pairs(map) do
        local list = spawner_data.spawn_points;
        local low = list[1];
        local high = list[#list];

        for _, val in pairs(list) do
            local val_evolution = val.evolution_factor
            if val_evolution <= evolution_factor and val_evolution > low.evolution_factor then
                low = val;
            end
            if val_evolution >= evolution_factor and val_evolution < high.evolution_factor then
                high = val
            end
        end

        local val
        if evolution_factor <= low.evolution_factor then
            val = low.weight
        elseif evolution_factor >= high.evolution_factor then
            val = high.weight;
        else
            val = lerp(low, high, evolution_factor)
        end
        sum = sum + val;

        result[spawner_data.unit] = val;
    end

    local weighted_table = {}
    local count = 0
    for index, _ in pairs(result) do
        count = count + 1
        weighted_table[count] = {index, result[index] / sum}
    end

    return weighted_table;
end

local function get_spawner_values(spawner, evolution)
    local spawner_specification = memory.spawner_specifications[spawner]
    if not spawner_specification then
        Debug.print(format('Spawner "%s" does not exist in the prototype data', spawner))
        return
    end

    local cache = memory.evolution_cache[spawner]

    if not cache then
        cache = {
            evolution = -1,
            weight_table = {},
        }
        memory.evolution_cache[spawner] = cache
    end

    local evolution_value = round(evolution * 100)
    if (cache.evolution < evolution_value) then
        cache.evolution = evolution_value
        cache.weight_table = get_values(spawner_specification, evolution)
    end

    return cache.weight_table
end

local function calculate_total(count, spawner, evolution)
    if count == 0 then
        return {}
    end

    local spawner_values = get_spawner_values(spawner, evolution)
    if not spawner_values then
        return {}
    end

    local aliens = {}
    for _ = 1, count do
        local name = get_random_weighted(spawner_values)
        aliens[name] = (aliens[name] or 0) + 1
    end

    return aliens
end

---Creates the spawner_request structure required for AlienEvolutionProgress.get_aliens for all
---available spawners. If dividing the total spawners by the total aliens causes a fraction, the
---fraction will decide a chance to spawn. 1 alien for 2 spawners will have 50% on both.
---@param total_aliens table
function AlienEvolutionProgress.create_spawner_request(total_aliens)
    local per_spawner = total_aliens / memory.spawner_specifications_count
    local fraction = per_spawner % 1

    local spawner_request = {}
    for spawner, _ in pairs(memory.spawner_specifications) do
        local count = per_spawner
        if fraction > 0 then
            if random() > fraction then
                count = ceil(count)
            else
                count = floor(count)
            end
        end
        spawner_request[spawner] = count
    end

    return spawner_request
end

function AlienEvolutionProgress.get_aliens(spawner_requests, evolution)
    local aliens = {}
    for spawner, count in pairs(spawner_requests) do
        for name, amount in pairs(calculate_total(count, spawner, evolution)) do
            aliens[name] = (aliens[name] or 0) + amount
        end
    end

    return aliens
end

return AlienEvolutionProgress
