minetest.register_privilege("cellestial", {
    description = "Can manage cellestial arenas",
    give_to_admin = true,
    give_to_singleplayer = true
})
function is_cellestial(name)
    return minetest.check_player_privs(name, { cellestial = true })
end
local creative = conf.creative
arenas = {}
colors = conf.colors
function add_area(params)
    table.insert(arenas, arena.new(params))
end

function get_tile(name)
    return "cellestial_fill.png^[multiply:" .. colors[name].fill .. "^(cellestial_edge.png^[multiply:" .. colors[name].edge .. ")"
end
local border = get_tile("border")
local cell = get_tile("cell")
local max_steps = conf.max_steps

local ces = minetest.get_color_escape_sequence
local _help_content = {
    2, 1, "About",
    1, 2, 'A mod made by LMD aka appguru(eu)',
    2, 1, "Automata",
    1, 2, 'Cellular automata work using simple principles:',
    1, 2, '- the world is made out of cells, which are dead or alive',
    1, 2, '- based on their neighbors, cells die or new ones are born',
    2, 1, "Instructions",
    1, 2, [[How to simulate cellular automata using Cellestial.
Remember that you can open this dialog using "/cells help".]],
    3, 2, "Chat",
    1, 3, [[The chat is where you talk with others and send commands.
Start your message with @name to send it to a player.
Use @#id to send it to all owners of the arena.]],
    3, 2, "Commands",
    1, 3,
    [[Use chatcommands to manage your arena and simulation.
Send "/help cells" in chat to see further help.]],
    3, 2, "Arenas",
    1, 3,
    [[Arenas are areas delimited by undestructible borders.
Only their owners can modify them.]],
    3, 2, "Cells",
    1, 3, "Cells live in your arenas. You can place & dig them at any time.",
    3, 2, "Wand",
    1, 3,
    [[A powerful tool controlling the simulation.
Right-click to configure, left-click to apply.
Possible modes / actions are:
- Advance: Simulates steps
- Simulate: Starts / stops simulation, steps per second
- Place: Living cell ray, steps are length
- Dig: Dead cell ray, steps are length
Rules work as follows:
- Short notation: As described by Bayes. Uses base 27.
- Neighbors: Numbers signify the amount of neighbors.]],
}
for i = 1, #_help_content, 3 do
    _help_content[i] = ({ "#FFFFFF", colors.cell.fill, colors.cell.edge })[_help_content[i]]
end
local help_content = {}
for i = 1, #_help_content, 3 do
    local parts = modlib.text.split(_help_content[i + 2], "\n")
    for _, part in ipairs(parts) do
        table.insert(help_content, _help_content[i])
        table.insert(help_content, _help_content[i + 1])
        table.insert(help_content, minetest.formspec_escape(part))
    end
end
help_formspec = ([[
size[8,5]
real_coordinates[true]
box[0,0;8,1;%s]
label[0.25,0.35;%sCellestial%s - cellular automata for Minetest]
label[0.25,0.7;%shttps://appgurueu.github.io/cellestial]
tablecolumns[color;tree;text]
tableoptions[background=#00000000;highlight=#00000000;border=false;opendepth=2]
table[-0.15,1.25;7.9,3.5;help;%s]
image_button_exit[7.25,0.25;0.5,0.5;cmdlib_cross.png;close;]
]]):format(colors.cell.fill, ces(colors.cell.edge), ces("#FFFFFF"), ces(colors.cell.edge), table.concat(help_content, ","))

function show_help(name)
    minetest.show_formspec(name, "cellestial:help", help_formspec)
end

-- Almost indestructible borders
minetest.register_node("cellestial:border", {
    description = "Arena Border",
    post_effect_color = colors.border.fill,
    sunlight_propagates = true,
    light_source = minetest.LIGHT_MAX,
    tiles = { border },
    groups = { not_in_creative_inventory = 1, fall_damage_add_percent = -100 },
    can_dig = function()
        return false
    end,
    on_dig = function()
    end,
    on_place = function()
    end,
    on_use = function()
    end,
    on_secondary_use = function()
    end
})
-- Cells, item can be used for digging & placing
minetest.register_node("cellestial:cell", {
    description = "Cell",
    -- TODO find a proper way for borders connecting to cells
    post_effect_color = "#00000000",
    sunlight_propagates = true,
    light_source = minetest.LIGHT_MAX,
    tiles = { cell },
    groups = { oddly_breakable_by_hand = 3, fall_damage_add_percent = -100 },
    range = (creative and 20) or 4,
    on_dig = function(pos, node, digger)
        if minetest.is_protected(pos, digger:get_player_name()) then
            return
        end
        local arena = arena.get(pos)
        if arena and arena:is_owner(digger:get_player_name()) then
            arena:set_cell(pos)
        else
            return minetest.node_dig(pos, node, digger)
        end
        if not creative then
            local leftover = digger:get_inventory():add_item("main", "cellestial:cell")
            if leftover then
                minetest.add_item(pos, leftover)
            end
        end
    end,
    on_place = function(itemstack, placer, pointed_thing)
        local pos = pointed_thing.above
        if not conf.place_inside_player then
            for _, player in pairs(minetest.get_connected_players()) do
                local ppos = player:get_pos()
                ppos.y = ppos.y + player:get_properties().eye_height
                if ppos.x >= pos.x and ppos.y >= pos.y and ppos.z >= pos.z and ppos.x <= pos.x +1 and ppos.y <= pos.y + 1 and ppos.z <= pos.z + 1 then
                    return itemstack
                end
            end
        end
        if minetest.is_protected(pos, placer:get_player_name()) then
            return
        end
        local arena = arena.get(pos)
        if arena and arena:is_content(pos) and arena:is_owner(placer:get_player_name()) then
            arena:set_cell(pos, true)
        elseif regular_placing then
            return minetest.item_place_node(itemstack, placer, pointed_thing)
        end
        if not creative then
            itemstack:take_item()
            return itemstack
        end
    end
})
local serialized_modes = { advance = "a", simulate = "s", place = "p", dig = "d" }
local function serialize_rule(rule)
    local number = 0
    for i = 26, 0, -1 do
        number = number * 2
        if rule[i] then
            number = number + 1
        end
    end
    return modlib.number.tostring(number, 36)
end
function serialize_wand(wand, meta)
    meta:set_string("mode", serialized_modes[wand.mode])
    meta:set_string("steps", modlib.number.tostring(wand.steps, 36))
    meta:set_string("death", serialize_rule(wand.rule.death))
    meta:set_string("birth", serialize_rule(wand.rule.birth))
end
local deserialized_modes = modlib.table.flip(serialized_modes)
local function deserialize_rule(text)
    local number = tonumber(text, 36)
    local rule = {}
    for i = 0, 26 do
        local digit = math.floor(number % 2)
        rule[i] = digit == 1
        number = math.floor(number / 2)
    end
    return rule
end
function deserialize_mode(meta)
    return deserialized_modes[meta:get("mode")]
end
function deserialize_steps(meta)
    return tonumber(meta:get("steps"), 36)
end
function deserialize_full_rule(meta)
    return { death = deserialize_rule(meta:get("death")), birth = deserialize_rule(meta:get("birth")) }
end
function deserialize_wand(meta)
    return {
        mode = deserialize_mode(meta),
        steps = deserialize_steps(meta),
        rule = deserialize_full_rule(meta)
    }
end
local c0, ca, cA = ("0"):byte(), ("a"):byte(), ("A"):byte()
function read_rule(text)
    if text:len() ~= 4 then
        return nil
    end
    local nums = { text:byte(1), text:byte(2), text:byte(3), text:byte(4) }
    for i, num in pairs(nums) do
        if num >= ca then
            num = num - ca + 10
        elseif num >= cA then
            num = num - cA + 10
        else
            num = num - c0
        end
        if num < 0 or num > 26 then
            return nil
        end
        nums[i] = num
    end
    if nums[1] > nums[2] or nums[3] > nums[4] then
        return nil
    end
    local min_env, max_env, min_birth, max_birth = unpack(nums)
    local rule = { death = {}, birth = {} }
    for i = 0, 26 do
        rule.death[i] = not (i >= min_env and i <= max_env)
        rule.birth[i] = i >= min_birth and i <= max_birth
    end
    return rule
end
local dfunc = modlib.number.default_digit_function
function find_rule(rule)
    local death, birth = rule.death, rule.birth
    -- Finding min. env. and max. env
    local min_env, max_env
    local i = 0
    while i <= 26 and death[i] do
        i = i + 1
    end
    min_env = i
    while i <= 26 and not death[i + 1] do
        i = i + 1
    end
    max_env = i
    for i = max_env + 1, 26 do
        if not death[i] then
            return
        end
    end
    -- Finding min. birth and max. birth
    local min_birth, max_birth
    i = 0
    while i <= 26 and not birth[i] do
        i = i + 1
    end
    min_birth = i
    while i <= 26 and birth[i + 1] do
        i = i + 1
    end
    max_birth = i
    for i = max_birth + 1, 26 do
        if birth[i] then
            return
        end
    end
    return dfunc(min_env) .. dfunc(max_env) .. dfunc(min_birth) .. dfunc(max_birth)
end
local default_wand = {
    mode = "advance",
    steps = 1,
    rule = read_rule("5766")
}
local ray_steps = 10
function ray_function(cell)
    return function(steps, player, arena)
        local eye_offset = player:get_eye_offset()
        eye_offset.y = eye_offset.y + player:get_properties().eye_height
        local lookdir = player:get_look_dir()
        local start = vector.add(vector.add(player:get_pos(), eye_offset), lookdir)
        local step = vector.multiply(lookdir, 1 / ray_steps)
        local set = {}
        local set_count = 0
        local pos = start
        for _ = 1, ray_steps * steps * math.sqrt(3) do
            local rounded = vector.round(pos)
            local min, max = arena.min, arena.max
            if rounded.x <= min.x or rounded.y <= min.y or rounded.z <= min.z or rounded.x >= max.x or rounded.y >= max.y or rounded.z >= max.z then
                break
            end
            local index = arena.voxelarea:indexp(rounded)
            if not set[index] then
                set[index] = true
                arena:set_cell(rounded, cell)
                set_count = set_count + 1
                if set_count == steps then
                    break
                end
            end
            pos = vector.add(pos, step)
        end
    end
end
actions = {
    advance = function(steps, _, arena, meta)
        arena:next_steps(steps, deserialize_full_rule(meta))
    end,
    simulate = function(steps, _, arena, meta)
        arena:simulate(steps, deserialize_full_rule(meta))
    end,
    place = ray_function(true),
    dig = ray_function()
}
function show_wand_formspec(name, wand)
    local function get_image(n)
        if wand.rule.death[n] then
            if wand.rule.birth[n] then
                return "cellestial_fertility.png"
            end
            return "cellestial_border.png"
        else
            if wand.rule.birth[n] then
                return "cellestial_cell.png"
            end
            return "cellestial_environment.png"
        end
    end
    local neighbor_buttons = {
        "image_button[5.25,1.25;0.5,0.5;" .. get_image(0) .. ";n0;0;false;false]",
        "image_button[6.25,1.25;0.5,0.5;" .. get_image(1) .. ";n1;1;false;false]",
        "image_button[7.25,1.25;0.5,0.5;" .. get_image(2) .. ";n2;2;false;false]"
    }
    for y = 0, 2 do
        for x = 0, 7 do
            local n = y * 8 + x + 3
            local t = get_image(n)
            table.insert(neighbor_buttons, ("image_button[%s,%s;0.5,0.5;%s;n%d;%d;false;false]"):format(tostring(0.25 + x * 1), tostring(2 + y * 0.75), t, n, n))
        end
    end
    neighbor_buttons = table.concat(neighbor_buttons, "\n")
    minetest.show_formspec(name, "cellestial:wand",
            ([[
size[8,5]
real_coordinates[true]
box[0,0;8,1;%s]
label[0.25,0.5;Mode:]
dropdown[1,0.25;1.5,0.5;mode;Advance,Simulate,Place,Dig;%d]
label[2.75,0.5;Steps:]
button[3.5,0.25;0.5,0.5;steps_minus;-]
field[4,0.25;0.75,0.5;steps;;%d]
field_close_on_enter[steps;false]
button[4.75,0.25;0.5,0.5;steps_plus;+]
button[5.75,0.25;1,0.5;apply;Apply]
image_button_exit[7.25,0.25;0.5,0.5;cmdlib_cross.png;close;]
label[0.25,1.5;Rule:]
field[1,1.25;1,0.5;rule;;%s]
button[2.25,1.25;1,0.5;set;Set]
label[3.75,1.5;Neighbors:]
%s
image[0.25,4.25;0.5,0.5;cellestial_border.png]
label[1,4.5;Death]
image[2.25,4.25;0.5,0.5;cellestial_environment.png]
label[3,4.5;Survival]
image[4.25,4.25;0.5,0.5;cellestial_fertility.png]
label[5,4.5;Birth]
image[6.25,4.25;0.5,0.5;cellestial_cell.png]
label[7,4.5;Both]
]]):format(colors.cell.fill, ({ advance = 1, simulate = 2, place = 3, dig = 4 })[wand.mode], wand.steps, find_rule(wand.rule) or "", neighbor_buttons))
end

function ensure_wand(meta)
    if not meta:get("mode") or not meta:get("steps") or not meta:get("death") or not meta:get("birth") then
        serialize_wand(default_wand, meta)
        return true
    end
end

function obtain_wand(meta)
    local wand
    if ensure_wand(meta) then
        wand = modlib.table.tablecopy(default_wand)
    else
        wand = deserialize_wand(meta)
    end
    return wand
end

function wand_on_secondary_use(itemstack, user, pointed_thing)
    local name = user:get_player_name()
    local meta = itemstack:get_meta()
    show_wand_formspec(name, obtain_wand(meta))
    return itemstack
end

-- Wand
minetest.register_tool("cellestial:wand", {
    description = "Cellestial Wand",
    inventory_image = "cellestial_wand.png",
    on_use = function(itemstack, user, pointed_thing)
        local name = user:get_player_name()
        local arena = arena.get_by_name(name)
        if arena and arena:is_owner(name) then
            local meta = itemstack:get_meta()
            ensure_wand(meta)
            local mode = deserialize_mode(meta)
            actions[mode](deserialize_steps(meta), user, arena, meta)
        end
        return itemstack
    end,
    on_secondary_use = wand_on_secondary_use,
    on_place = wand_on_secondary_use
})
modlib.minetest.register_form_listener("cellestial:wand", function(player, fields)
    if fields.quit then
        return
    end

    local wielded_item = player:get_wielded_item()
    local meta = wielded_item:get_meta()
    local wand = obtain_wand(meta)
    if fields.steps then
        local steps = tonumber(fields.steps)
        if steps then
            wand.steps = steps
        end
    end
    if fields.mode then
        local lower = fields.mode:lower()
        if serialized_modes[lower] then
            wand.mode = lower
        end
    end
    if fields.apply then
        local arena = arena.get_by_player(player)
        if arena and arena:is_owner(player:get_player_name()) then
            actions[wand.mode](wand.steps, player, arena, meta)
        end
    elseif fields.set or fields.key_enter_field == "rule" then
        local rule = read_rule(fields.rule)
        if rule then
            wand.rule = rule
        end
    elseif fields.steps_minus then
        wand.steps = wand.steps - 1
    elseif fields.steps_plus then
        wand.steps = wand.steps + 1
    else
        for field, _ in pairs(fields) do
            if modlib.text.starts_with(field, "n") then
                local n = tonumber(field:sub(2))
                if n then
                    if wand.rule.birth[n] then
                        if wand.rule.death[n] then
                            wand.rule.death[n] = false
                        else
                            wand.rule.death[n] = true
                            wand.rule.birth[n] = false
                        end
                    else
                        if wand.rule.death[n] then
                            wand.rule.death[n] = false
                        else
                            wand.rule.death[n] = true
                            wand.rule.birth[n] = true
                        end
                    end
                end
                break
            end
        end
    end
    wand.steps = math.max(1, math.min(wand.steps, max_steps))
    serialize_wand(wand, meta)
    player:set_wielded_item(wielded_item)
    if not fields.close then
        show_wand_formspec(player:get_player_name(), wand)
    end
end)

local adv_chat = minetest.global_exists("adv_chat") and adv_chat

minetest.register_on_joinplayer(function(player)
    arena.get(player:get_pos())
    local name = player:get_player_name()
    for _, id in pairs(arena.list_ids_by_name(name)) do
        local role = "#" .. id
        if adv_chat and adv_chat.roles[role] then
            adv_chat.add_role(name, role)
        end
    end
end)

if adv_chat then
    adv_chat.roles.minetest.color = colors.cell.fill
end