local S = minetest.get_translator("crafting")

-- Crafting menu display modes
local MODE_CRAFTABLE = 1 -- crafting guide mode, show all recipes (default)
local MODE_GUIDE = 2 -- craftable mode, only show recipes craftable from input slots

--
-- API
--

crafting = {}

-- Callbacks

crafting.callbacks = {}

-- Array of registered craft recipes

crafting.registered_crafts = {}

-- User table of last selected row etc.

crafting.userdata = {}

-- Crafting can only take 4 itemstacks as input for sanity/interface reasons

crafting.max_inputs = 4

-- Default crafting definition values

crafting.default_craftdef = {
   output = nil,
   items = {},
   groups = {},
}

function crafting.register_craft(def)
   if def.output == nil then
      minetest.log("warning",
                   "No output for craft recipe")
      return
   end

   local itemstack = ItemStack(def.output)
   local itemkey = itemstack:to_string()

   -- Each output item may only be used once. There can't be 2 recipes with
   -- the exact same output
   assert(crafting.registered_crafts[itemkey] == nil, "Crafting recipe collision! itemkey="..itemkey.." def="..dump(def))

   if not minetest.registered_items[itemstack:get_name()] then
      minetest.log("warning",
                   "Trying to register craft " .. itemkey
                      .. " that has an unknown output item")
   end

   local craftdef = {
      output = itemstack,
      items = def.items or crafting.default_craftdef.items,
      groups = def.groups or crafting.default_craftdef.groups,
   }

   if #craftdef.items > crafting.max_inputs then
      minetest.log("warning",
                   "Attempting to register craft " .. itemkey .." with more than "
                      .. crafting.max_inputs .. " inputs, allowing")
   end

   for i = 1, crafting.max_inputs do
      if craftdef.items[i] ~= nil then
         craftdef.items[i] = ItemStack(craftdef.items[i])
      end
   end

   crafting.registered_crafts[itemkey] = craftdef

   minetest.log("info", "Registered recipe for " .. itemkey)
end

function crafting.get_crafts(player_inventory)
   local results = {}

   local function get_filtered()
      for craftname, craftdef in pairs(crafting.registered_crafts) do
         local contains_all = true
         for c=1, #craftdef.items do
             local name = craftdef.items[c]:get_name()
             if string.sub(name, 1, 6) == "group:" then
                 local group = string.sub(name, 7)
                 local gcount = craftdef.items[c]:get_count()
                 local items_in = player_inventory:get_list("craft_in")
                 if items_in == nil then
                     contains_all = false
                     break
                 end
                 local count = 0
                 for i=1, #items_in do
                     if minetest.get_item_group(items_in[i]:get_name(), group) ~= 0 then
                         count = count + items_in[i]:get_count()
                     end
                 end
                 if count < gcount then
                     contains_all = false
                     break
                 end
             elseif not player_inventory:contains_item("craft_in", craftdef.items[c]) then
                 contains_all = false
                 break
             end
         end
         if contains_all then
             table.insert(results, craftname)
         end
      end
   end

   local function get_all()
      for craftname, _ in pairs(crafting.registered_crafts) do
         table.insert(results, craftname)
      end
   end

   if player_inventory == nil then
      get_all()
   else
      get_filtered()
   end

   local function sort_function(a, b)
      local a_itemn = ItemStack(a):get_name()
      local b_itemn = ItemStack(b):get_name()

      local a_name = minetest.registered_items[a_itemn].description
      local b_name = minetest.registered_items[b_itemn].description

      return a_name < b_name
   end

   table.sort(results, sort_function)

   return results
end

function crafting.register_on_craft(func)
   if not crafting.callbacks.on_craft then
      crafting.callbacks.on_craft = {}
   end

   table.insert(crafting.callbacks.on_craft, func)
end

