local mod_name = minetest.get_current_modname()


pmb_tcraft = {}


local pl = {}

pmb_tcraft.checks_per_step = 100

local all_crafts_size = 0
local all_craft_recipes = {}
local recipe_index = {}
local index_of_item = {} -- index_of_item[name] --> index to put into all_craft_recipes[index] to get recipes for that item
local all_recipes_by_method = {}

function pmb_tcraft.get_all_recipes_for_item(name)
    if not index_of_item[name] then return nil end
    return (all_craft_recipes[index_of_item[name]])
end

function pmb_tcraft.get_all_craft_recipes()
    return all_craft_recipes
end

local function get_inv(player)
    local inv = player:get_inventory():get_list("main") or {}
    pl[player].item_quantity = {}
    for i, itemstack in pairs(inv) do
        local name = itemstack:get_name()
        pl[player].item_quantity[name] = (pl[player].item_quantity[name] or 0) + itemstack:get_count()
    end
    inv = nil
end

local function check_player(player, force)
    if force or not pl[player] then
        pl[player] = {
            craftable = {},
            item_quantity = {},
            cur_item_i = 1,
            cur_able_i = 1,
            timer = 1,
            changes_made = true,
        }
    end
end

-- check things you think you can craft to make sure you still can craft them
local function player_has_items(player, items)
    if minetest.is_creative_enabled(player:get_player_name()) then return true end
    local has = pl[player].item_quantity
    for name, count in pairs(items) do
        if (not has[name]) or has[name] < count then
            return false
        end
    end
    return true
end
-- allow this to be called externally
pmb_tcraft.player_has_items = player_has_items

