--
--{{{ print_r(t)
local function print_r(t)
    local print_r_cache = {}
    local function sub_print_r(t, indent)
        if (print_r_cache[tostring(t)]) then
            print(indent .. "*" .. tostring(t))
        else
            print_r_cache[tostring(t)] = true
            if (type(t) == "table") then
                local tLen = #t
                for i = 1, tLen do
                    local val = t[i]
                    if (type(val) == "table") then
                        print(indent .. "#[" .. i .. "] => " .. tostring(t) .. " {")
                        sub_print_r(val, indent .. string.rep(" ", string.len(i) + 8))
                        print(indent .. string.rep(" ", string.len(i) + 6) .. "}")
                    elseif (type(val) == "string") then
                        print(indent .. "#[" .. i .. '] => "' .. val .. '"')
                    else
                        print(indent .. "#[" .. i .. "] => " .. tostring(val))
                    end
                end
                for pos, val in pairs(t) do
                    if type(pos) ~= "number" or math.floor(pos) ~= pos or (pos < 1 or pos > tLen) then
                        if (type(val) == "table") then
                            print(indent .. "[" .. pos .. "] => " .. tostring(t) .. " {")
                            sub_print_r(val, indent .. string.rep(" ", string.len(pos) + 8))
                            print(indent .. string.rep(" ", string.len(pos) + 6) .. "}")
                        elseif (type(val) == "string") then
                            print(indent .. "[" .. pos .. '] => "' .. val .. '"')
                        else
                            print(indent .. "[" .. pos .. "] => " .. tostring(val))
                        end
                    end
                end
            else
                print(indent .. tostring(t))
            end
        end
    end

    if (type(t) == "table") then
        print(tostring(t) .. " {")
        sub_print_r(t, "  ")
        print("}")
    else
        sub_print_r(t, "  ")
    end

    print()
end
--}}}

--{{{ table_count(t)
function table_count(t)
    local count = 0
    for _ in pairs(t) do count = count + 1 end
    return count
end

--}}}

--{{{ range(low, high, n)
---Create a table with integers in a given range
---@param low integer Lowest number
---@param high integer Highest number
---@param n integer Increment - the array will contain every nth number between [low..high]
---@return table
local function range(low, high, n)
    local t = {}
    for i = low, high, n do
        table.insert(t, i)
    end

    return t
end
---}}}

-- {{{ sortMapByValueDesc(m)
---Sort the table m by value.
---@param m table
---@return table
local function sortMapByValueDesc(m)
    local tmp = {}

    -- create a temporary table that is easier to sort
    for damage, occurrences in pairs(m) do
        local entry = {
            damage = damage,
            count = occurrences,
        }
        table.insert(tmp, entry)
    end

    table.sort(tmp, function(a, b)
        return a.count > b.count
    end)

    return tmp
end
--}}}

-- {{{ arraySum(t)
local function table_sum(t)
    local accumulator = 0

    for _, v in pairs(t) do
        accumulator = accumulator + v
    end

    return accumulator
end
---}}}

--{{{ mostCommonDamageValues(instances, ratio)
--- Calculate the most likely values
---@param instances table A map of [damage score] => [occurrences]
---@param ratio number Include most common damage until the sum of the probabilities of the damage scores are at least this number.
---@return table
local function mostCommonDamageValues(instances, ratio)
    if ratio == nil then
        ratio = 0.25
    end

    local sortedInstances = sortMapByValueDesc(instances)
    local totalOccurrences = table_sum(instances)

    local result = {}
    local occurrenceAccumulator = 0

    for _, instance in pairs(sortedInstances) do
        occurrenceAccumulator = occurrenceAccumulator + instance.count
        table.insert(result, instance.damage)

        if occurrenceAccumulator / totalOccurrences > ratio then
            return result
        end
    end

    return result
end
--}}}

-- {{{ calculateDamageD20(skill, base_damage)
---@param skill integer
---@param base_damage integer
---@return number average The average damage
---@return table typical The damage numbers that, together, are likely to occur more than 50% of the time.
local function calculateDamageD20(skill, base_damage)
    local sum = 0
    local n = 0
    local tn = math.min(skill, 15)

    local outcomes = {}

    for rolled = 1, 20, 1 do
        local damage = 0

        if rolled > tn then
            -- glancing blow
            damage = base_damage
        end
        if rolled == tn then
            -- crit
            damage = 2 * (skill + base_damage)
        end
        if rolled < tn then
            -- hit
            damage = rolled + base_damage
        end

        local key = tostring(damage)

        if outcomes[key] == nil then
            outcomes[key] = 1
        else
            outcomes[key] = outcomes[key] + 1
        end

        sum = sum + damage
        n = n + 1
    end

    return sum / n, mostCommonDamageValues(outcomes, 0.5)
end
-- }}}

local skill_min = 5
local skill_max = 15
local skill_inc = 1
local skills_table = range(skill_min, skill_max, skill_inc)

local damage_min = 0
local damage_max = 10
local damage_inc = 3
local damages_table = range(damage_min, damage_max, damage_inc)

io.write("skill\\damage", ";")
for i, dmg in pairs(damages_table) do
    io.write(string.format("%d (avg);%d (typical)", dmg, dmg))
    if i < #damages_table then
        io.write(";")
    end
end
print("")

for _, skill in pairs(skills_table) do
    io.write(skill, ";")

    local tmp = {}
    for key, damage in ipairs(damages_table) do
        local avg, dmg = calculateDamageD20(skill, damage)
        tmp[key] = string.format("%.2f;%s", avg, table.concat(dmg, ", "))
    end
    print(table.concat(tmp, ";"))
end