function crafting.craft(player, wanted, wanted_count, output, items)
   -- `output` can be any ItemStack value
   -- Duplicate items in `items` should work correctly
   if wanted:is_empty() then
      return nil
   end

   local craftdef = crafting.registered_crafts[wanted:to_string()]

   if craftdef == nil then
      minetest.log("warning",
                   "Tried to craft an unregistered item " .. wanted:to_string())

      return nil
   end

   -- Check for validity

   local craft_count = wanted_count

   for i = 1, crafting.max_inputs do
      local required_itemstack = ItemStack(craftdef.items[i])
      local itemc = 0

      local group = string.match(required_itemstack:get_name(), "group:(.*)")

      if required_itemstack ~= nil and required_itemstack:get_count() ~= 0 then
         for j = 1, crafting.max_inputs do
            local input_itemstack = ItemStack(items[j])

            if (group ~= nil
                   and minetest.get_item_group(input_itemstack:get_name(), group) ~= 0)
               or (input_itemstack ~= nil
                   and input_itemstack:get_name() == required_itemstack:get_name()) then
                  itemc = itemc + input_itemstack:get_count()
            end
         end

         craft_count = math.min(craft_count,
                                math.floor(itemc / required_itemstack:get_count()))

         if craft_count < 1 then
            return nil -- Not enough items to craft
         end
      end
   end

   -- Put stuff in output stack

   local free_space = wanted:get_stack_max() - output:get_count()
   if free_space < (craft_count * craftdef.output:get_count()) then
      craft_count = math.floor(free_space / craftdef.output:get_count())
   end

   if craft_count < 1 then
      return nil -- Can't hold any output
   end

   output:add_item(
      ItemStack({
            name = craftdef.output:get_name(),
            count = craftdef.output:get_count() * craft_count
   }))

   -- Iterate through second time to take items used for crafting

   local function remove_used_item(itemn, count)
      local items_required = count

      local group = string.match(itemn, "group:(.*)")

      for i = 1, crafting.max_inputs do
         local input_itemstack = ItemStack(items[i])

         if (group ~= nil
                and minetest.get_item_group(input_itemstack:get_name(), group) ~= 0)
            or (items[i] ~= nil
                and input_itemstack:get_name() == itemn) then
               local items_left = items_required - input_itemstack:get_count()

               input_itemstack:take_item(items_required)

               if items_left > 0 then
                  items_required = items_required - (items_required - items_left)
               else
                  items[i] = input_itemstack:to_table()
                  break
               end

               items[i] = input_itemstack:to_table()
         end
      end
   end

   for i = 1, crafting.max_inputs do
      local required_itemstack = ItemStack(craftdef.items[i])

      if craftdef.items[i] ~= nil then
         remove_used_item(required_itemstack:get_name(),
                          required_itemstack:get_count() * craft_count)
      end
   end

   for _, func in ipairs(crafting.callbacks.on_craft) do
      for i = 1, (craftdef.output:get_count() * craft_count) do
         func(output, player)
      end
   end

   return {items = items, output = output}
end

local form = default.ui.get_page("default:2part")

form = form .. "field[-1,-1;0,0;crafting_tracker;;]"

form = form .. "list[current_player;main;0.25,4.75;8,4;]"
form = form .. default.ui.get_hotbar_itemslot_bg(0.25, 4.75, 8, 1)
form = form .. default.ui.get_itemslot_bg(0.25, 5.75, 8, 3)

form = form .. "list[current_player;craft_in;0.25,0.25;1,4;]"

form = form .. "list[current_player;craft_out;7.25,3.25;1,1;]"

form = form .. "listring[current_player;main]"
form = form .. "listring[current_player;craft_in]"
form = form .. "listring[current_player;main]"
form = form .. "listring[current_player;craft_out]"

form = form .. default.ui.get_itemslot_bg(0.25, 0.25, 1, 4)
form = form .. default.ui.get_output_itemslot_bg(7.25, 3.25, 1, 1)

form = form .. default.ui.button(7.25, 1.25, 1, 1, "do_craft_1", "1", nil, S("Craft once"))
form = form .. default.ui.button(7.25, 2.25, 1, 1, "do_craft_10", "10", nil, S("Craft 10 times"))

form = form .. "tablecolumns[text,align=left,width=2;text,align=left,width=40]"

default.ui.register_page("crafting:crafting", form)

