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 = {}

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, clear_all)
    if clear_all then
        pl[player].craftable = {}
    end
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
    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 = 1
            -- minetest.chat_send_all("Iterating everything")
            get_inv(player)
            iterate_craftable_list(player, true)
            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 cur_uid = 1
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"
    local _index = index_of_item[item_name]
    -- if there is no list for this item, make one
    if _index == nil then
        _index = #all_craft_recipes+1
        all_craft_recipes[_index] = {}
        index_of_item[item_name] = _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.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\"")
    end
    -- add them to the list
    all_craft_recipes[_index][#all_craft_recipes[_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

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()
    -- 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 to_take = table.copy(recipe.items)
    local give_list = {ItemStack(recipe.output)}
    for i, itemstring in ipairs(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()
    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

    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)