-- check things you think you can craft to make sure you still can craft them
local function iterate_craftable_list(player, iterations, clear_all)
    local pi = pl[player]
    if clear_all then
        pi.craftable = {}
    end
    -- NOT IMPLEMENTED YET, CAUSES OVERFLOW
    pi.craftable_iter = (pi.craftable_iter or #pi.craftable)
    if pi.craftable_iter > #pi.craftable then
        pi.craftable_iter = #pi.craftable
    end
    for i = 0, iterations-1 do
        if pi.craftable_iter - i < 1 then
            pi.craftable_iter = #pi.craftable
            return
        end
        local recipe = pi.craftable[pi.craftable_iter - i]
        if not player_has_items(player, recipe.items) then
            table.remove(pi.craftable, pi.craftable_iter - i)
        end
    end
    pi.craftable_iter = pi.craftable_iter + iterations + 1
end

-- check new things from the list of recipes to see if there are any new items you can craft
local function iterate_all_registered_crafts(player, force_all)
    if minetest.get_modpath("pmb_gamemodes") and not pmb_gamemodes.player_has_tag(player, "crafting") then
        pl[player].craftable = {}
        return
    end
    if force_all then
        pl[player].craftable = {}
    end
    local start_i = (force_all and 1) or pl[player].cur_item_i
    local end_i = (force_all and all_crafts_size) or start_i + pmb_tcraft.checks_per_step
    pl[player].cur_item_i = end_i

    local craftable = pl[player].craftable

    for i = start_i, end_i do
        local recipe_list = all_craft_recipes[i]
        for k, recipe in pairs(recipe_list) do
            local this_craftable = player_has_items(player, recipe.items)
            if this_craftable then
                craftable[#craftable+1] = recipe
            end
        end
    end
end

-- every step
local function on_step(dtime)
    for _, player in ipairs(minetest.get_connected_players()) do
        check_player(player)
        pl[player].timer = (pl[player].timer or 1) - dtime
        if pl[player].timer <= 0 and (pl[player].changes_made or pl[player].timer < -5) then
            pl[player].timer = pl[player].timer - (math.random() * 0.1 + 1)
            -- minetest.chat_send_all("Iterating everything")
            get_inv(player)
            iterate_craftable_list(player, 100)
            iterate_all_registered_crafts(player, true)
            pl[player].changes_made = false
        end
    end
end

function pmb_tcraft.delay_recalc(player, amount)
    pl[player].timer = amount or 1
end

minetest.register_globalstep(on_step)

-- for a recipe in minetest's format, return the list of items and their quantity needed to craft it
local function get_ingedients_from_recipe(recipe)
    local list = {}
    for i, itemstring in pairs(recipe.items or {}) do
        if string.find(itemstring, "group:") then return nil end
        list[itemstring] = (list[itemstring] or 0) + 1
    end
    return list
end

--[[
all_craft_recipes[7] = {
    { -- way to craft this
        output = "pmb_tools:iron_pickaxe",
        items = {
            ["pmb_items:iron_bar"] = 3,
            ["pmb_items:stick"] = 2
        }
    },
    { -- another way to craft this
        --blah
    }
}

all_craft_recipes
    index
        ways to craft this item
            output = output from crafting
            items = items needed for crafting

all_craft_recipes[i][1].output
all_craft_recipes[i][1].items
]]

local function sort_crafts()
    local copy = table.copy(all_craft_recipes)
    table.sort(copy, function(a, b)
        return a[1].output < b[1].output
    end)

    for i, rec in ipairs(copy) do
        local item_name = string.split(rec[1].output, " ", true)[1] or "nil"
        index_of_item[item_name] = i
    end
    all_craft_recipes = copy
end

local craft_process_callbacks = {}
function pmb_tcraft.register_craft_process(callback)
    craft_process_callbacks[#craft_process_callbacks+1] = callback
end

local function do_all_craft_processes(def, item_name)
    for i, callback in ipairs(craft_process_callbacks) do
        callback(def, item_name)
    end
end

pmb_tcraft.register_craft_process(function (def, item_name)

    if minetest.get_item_group(item_name, "mechanisms") > 0 then
        if table.indexof(def.tags, "mechanisms") <= 0 then
            table.insert(def.tags, "mechanisms")
        end
        return
    end
    if minetest.get_item_group(item_name, "decoration") > 0 then
        if table.indexof(def.tags, "decor") <= 0 then
            table.insert(def.tags, "decor")
        end
        return
    end
    if minetest.get_item_group(item_name, "furniture") > 0 then
        if table.indexof(def.tags, "furniture") <= 0 then
            table.insert(def.tags, "furniture")
        end
        return
    end
    if minetest.get_item_group(item_name, "food") > 0 then
        if table.indexof(def.tags, "food") <= 0 then
            table.insert(def.tags, "food")
        end
        return
    end

    local tdef = minetest.registered_tools[item_name]
    if tdef or (minetest.get_item_group(item_name, "tool") > 0) then
        if table.indexof(def.tags, "tools") <= 0 then
            table.insert(def.tags, "tools")
        end
        return
    end

    local idef = minetest.registered_items[item_name]
    if idef and (idef.type ~= "node")
    and (table.indexof(def.tags, "goods") <= 0) then
        table.insert(def.tags, "goods")
    end

    local ndef = minetest.registered_nodes[item_name]
    if ndef and minetest.get_item_group(item_name, "full_solid") > 0 then
        if table.indexof(def.tags, "blocks") <= 0 then
            table.insert(def.tags, "blocks")
        end
        return
    elseif minetest.get_item_group(item_name, "shape") > 0 then
        if table.indexof(def.tags, "shapes") <= 0 then
            table.insert(def.tags, "shapes")
        end
        return
    end
end)

-- iterates when recipes are registered, so that it can be looked up and iterated fast later
local cur_uid = 1
---@param _def table
--[[
Example

    pmb_tcraft.register_craft({
        output = "pmb_stone:cobble_moss_2 10",
        extra_items = {"pmb_items:wooden_cup"},
        items = {
            ["pmb_stone:cobble_moss_1"] = 10,
        },
    })
]]
function pmb_tcraft.register_craft(_def)
    cur_uid = cur_uid + 1
    local def = table.copy(_def)
    local item_name = string.split(def.output, " ", true)[1] or "nil"
    -- _item_index tells you where in all_craft_recipes
    local _item_index = index_of_item[item_name]
    -- if there is no list for this item, make one
    if _item_index == nil then
        _item_index = #all_craft_recipes+1
        all_craft_recipes[_item_index] = {}
        index_of_item[item_name] = _item_index
        all_crafts_size = all_crafts_size + 1
    end
    -- set the uid so you can find it later
    def.uid = cur_uid

    if not def.tags then def.tags = {} end
    do_all_craft_processes(def, item_name)
    if #def.tags == 0 then def.tags = {"misc"} end

    if not def.method then
        def.method = {"normal"}
    elseif type(def.method) ~= "table" then
        minetest.log("warning", def.output .. " def.method is not a table, ignoring and setting to \"normal\"")
        def.method = {"normal"}
    end

    for i, methodname in ipairs(def.method) do
        if not all_recipes_by_method[methodname] then
            all_recipes_by_method[methodname] = {}
        end
        table.insert(all_recipes_by_method[methodname], def)
    end

    -- add them to the list of recipes per item index
    all_craft_recipes[_item_index][#all_craft_recipes[_item_index]+1] = def
    recipe_index[def.uid] = def
end

local _group_crafts = {}
local _tracked_groups = {}
function pmb_tcraft.register_group_craft(_def)
    if _group_crafts == nil then
        minetest.log("error", "You may not register tcraft recipes after mods are loaded! Dump:\n"..dump(_def))
    end
    _group_crafts[#_group_crafts+1] = _def
end

local function do_all_group_crafts()
    local group_items = {}
    -- group_items is a list of [name] = {all items in this group}
    for l, def in pairs(_group_crafts) do
        _tracked_groups[def.group] = {}
    end
    for name, idef in pairs(minetest.registered_items) do
        for group, val in pairs(idef.groups or {}) do
            if _tracked_groups[group] then
                _tracked_groups[group][#_tracked_groups[group]+1] = name
            end
        end
    end

    -- finally, we can actually do the recipes
    for l, def in pairs(_group_crafts) do
        for i, item_alt in ipairs(_tracked_groups[def.group] or {}) do
            local items = table.copy(def.items)
            items[item_alt] = def.group_count
            pmb_tcraft.register_craft({
                output = def.output,
                items = items,
            })
        end
    end
    _group_crafts = nil
    _tracked_groups = nil
end

local function is_item_craftable(itemname)
    for i, recipe in pairs(recipe_index) do
        if ItemStack(recipe.output):get_name() == itemname then
            return true
        end
    end
    return false
end

minetest.register_node("pmb_tcraft:unobtainium", {
    description = "unobtainium",
    groups = { not_in_creative_inventory = 1, blast_resistance = -1 },
    paramtype = 'light',
    drawtype = "glasslike",
    tiles = {"blank.png"},
    sunlight_propagates = true,
    floodable = false,
    pointable = false,
    walkable = true,
    buildable_to = false,
    diggable = false,
    _on_node_update = function(pos, cause, user, count, payload)
        minetest.set_node(pos, {name="air"})
    end,
    drop = "",
})

local function add_crafts_for_noncraftable_nodes()
    for itemname, itemdef in pairs(minetest.registered_items) do
        if (minetest.get_item_group(itemname, "not_in_creative_inventory") == 0)
        and (not is_item_craftable(itemname)) then
            pmb_tcraft.register_craft({
                output = itemname .. " 10",
                items = {
                    ["unobtanable"] = 1
                }
            })
        end
    end
end

minetest.register_on_mods_loaded(function()
    local _start = os.clock()

    for item_name, def in pairs(minetest.registered_items) do

        local all_recipes_for_this_item = minetest.get_all_craft_recipes(item_name)
        -- if item_name == "pmb_items:stick" then minetest.log(dump(all_recipes_for_this_item)) end

        if all_recipes_for_this_item then
            for k, recipe in pairs(all_recipes_for_this_item) do
                if recipe.method == "normal" then
                    local ingreds = get_ingedients_from_recipe(recipe)
                    if ingreds then
                        pmb_tcraft.register_craft({
                            output = recipe.output,
                            items = ingreds,
                            method = recipe._pmb_tcraft_method,
                            builtin = true,
                        })
                    end
                end
            end
        end
    end
    -- minetest.log("warning", "[pmb_tcraft] getting all crafting recipes took: " .. tostring(os.clock() - _start))
    -- minetest.log(dump(all_craft_recipes))


    do_all_group_crafts()
    add_crafts_for_noncraftable_nodes()
    -- sort_crafts()
end)

function pmb_tcraft.get_player_craftable(player)
    check_player(player)
    return pl[player]
end

function pmb_tcraft.get_recipe_from_index(index)
    return recipe_index[index]
end

function pmb_tcraft.try_to_craft_uid(player, uid, count)
    local recipe = recipe_index[uid]
    if not recipe then minetest.log("no recipe "..tostring(uid))
        return false end
    local is_creative_enabled = minetest.is_creative_enabled(player:get_player_name())
    local to_take = table.copy(recipe.items)
    local give_list = {ItemStack(recipe.output)}
    for i, itemstring in ipairs((not is_creative_enabled) and recipe.extra_items or {}) do
        give_list[#give_list+1] = ItemStack(itemstring)
    end
    -- allow for a custom number of crafts
    if count and count > 1 then
        for i,v in pairs(to_take) do to_take[i] = v * count end
        for i,v in pairs(give_list) do
            v:set_count(v:get_count() * count)
            give_list[i] = v
        end
    end

    if not player_has_items(player, to_take) then return false end
    -- hardcore make certain it's not created but at cost of cpu:
    get_inv(player)
    if not player_has_items(player, to_take) then return false end

    -- we can be sure the player can craft this now, so we just need to subtract the items and add the crafted ones
    local inv = player:get_inventory()
    if not minetest.is_creative_enabled(player:get_player_name()) then
        for i = 0, inv:get_size("main") do
            local stack = inv:get_stack("main", i)
            local name = stack:get_name()
            if to_take[name] and to_take[name] > 0 then
                local tmp_count = to_take[name]
                to_take[name] = to_take[name] - stack:get_count()
                stack:take_item(tmp_count)
                inv:set_stack("main", i, stack)
            elseif to_take[name] then
                to_take[name] = nil
            end
        end
    end

    for i, stack in pairs(give_list) do
        stack = inv:add_item("main", stack)
        if stack:get_count() > 0 then
            minetest.add_item(player:get_pos(), stack)
        end
        pl[player].changes_made = true
    end
    return true
end

-- pmb_tcraft.register_craft({
--     output = "pmb_items:stick 4",
--     items = {
--         ["pmb_wood:oak_planks"] = 2,
--     }
-- })

-- use SPARINGLY
-- can cause HUGE amounts of registrations
-- limited to 20, because NO.
pmb_tcraft.register_group_craft({
    output = "pmb_items:stick 4",
    items = {},
    group = "planks",
    group_count = 2,
})

minetest.register_on_player_inventory_action(function(player, action, inventory, inventory_info)
    if action == "move" and inventory_info.from_list == inventory_info.to_list then return end
    check_player(player)
    pl[player].changes_made = true
end)
minetest.register_on_item_pickup(function(itemstack, picker, pointed_thing, time_from_last_punch,  ...)
    if not minetest.is_player(picker) then return end
    check_player(picker)
    pl[picker].changes_made = true
end)
minetest.register_on_craft(function(itemstack, player, old_craft_grid, craft_inv)
    check_player(player)
    pl[player].changes_made = true
end)