function crafting.get_formspec(name, select_item)
   local row = 1

   if crafting.userdata[name] ~= nil then
      row = crafting.userdata[name].row
   end

   local inv = minetest.get_player_by_name(name):get_inventory()

   local craft_list = ""

   local craftitems
   if crafting.userdata[name] and crafting.userdata[name].mode == MODE_GUIDE then
       craftitems = crafting.get_crafts()
   else
       craftitems = crafting.get_crafts(inv)
   end
   if select_item == nil then
       if row > #craftitems then
           row = #craftitems
           if crafting.userdata[name] ~= nil then
               crafting.userdata[name].row = row
           end
       elseif row < 1 and #craftitems >= 1 then
           row = 1
           if crafting.userdata[name] ~= nil then
               crafting.userdata[name].row = row
           end
       end
   end

   local selected_craftdef = nil

   local craft_count = 0
   for i, itemn in ipairs(craftitems) do
      local itemstack = ItemStack(itemn)
      local itemname = itemstack:get_name()
      local itemdef = minetest.registered_items[itemname]

      if select_item then
         if itemn == select_item then
            selected_craftdef = crafting.registered_crafts[itemn]
            row = i
            if crafting.userdata[name] ~= nil then
                crafting.userdata[name].row = row
            end
         end
      elseif i == row then
         selected_craftdef = crafting.registered_crafts[itemn]
      end

      if itemdef ~= nil then
        local craftdef = crafting.registered_crafts[itemn]

         if craft_list ~= "" then
            craft_list = craft_list .. ","
         end

         if itemstack:get_count() ~= 1 then
            craft_list = craft_list .. minetest.formspec_escape(itemstack:get_count())
         end

         local desc = itemdef.description
         -- Cut off item description after first newline
         local firstnewline = string.find(desc, "\n")
         if firstnewline then
             desc = string.sub(desc, 1, firstnewline-1)
         end

         craft_list = craft_list .. "," .. minetest.formspec_escape(desc)
         craft_count = craft_count + 1
      end
   end
   if select_item and (not selected_craftdef) and #craftitems > 0 then
      row = 1
      selected_craftdef = crafting.registered_crafts[craftitems[row]]
      if crafting.userdata[name] ~= nil then
          crafting.userdata[name].row = row
      end
   end

   local form = default.ui.get_page("crafting:crafting")

   if craft_count > 0 then
       form = form .. "table[2.25,0.25;4.75,3.75;craft_list;" .. craft_list
          .. ";" .. row .. "]"
   end

   if selected_craftdef ~= nil then
      if selected_craftdef.items[1] ~= nil then
         form = form .. default.ui.fake_itemstack_any(
            1.25, 0.25, selected_craftdef.items[1], "craftex_in_1")
      end
      if selected_craftdef.items[2] ~= nil then
         form = form .. default.ui.fake_itemstack_any(
            1.25, 1.25, selected_craftdef.items[2], "craftex_in_2")
      end
      if selected_craftdef.items[3] ~= nil then
         form = form .. default.ui.fake_itemstack_any(
            1.25, 2.25, selected_craftdef.items[3], "craftex_in_3")
      end
      if selected_craftdef.items[4] ~= nil then
         form = form .. default.ui.fake_itemstack_any(
            1.25, 3.25, selected_craftdef.items[4], "craftex_in_4")
      end
      if selected_craftdef.output ~= nil then
         form = form .. default.ui.fake_itemstack_any(
            7.25, 0.25, selected_craftdef.output, "craftex_out")
      end
   end

   local guide_icon, guide_tip
   if crafting.userdata[name] and crafting.userdata[name].mode == MODE_GUIDE then
      guide_icon = "ui_icon_craftingguide_active.png"
      guide_tip = S("Show only craftable recipes")
   else
      guide_icon = "ui_icon_craftingguide.png"
      guide_tip = S("Show all recipes")
   end
   form = form .. default.ui.tab(8.37, 0.5, "toggle_filter", guide_icon, guide_tip, "right")

   return form
end

local function clear_craft_slots(player)
   local inv = player:get_inventory()
   -- Move items out of input and output slots
   local items_moved = false
   local pos = player:get_pos()
   local lists = { "craft_out", "craft_in" }
   for l = 1, #lists do
      local list = lists[l]
      for i = 1, inv:get_size(list) do
          local itemstack = inv:get_stack(list, i)
          if not itemstack:is_empty() then
              if inv:room_for_item("main", itemstack) then
                  inv:add_item("main", itemstack)
              else
                  item_drop.drop_item(pos, itemstack)
              end

              itemstack:clear()
              inv:set_stack(list, i, itemstack)

              items_moved = true
          end
      end
   end

   if items_moved then
       player:set_inventory_formspec(crafting.get_formspec(player:get_player_name()))
   end
end

