-- see elepower_compat >> external.lua for explanation
-- shorten table ref
local epi = ele.external.ing
local epr = ele.external.ref
local efs = ele.formspec
local S = ele.translator

--[[
	Reactor fitness check:
		8x8x8 area surrounding the core must either contain..
			Water source nodes
			Neutron Absorber (with active medium)
			Fluid Port (with COLD coolant available)
			Also acceptable nodes:
				Any fluid transfer conduit
				Any reactor component
			Unacceptable nodes (These raise heat INSTANTLY!):
				Lava source
				Hot coolant
		..in order to keep the heat below critical. Any other detected node will either be MOLTEN or ACTIVATED (TODO) (you don't want this!)
	Reactor core will be replaced by a molten core when the heat reaches 100%. All components and fuel will be lost!
]]

local AREA_SIZE = 8

local function calculate_fitness(pos)
    -- Calculate the heat sink percentage
    -- Amount of nodes we shall count down from
    local add = {x = (AREA_SIZE) / 2, y = (AREA_SIZE) / 2, z = (AREA_SIZE) / 2}
    local minp = vector.subtract(pos, add)
    local maxp = vector.add(pos, add)

    -- Get the vmanip mapgen object and the nodes and VoxelArea
    local manip = minetest.get_voxel_manip()
    local e1, e2 = manip:read_from_map(minp, maxp)
    local area = VoxelArea:new{MinEdge = e1, MaxEdge = e2}
    local data = manip:get_data()

    local ids = {
        c_water = minetest.get_content_id(epi.water_source),
        c_lava = minetest.get_content_id(epi.lava_source)
    }

    local excession = 0
    local hu = 0
    local nodes = 0
    for i in area:iter(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z) do
        nodes = nodes + 1
        if data[i] == ids["c_water"] then
            hu = hu - 1
        elseif data[i] == ids["c_lava"] then
            hu = hu + 1
        else
            local dp = minetest.get_name_from_content_id(data[i])
            if excession <= 16 and
                (ele.helpers.get_item_group(dp, "ele_reactor_component") or
                    ele.helpers.get_item_group(dp, "ele_neutron_absorbant") or
                    ele.helpers.get_item_group(dp, "fluid_transport_source") or
                    ele.helpers.get_item_group(dp, "fluid_transport") or
                    ele.helpers.get_item_group(dp, "tube") or
                    ele.helpers.get_item_group(dp, "tubedevice")) then
                hu = hu - 1
                excession = excession + 1
            elseif ele.helpers.get_item_group(dp, "hot") then
                hu = hu + 1
            else
                hu = hu + 1
            end
        end
    end

    hu = nodes + hu

    return 100 - math.floor(100 * hu / nodes), hu
end

local function fuel_after_depletion(inv, power)
    local fuel_count = 0
    local change = false

    local list = inv:get_list("fuel")
    for i, stack in pairs(list) do
        local sname = stack:get_name()
        if ele.helpers.get_item_group(sname, "fissile_fuel") then
            local stdef = minetest.registered_items[sname]
            if stdef.fissile_count then
                local meta = stack:get_meta()
                local fscount = meta:get_int("fission_count")
                if fscount < stdef.fissile_count then
                    fscount = fscount + 1
                    fuel_count = fuel_count + 1

                    meta:set_int("fission_count", fscount)
                    meta:set_string("description",
                                    ("%s\n" .. S("Depleted") .. ": %d "):format(
                                        stdef.description, math.floor(
                                            100 * fscount / stdef.fissile_count)) ..
                                        " %")
                else
                    stack = ItemStack("elepower_nuclear:fuel_rod_depleted")
                end
                list[i] = stack
                change = true
            end
        end
    end

    if change then inv:set_list("fuel", list) end

    return fuel_count
end

local function can_dig(pos, player)
    local meta = minetest.get_meta(pos)
    local inv = meta:get_inventory()
    return inv:is_empty("fuel")
end

local function allow_metadata_inventory_put(pos, listname, index, stack, player)
    if minetest.is_protected(pos, player:get_player_name()) then return 0 end

    if not ele.helpers.get_item_group(stack:get_name(), "fissile_fuel") then
        return 0
    end

    return stack:get_count()
end

local function allow_metadata_inventory_move(pos, from_list, from_index,
                                             to_list, to_index, count, player)
    local meta = minetest.get_meta(pos)
    local inv = meta:get_inventory()
    local stack = inv:get_stack(from_list, from_index)
    return allow_metadata_inventory_put(pos, to_list, to_index, stack, player)
end

local function get_core_formspec(heat, power)
    local start, bx, by, mx, _, center_x = efs.begin(11.75, 10.45)
    local status = S("Activate by extracting the control rods")

    if heat > 80 then
        status = S("!!! TEMPERATURE CRITICAL !!!")
    elseif heat > 90 then
        status = S("!!! REACTOR CRITICAL !!!")
    elseif heat > 95 then
        status = S("!!! REACTOR MELTDOWN IMMINENT !!!")
    elseif power > 0 then
        status = S("Active reaction chain")
    end

    return start .. efs.list("context", "fuel", center_x - 1.25, by, 3, 3) ..
               efs.create_bar(bx, by, power, "#ff0000", true) ..
               efs.create_bar(bx + 0.5, by, heat, "#ffdd11", true) ..
               efs.tooltip(bx, by, 0.25, 2.9, S("Power: @1", power .. "%")) ..
               efs.tooltip(bx + 0.5, by, 0.25, 2.9, S("Heat: @1", heat .. "%")) ..
               efs.label(bx, by + 4.25, status) .. epr.gui_player_inv() ..
               "listring[current_player;main]" .. "listring[context;fuel]" ..
               "listring[current_player;main]"
end

-- TODO: Reactor-dependent rod count

local function get_controller_formspec(rod_pos, selected)
    local start, bx, by, mx, _, center_x = efs.begin(11.75, 10.45)
    local gutter = 1.25
    local stops = ((mx - bx) + gutter) / 3 - gutter - 0.125
    local ctrls = {}

    for num, depth in pairs(rod_pos) do
        local xoffset = bx + (stops * (num - 1)) + gutter
        local sel = ""

        if num == selected then sel = " <- " end

        local fspc = efs.label(xoffset, by, depth .. " %" .. sel)
        fspc = fspc ..
                   efs.create_bar(xoffset, by + 0.25, 100 - depth, "#252625",
                                  true)

        table.insert(ctrls, fspc)
    end

    return start .. table.concat(ctrls, "") ..
               efs.button(bx, by + 3.5, 1.5, 0.5, "next", S("Next")) ..
               efs.button(bx + 1.75, by + 3.5, 1.5, 0.5, "prev", S("Previous")) ..
               efs.button(center_x - 0.25, by + 3.5, 1.5, 0.5, "stop",
                          S("SCRAM")) ..
               efs.button(mx - 1.5, by + 3.5, 1.5, 0.5, "up", S("Raise")) ..
               efs.button(mx - 3.25, by + 3.5, 1.5, 0.5, "down", S("Lower")) ..
               "tooltip[next;" .. S("Select the next control rod") .. "]" ..
               "tooltip[prev;" .. S("Select the previous control rod") .. "]" ..
               "tooltip[stop;" ..
               S(
                   "Drops all the rods into the reactor core, instantly stopping it") ..
               "]" .. "tooltip[up;" .. S("Raise selected control rod") .. "]" ..
               "tooltip[down;" .. S("Lower selected control rod") .. "]" ..
               epr.gui_player_inv() .. "listring[current_player;main]"
end

local function get_port_formspec(cool, hot)
    local start, bx, by, mx, _, center_x = efs.begin(11.75, 10.45)
    return start .. efs.fluid_bar(bx, by, cool) ..
               efs.progress(center_x, by + 1.25, 0) ..
               efs.fluid_bar(mx - 1, by, hot) .. epr.gui_player_inv() ..
               "listring[current_player;main]"
end

local function reactor_core_timer(pos)
    local meta = minetest.get_meta(pos)
    local inv = meta:get_inventory()
    local headless = false
    local fuel_reactor = 0

    -- SAFEGUARD: Expect a controller to be above the core
    local controller_pos = {x = pos.x, y = pos.y + 1, z = pos.z}
    local controller_node = minetest.get_node_or_nil(controller_pos)
    if not controller_node or controller_node.name ~=
        "elepower_nuclear:fission_controller" then
        -- Don't do anything without a head
        headless = true
    end

    local controller = minetest.get_meta(controller_pos)

    -- Only read area ever so often
    -- Calculate the absorbance of heat around the core
    local hp = meta:get_int("absorbance")
    local absorb_tick = meta:get_int("absorb_tick")
    if absorb_tick > 10 then
        hp = calculate_fitness(pos)
        absorb_tick = 0
    else
        absorb_tick = absorb_tick + 1
    end

    meta:set_int("absorb_tick", absorb_tick)
    meta:set_int("absorbance", hp)

    -- Get reactor power setting
    local power_setting_target = controller:get_int("setting")
    local power_setting = meta:get_int("setting")

    -- Do nothing
    if headless then
        power_setting = 0
    else
        if not (power_setting_target == 0 and power_setting == 0) then
            -- Decrease or increase power
            if power_setting_target > power_setting then
                power_setting = power_setting + 1
            elseif power_setting_target < power_setting then
                power_setting = power_setting - 5
            end

            if power_setting < 0 then
                power_setting = 0
            elseif power_setting > 100 then
                power_setting = 100
            end
        end
    end

    -- Deplete fuel
    if power_setting > 0 then
        fuel_reactor = fuel_after_depletion(inv, power_setting)
        if fuel_reactor == 0 then
            -- Enforce zero power setting when no fuel present
            power_setting = 0
        end
    end

    -- Set power setting
    meta:set_int("setting", power_setting)

    -- Get reactor heat
    local heat = meta:get_int("heat")

    -- Calculate heat
    -- I dont really know what I'm doing here, just playing around with the numbers
    -- to get something workable
    if power_setting > 5 then
        local ceiling = math.floor(power_setting / 2)
        if heat > ceiling and hp > 75 then
            -- Remove heat when the heat goes above power setting and there's sufficient coolant
            heat = heat + math.floor(((74 - hp) / 2) / ceiling)
        else
            -- Heat up the reactor by the amount of fuel
            -- If the reactor coolant is insufficient, add that factor to play
            heat = heat + fuel_reactor + math.floor(80 * (1 - (hp / 100)))

            -- Catch up to the power setting
            if heat < ceiling then
                heat = heat + math.floor((ceiling - heat) / 2)
            end
        end
    elseif heat > 0 then
        heat = heat + math.floor((-hp) / 4)
    end

    if heat >= 100 then
        minetest.set_node(pos, {name = "elepower_nuclear:corium_source"})
        return false
    end

    if heat < 0 then heat = 0 end

    -- Nothing left to do in this timer, exit
    if power_setting == 0 and heat == 0 then
        meta:set_int("heat", heat)
        meta:set_string("formspec", get_core_formspec(heat, power_setting))
        return false
    end

    -- Expect a fluid port below the core
    -- TODO: Allow multiple fluid ports in the core's affected area
    local fluid_port_pos = {x = pos.x, y = pos.y - 1, z = pos.z}
    local fluid_port_node = minetest.get_node_or_nil(fluid_port_pos)
    if fluid_port_node ~= nil and fluid_port_node.name ==
        "elepower_nuclear:reactor_fluid_port" then
        local fpmeta = minetest.get_meta(fluid_port_pos)

        -- Calculate how much heat is given to the fluid port
        local burst_strength = math.max(math.floor((heat / 100) * 64), 1)

        if fpmeta:get_int("burst") == 0 and heat > 0 then
            fpmeta:set_int("burst", burst_strength)
            minetest.get_node_timer(fluid_port_pos):start(1.0)
            heat = heat - burst_strength
        end
    end

    meta:set_int("heat", heat)
    meta:set_string("formspec", get_core_formspec(heat, power_setting))

    return true
end

local function reactor_controller_timer(pos)
    local meta = minetest.get_meta(pos)
    local settings = {}
    local averg = 0

    for i = 1, 4 do
        table.insert(settings, meta:get_int("c" .. i))
        averg = averg + settings[i]
    end

    meta:set_int("setting", 100 - (averg / 4))
    meta:set_string("formspec",
                    get_controller_formspec(settings, meta:get_int("selected")))

    -- Ping the core
    local core_pos = {x = pos.x, y = pos.y - 1, z = pos.z}
    local core_node = minetest.get_node_or_nil(core_pos)
    if core_node and core_node.name == "elepower_nuclear:fission_core" then
        local timer = minetest.get_node_timer(core_pos)
        if not timer:is_started() then timer:start(1.0) end
    end

    return false
end

local function reactor_controller_manage(pos, formname, fields, sender)
    if sender and sender ~= "" and
        minetest.is_protected(pos, sender:get_player_name()) then return end

    local meta = minetest.get_meta(pos)
    local selected = meta:get_int("selected")
    local change = false

    if fields["next"] then
        selected = selected + 1
        if selected > 4 then selected = 1 end

        meta:set_int("selected", selected)
        change = true
    elseif fields["prev"] then
        selected = selected - 1
        if selected == 0 then selected = 4 end

        meta:set_int("selected", selected)
        change = true
    elseif fields["stop"] then
        for i = 1, 4 do meta:set_int("c" .. i, 100) end
        change = true
    elseif fields["up"] then
        local sl = meta:get_int("c" .. selected)
        sl = sl - 10

        if sl <= 0 then sl = 0 end

        meta:set_int("c" .. selected, sl)
        change = true
    elseif fields["down"] then
        local sl = meta:get_int("c" .. selected)
        sl = sl + 10

        if sl >= 100 then sl = 100 end

        meta:set_int("c" .. selected, sl)
        change = true
    end

    if change then minetest.get_node_timer(pos):start(0.2) end
end

local function reactor_port_timer(pos)
    local refresh = false
    local meta = minetest.get_meta(pos)
    local cool = fluid_lib.get_buffer_data(pos, "cool")
    local hot = fluid_lib.get_buffer_data(pos, "hot")

    local heat_burst = meta:get_int("burst")
    if heat_burst > 0 then
        -- Convert a bucket of cold coolant into hot coolant

        local heat_take = math.floor(cool.capacity * (heat_burst / 100))
        local coolant = heat_take
        if coolant > cool.amount then coolant = cool.amount end

        if hot.amount + coolant > hot.capacity and hot.amount < hot.capacity then
            coolant = hot.capacity - hot.amount
        end

        if coolant > 0 and hot.amount + coolant < hot.capacity then
            meta:set_int("burst", 0)

            cool.amount = cool.amount - coolant
            hot.amount = hot.amount + coolant

            refresh = true

            meta:set_string("cool_fluid", "elepower_nuclear:coolant_source")
            meta:set_string("hot_fluid", "elepower_nuclear:hot_coolant_source")

            meta:set_int("cool_fluid_storage", cool.amount)
            meta:set_int("hot_fluid_storage", hot.amount)
        end
    end

    meta:set_string("formspec", get_port_formspec(cool, hot))

    return refresh
end

-- Reactor Core
ele.register_base_device("elepower_nuclear:fission_core", {
    description = S("Fission Reactor Core"),
    groups = {
        cracky = 2,
        pickaxey = 2,
        ele_reactor_core = 1,
        ele_reactor_component = 1
    },
    tiles = {
        "elenuclear_fission_core_top.png", "elepower_lead_block.png",
        "elenuclear_fission_core_side.png", "elenuclear_fission_core_side.png",
        "elenuclear_fission_core_side.png", "elenuclear_fission_core_side.png"
    },
    on_timer = reactor_core_timer,
    on_construct = function(pos)
        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()

        inv:set_size("fuel", 9)
        meta:set_int("absorb_tick", 11)

        meta:set_string("formspec", get_core_formspec(0, 0, false))
    end,
    can_dig = can_dig,
    allow_metadata_inventory_put = allow_metadata_inventory_put,
    allow_metadata_inventory_move = allow_metadata_inventory_move,
    allow_metadata_inventory_take = ele.default.allow_metadata_inventory_take,

    on_metadata_inventory_move = ele.default.metadata_inventory_changed,
    on_metadata_inventory_put = ele.default.metadata_inventory_changed,
    on_metadata_inventory_take = ele.default.metadata_inventory_changed
})

-- Reactor Control
ele.register_base_device("elepower_nuclear:fission_controller", {
    description = S("Fission Control Module") .. "\n" ..
        S("Place me on top of a Fission Reactor Core"),
    groups = {cracky = 2, pickaxey = 2, ele_reactor_component = 1},
    tiles = {
        "elenuclear_fission_core_top.png", "elepower_lead_block.png",
        "elenuclear_fission_controller_side.png",
        "elenuclear_fission_controller_side.png",
        "elenuclear_fission_controller_side.png",
        "elenuclear_fission_controller_side.png"
    },
    on_timer = reactor_controller_timer,
    on_receive_fields = reactor_controller_manage,
    on_construct = function(pos)
        local meta = minetest.get_meta(pos)
        local inv = meta:get_inventory()

        meta:set_int("c1", 100)
        meta:set_int("c2", 100)
        meta:set_int("c3", 100)
        meta:set_int("c4", 100)

        meta:set_int("setting", 0)
        meta:set_int("selected", 1)

        meta:set_string("formspec",
                        get_controller_formspec({100, 100, 100, 100}, 1))
    end
})

-- Reactor Fluid Port
ele.register_base_device("elepower_nuclear:reactor_fluid_port", {
    description = S("Reactor Fluid Port") .. "\n" ..
        S("Place me on the bottom of a Fission Reactor Core"),
    groups = {
        cracky = 2,
        pickaxey = 2,
        ele_reactor_component = 1,
        ele_port = 1,
        fluid_container = 1
    },
    tiles = {
        "elenuclear_machine_top.png", "elepower_lead_block.png",
        "elenuclear_machine_side.png^elepower_power_port.png",
        "elenuclear_machine_side.png^elepower_power_port.png",
        "elenuclear_machine_side.png^elepower_power_port.png",
        "elenuclear_machine_side.png^elepower_power_port.png"
    },
    on_timer = reactor_port_timer,
    on_construct = function(pos)
        local meta = minetest.get_meta(pos)

        meta:set_string("cool_fluid", "elepower_nuclear:coolant_source")
        meta:set_string("hot_fluid", "elepower_nuclear:hot_coolant_source")

        meta:set_string("formspec", get_port_formspec())
    end,
    fluid_buffers = {
        cool = {
            capacity = 16000,
            accepts = {epi.water_source, "elepower_nuclear:coolant_source"},
            drainable = false
        },
        hot = {
            capacity = 16000,
            accepts = {"elepower_nuclear:hot_coolant_source"},
            drainable = true
        }
    }
})

-- Load reactor cores
minetest.register_lbm({
    label = "Refresh Reactors on load",
    name = "elepower_nuclear:fission_core",
    nodenames = {"elepower_nuclear:fission_core"},
    run_at_every_load = true,
    action = ele.helpers.start_timer
})
