---- module inserter
-- @gui Module

local AABB = require("modules/exp_util/aabb")
local Gui = require("modules.exp_legacy.expcore.gui") --- @dep expcore.gui
local Event = require("modules/exp_legacy/utils/event") --- @dep utils.event
local Roles = require("modules.exp_legacy.expcore.roles") --- @dep expcore.roles
local config = require("modules.exp_legacy.config.module") --- @dep config.module
local Selection = require("modules.exp_legacy.modules.control.selection") --- @dep modules.control.selection
local SelectionModuleArea = "ModuleArea"

local module_container -- Container for this GUI

local machine_names = {}
for mod_name, machine_set in pairs(config.machine_set) do
    if script.active_mods[mod_name] then
        for machine_name, v in pairs(machine_set) do
            config.machine[machine_name] = v
            table.insert(machine_names, machine_name)
        end
    end
end

local prod_module_names = {}
for name, item in pairs(prototypes.item) do
    if item.module_effects and item.module_effects.productivity and item.module_effects.productivity > 0 then
        prod_module_names[#prod_module_names + 1] = name
    end
end

local elem_filter = {
    machine_name = { {
        filter = "name",
        name = machine_names,
    } },
    no_prod = { {
        filter = "type",
        type = "module",
    }, {
        filter = "name",
        name = prod_module_names,
        mode = "and",
        invert = true,
    } },
    with_prod = { {
        filter = "type",
        type = "module",
    } },
}

--- Apply module changes to a crafting machine
--- @param player LuaPlayer
--- @param area BoundingBox
--- @param machine_name string
--- @param planner_with_prod LuaItemStack
--- @param planner_no_prod LuaItemStack
local function apply_module_to_crafter(player, area, machine_name, planner_with_prod, planner_no_prod)
    local force = player.force
    local surface = player.surface
    local upgrade_area = surface.upgrade_area

    --- @type BoundingBox
    local param_area = { left_top = {}, right_bottom = {} }

    --- @type LuaSurface.upgrade_area_param
    local params = {
        area = param_area,
        item = planner_with_prod,
        player = player,
        force = force,
    }

    for _, entity in pairs(surface.find_entities_filtered{ area = area, name = machine_name, force = force }) do
        local pos = entity.position
        param_area.left_top = pos
        param_area.right_bottom = pos

        local m_current_recipe = entity.get_recipe()
        local r_proto = m_current_recipe and m_current_recipe.prototype

        if r_proto and (r_proto.maximum_productivity or (r_proto.allowed_effects and r_proto.allowed_effects["productivity"])) then
            params.item = planner_with_prod
            upgrade_area(params)
        else
            params.item = planner_no_prod
            upgrade_area(params)
        end
    end
end

--- when an area is selected to add protection to the area
--- @param event EventData.on_player_selected_area
Selection.on_selection(SelectionModuleArea, function(event)
    local area = AABB.expand(event.area)
    local player = game.players[event.player_index]
    local frame = Gui.get_left_element(player, module_container) --- @type LuaGuiElement
    local scroll_table = frame.container.scroll.table

    -- Create an inventory with three upgrade planners
    local inventory = game.create_inventory(3)
    inventory.insert{ name = "upgrade-planner", count = 3 }
    local planner_all = inventory[1]
    local planner_with_prod = inventory[2]
    local planner_no_prod = inventory[3]
    local mapper_index = 1

    for row = 1, config.default_module_row_count do
        local machine_name = scroll_table["module_mm_" .. row .. "_0"].elem_value --[[@as string]]
        local entity_proto = prototypes.entity[machine_name]

        if machine_name then
            local is_prod_crafter = false
            local module_index = 1
            local modules = {}
            local no_prod = {}

            -- Add all the modules selected
            for column = 1, entity_proto.module_inventory_size do
                local module_name = scroll_table["module_mm_" .. row .. "_" .. column].elem_value --[[ @as {name:string, quality:string} ]]

                if module_name then
                    local not_prod = module_name.name:gsub("productivity", "efficiency")
                    modules[module_index] = module_name
                    no_prod[module_index] = { name = not_prod, quality = module_name.quality }
                    module_index = module_index + 1
                    if not is_prod_crafter and module_name ~= not_prod and entity_proto.get_crafting_speed() then
                        is_prod_crafter = true
                    end
                else
                    modules[module_index] = {}
                    no_prod[module_index] = {}
                    module_index = module_index + 1
                end
            end

            if is_prod_crafter then
                -- Crafting machines with prod need to be handled on a case by case biases
                local i = 0
                for quality_name in pairs(prototypes.quality) do
                    i = i + 1
                    planner_with_prod.set_mapper(i, "from", {
                        type = "entity",
                        name = machine_name,
                        quality = quality_name,
                        comparator = "=",
                    })
                    planner_no_prod.set_mapper(i, "from", {
                        type = "entity",
                        name = machine_name,
                        quality = quality_name,
                        comparator = "=",
                    })
                    planner_with_prod.set_mapper(i, "to", {
                        type = "entity",
                        name = machine_name,
                        module_slots = modules,
                        quality = quality_name,
                        comparator = "=",
                    })
                    planner_no_prod.set_mapper(i, "to", {
                        type = "entity",
                        name = machine_name,
                        module_slots = no_prod,
                        quality = quality_name,
                        comparator = "=",
                    })
                end
                apply_module_to_crafter(player, area, machine_name, planner_with_prod, planner_no_prod)
            else
                -- All other machines can be applied in a single upgrade planner
                for quality_name in pairs(prototypes.quality) do
                    planner_all.set_mapper(mapper_index, "from", {
                        type = "entity",
                        name = machine_name,
                        quality = quality_name,
                        comparator = "=",
                    })
                    planner_all.set_mapper(mapper_index, "to", {
                        type = "entity",
                        name = machine_name,
                        module_slots = modules,
                        quality = quality_name,
                        comparator = "=",
                    })
                    mapper_index = mapper_index + 1
                end
            end
        end
    end

    -- Apply module changes for non crafting (or without prod selected)
    if mapper_index > 1 then
        player.surface.upgrade_area{
            area = area,
            item = planner_all,
            force = player.force,
            player = player,
        }
    end

    inventory.destroy()
end)

--- Set the state of all elem selectors on a row
--- @param player LuaPlayer
--- @param element_name string
local function row_set(player, element_name)
    local frame = Gui.get_left_element(player, module_container) --[[ @as LuaGuiElement ]]
    local scroll_table = frame.container.scroll.table
    local machine_name = scroll_table[element_name .. "0"].elem_value --[[ @as string ]]

    if machine_name then
        local active_to = prototypes.entity[machine_name].module_inventory_size
        local row_count = math.ceil(active_to / config.module_slots_per_row)
        local visible_to = row_count * config.module_slots_per_row
        for i = 1, config.module_slot_max do
            local element = scroll_table[element_name .. i]
            if i <= active_to then
                if config.machine[machine_name].prod then
                    element.elem_filters = elem_filter.with_prod
                else
                    element.elem_filters = elem_filter.no_prod
                end

                element.visible = true
                element.enabled = true
                element.elem_value = { name = config.machine[machine_name].module }
            else
                element.visible = i <= visible_to
                element.enabled = false
                element.elem_value = nil
            end
            if i % (config.module_slots_per_row + 1) == 0 then
                scroll_table[element_name .. "pad" .. i].visible = element.visible
            end
        end
    else
        for i = 1, config.module_slot_max do
            local element = scroll_table[element_name .. i]
            element.visible = i <= config.module_slots_per_row
            element.enabled = false
            element.elem_value = nil
            if i % (config.module_slots_per_row + 1) == 0 then
                scroll_table[element_name .. "pad" .. i].visible = false
            end
        end
    end
end

local button_apply =
    Gui.element{
        type = "button",
        caption = { "module.apply" },
        style = "button",
    }:on_click(function(player)
        if Selection.is_selecting(player, SelectionModuleArea) then
            Selection.stop(player)
        else
            Selection.start(player, SelectionModuleArea)
        end
    end)

module_container =
    Gui.element(function(definition, parent)
        local container = Gui.container(parent, definition.name, (config.module_slots_per_row + 2) * 36)
        Gui.header(container, "Module Inserter", "", true)

        local slots_per_row = config.module_slots_per_row + 1
        local scroll_table = Gui.scroll_table(container, (config.module_slots_per_row + 2) * 36, slots_per_row)

        for i = 1, config.default_module_row_count do
            scroll_table.add{
                name = "module_mm_" .. i .. "_0",
                type = "choose-elem-button",
                elem_type = "entity",
                elem_filters = elem_filter.machine_name,
                style = "slot_button",
            }

            for j = 1, config.module_slot_max do
                if j % slots_per_row == 0 then
                    scroll_table.add{
                        type = "flow",
                        name = "module_mm_" .. i .. "_pad" .. j,
                        visible = false,
                    }
                end
                scroll_table.add{
                    name = "module_mm_" .. i .. "_" .. j,
                    type = "choose-elem-button",
                    elem_type = "item-with-quality",
                    elem_filters = elem_filter.no_prod,
                    style = "slot_button",
                    enabled = false,
                    visible = j <= config.module_slots_per_row,
                }
            end
        end

        button_apply(container)

        return container.parent
    end)
    :static_name(Gui.unique_static_name)
    :add_to_left_flow()

Gui.left_toolbar_button("item/productivity-module-3", { "module.main-tooltip" }, module_container, function(player)
    return Roles.player_allowed(player, "gui/module")
end)

--- @param event EventData.on_gui_elem_changed
Event.add(defines.events.on_gui_elem_changed, function(event)
    if event.element.name:sub(1, 10) == "module_mm_" then
        if event.element.name:sub(-1) == "0" then
            row_set(game.players[event.player_index], "module_mm_" .. event.element.name:sub(-3):sub(1, 1) .. "_")
        end
    end
end)

--- @param event EventData.on_entity_settings_pasted
Event.add(defines.events.on_entity_settings_pasted, function(event)
    local source = event.source
    local destination = event.destination
    local player = game.players[event.player_index]

    if not player then
        return
    end

    if not source or not source.valid then
        return
    end

    if not destination or not destination.valid then
        return
    end

    -- rotate machine also
    if config.copy_paste_rotation then
        if (source.name == destination.name or source.prototype.fast_replaceable_group == destination.prototype.fast_replaceable_group) then
            if source.supports_direction and destination.supports_direction and source.type ~= "transport-belt" then
                local destination_box = destination.bounding_box

                local ltx = destination_box.left_top.x
                local lty = destination_box.left_top.y
                local rbx = destination_box.right_bottom.x
                local rby = destination_box.right_bottom.y

                local old_direction = destination.direction
                destination.direction = source.direction

                if ltx ~= destination_box.left_top.x or lty ~= destination_box.left_top.y or rbx ~= destination_box.right_bottom.x or rby ~= destination_box.right_bottom.y then
                    destination.direction = old_direction
                end
            end
        end
    end

    --[[
    TODO handle later as may need using global to reduce creation of upgrade plans

    if config.copy_paste_module then
        if source.name ~= destination.name then
            return
        end

        local source_inventory = source.get_module_inventory()

        if not source_inventory then
            return
        end

        local source_inventory_content = source_inventory.get_contents()

        if not source_inventory_content then
            return
        end

        clear_module(player, destination.bounding_box, destination.name)

        if next(source_inventory_content) ~= nil then
            apply_module(player, destination.bounding_box, destination.name, { ["n"] = source_inventory_content, ["p"] = source_inventory_content })
        end
    end
    ]]
end)