local function on_player_receive_fields(player, form_name, fields)
   local inv = player:get_inventory()


   if fields.quit then
      clear_craft_slots(player)
   end

   if fields.crafting_tracker == nil then
      return
   end

   local name = player:get_player_name()

   local do_craft_1, do_craft_10 = false, false
   if fields.craft_list then
      -- Double-click on list entry crafts single time
      local selection = minetest.explode_table_event(fields.craft_list)
      if selection.type == "DCL" then
          do_craft_1 = true
      end
   else
      do_craft_1 = fields.do_craft_1 ~= nil
   end
   do_craft_10 = fields.do_craft_10 ~= nil
   if do_craft_1 or do_craft_10 then
      local craftitems
      if crafting.userdata[name] and crafting.userdata[name].mode == MODE_GUIDE then
          craftitems = crafting.get_crafts()
      else
          craftitems = crafting.get_crafts(inv)
      end
      local old_item = nil
      if crafting.userdata[name] then
          old_item = craftitems[crafting.userdata[name].row]
      end

      local wanted_itemstack = ItemStack(craftitems[crafting.userdata[name].row])
      local output_itemstack = inv:get_stack("craft_out", 1)

      if output_itemstack:get_name() ~= wanted_itemstack:get_name()
      and output_itemstack:get_count() ~= 0 then
         return -- Different item type in output already
      end

      local count = 1

      if do_craft_1 then
         count = 1
      elseif do_craft_10 then
         count = 10
      else
         return
      end

      local crafted = crafting.craft(player, wanted_itemstack, count,
                                     output_itemstack, inv:get_list("craft_in"))

      if crafted then
         inv:set_stack("craft_out", 1, "")

         if inv:room_for_item("craft_out", crafted.output) then
            inv:set_stack("craft_out", 1, crafted.output)

            local new_list = {}
            for i=1, #crafted.items do
                new_list[i] = ItemStack(crafted.items[i])
            end
            inv:set_list("craft_in", new_list)

            crafting.update_crafting_formspec(player, old_item)
         end
      end
   elseif fields.craft_list then
      local selection = minetest.explode_table_event(fields.craft_list)

      if selection.type == "CHG" then
         crafting.userdata[name].row = selection.row

         minetest.show_formspec(name, "crafting:crafting",
                                crafting.get_formspec(name))
      end
   elseif fields.toggle_filter then
      local craftitems
      if crafting.userdata[name] and crafting.userdata[name].mode == MODE_GUIDE then
          craftitems = crafting.get_crafts()
      else
          craftitems = crafting.get_crafts(inv)
      end
      local old_item = craftitems[crafting.userdata[name].row]
      if crafting.userdata[name].mode == MODE_GUIDE then
          crafting.userdata[name].mode = MODE_CRAFTABLE
      else
          crafting.userdata[name].mode = MODE_GUIDE
      end

      minetest.show_formspec(name, "crafting:crafting",
                             crafting.get_formspec(name, old_item))
   end

   player:set_inventory_formspec(crafting.get_formspec(name))
end

function crafting.update_crafting_formspec(player, old_item)
   local name = player:get_player_name()
   local newform = crafting.get_formspec(name, old_item)
   minetest.show_formspec(name, "crafting:crafting", newform)
   player:set_inventory_formspec(newform)
end

minetest.register_allow_player_inventory_action(function(player, action, inventory, inventory_info)
   if action == "put" then
      if inventory_info.listname == "craft_out" then
          return 0
      end
   elseif action == "move" then
      if inventory_info.to_list == "craft_out" then
          return 0
      end
   end
end)

minetest.register_on_player_inventory_action(function(player, action, inventory, inventory_info)
   if action == "move" then
      if inventory_info.from_list == "craft_in" or inventory_info.to_list == "craft_in" then
          crafting.update_crafting_formspec(player)
      end
   elseif action == "put" or action == "take" then
      if inventory_info.listname == "craft_in" then
          crafting.update_crafting_formspec(player)
      end
   end
end)

local function on_joinplayer(player)
   local name = player:get_player_name()

   local inv = player:get_inventory()

   if crafting.userdata[name] == nil then
      crafting.userdata[name] = {row = 1, mode = MODE_CRAFTABLE}
   end

   if inv:get_size("craft_in") ~= 4 then
      inv:set_size("craft_in", 4)
   end

   if inv:get_size("craft_out") ~= 1 then
      inv:set_size("craft_out", 1)
   end

   clear_craft_slots(player)
end

local function on_leaveplayer(player)
   local name = player:get_player_name()

   crafting.userdata[name] = nil
end

if minetest.get_modpath("drop_items_on_die") ~= nil then
   drop_items_on_die.register_listname("craft_in")
   drop_items_on_die.register_listname("craft_out")
end

minetest.register_on_joinplayer(on_joinplayer)
minetest.register_on_leaveplayer(on_leaveplayer)
minetest.register_on_player_receive_fields(on_player_receive_fields)

default.log("api", "loaded")
