local STAIR_BURNTIME_RATIO = 0.75
local SLAB_BURNTIME_RATIO = 0.5
local FALLBACK_BURNTIME = 7

--
-- Partial blocks API
--

local adv_slab_tex = function(tiles, tex_prefix, force_white)
	local t1 = tiles[1]
	local t2 = tex_prefix.."_slab.png"
	if force_white then
		t2 = { name = t2, color = "white" }
	end
	return { t1, t1, t2 }
end
local adv_stair_tex = function(tiles, tex_prefix, force_white)
	local t1 = tiles[1]
	local t2 = tex_prefix.."_stair.png"
	local t3 = tex_prefix.."_slab.png"
	local t4 = tex_prefix.."_stair.png^[transformFX"
	if force_white then
		t2 = { name = t2, color = "white" }
		t3 = { name = t3, color = "white" }
		t4 = { name = t4, color = "white" }
	end
	return { t3, t1, t4, t2, t1, t3 }
end

local parse_slab_tiles = function(tiles_slab_def, tiles_fallback)
   local tiles
   if tiles_slab_def then
      -- Advanced slab tiles
      if type(tiles_slab_def) == "string" and string.sub(tiles_slab_def, 1, 2) == "a|" then
          local texpref = string.sub(tiles_slab_def, 3)
	  tiles = adv_slab_tex(tiles_fallback, texpref)
      elseif type(tiles_slab_def) == "string" and string.sub(tiles_slab_def, 1, 2) == "A|" then
          local texpref = string.sub(tiles_slab_def, 3)
	  tiles = adv_slab_tex(tiles_fallback, texpref, true)
      else
      -- Explicit slab tiles
          tiles = tiles_slab_def
      end
   else
      -- Slab tiles from base node
      tiles = tiles_fallback
   end
   return tiles
end

local parse_stair_tiles = function(tiles_stair_def, tiles_fallback)
   local tiles
   if tiles_stair_def then
      -- Advanced stair tiles
      if type(tiles_stair_def) == "string" and string.sub(tiles_stair_def, 1, 2) == "a|" then
          local texpref = string.sub(tiles_stair_def, 3)
	  tiles = adv_stair_tex(tiles_fallback, texpref)
      elseif type(tiles_stair_def) == "string" and string.sub(tiles_stair_def, 1, 2) == "A|" then
          local texpref = string.sub(tiles_stair_def, 3)
	  tiles = adv_stair_tex(tiles_fallback, texpref, true)
      elseif tiles_stair_def == "w" then
      -- World-aligned stair textures
          if tiles_fallback then
             local tex1 = tiles_fallback[1]
             if type(tex1) == "string" then
                tiles = {{ name = tex1, align_style = "world" }}
             elseif type(tex1) == "table" then
                tiles = { table.copy(tex1) }
                tiles[1].align_style = "world"
             else
                minetest.log("error", "[rp_partialblocks] Failed to generate world-aligned stair texture!")
             end
          end
      else
      -- Explicit stair tiles
          tiles = tiles_stair_def
      end
   else
      -- Stair tiles from base node
      tiles = tiles_fallback
   end
   return tiles
end

function partialblocks.register_material(name, desc_slab, desc_stair, node, groups, is_fuel, tiles_slab, tiles_stair, overlay_tiles_slab, overlay_tiles_stair, register_crafts)
   local nodedef = minetest.registered_nodes[node]
   if register_crafts == nil then
      register_crafts = true
   end

   if nodedef == nil then
      minetest.log("warning", "[rp_partialblocks] Cannot find node for partialblock: " .. node)

      return
   end

   -- Node identifiers
   local slabname = "rp_partialblocks:slab_" .. name
   local slabupname = "rp_partialblocks:slab_up_" .. name
   local stairname = "rp_partialblocks:stair_" .. name
   local stairupname = "rp_partialblocks:stair_up_" .. name

   -- Slab
   local tiles = parse_slab_tiles(tiles_slab, nodedef.tiles)
   local overlay_tiles = parse_slab_tiles(overlay_tiles_slab, nodedef.overlay_tiles)

   local groups_slab
   if not groups then
      groups_slab = table.copy(nodedef.groups)
      if groups_slab.level then
         groups_slab.level = math.max(-32737, groups_slab.level - 1)
      else
         groups_slab.level = -1
      end
   else
      groups_slab = table.copy(groups)
      if not groups_slab.level then
         groups_slab.level = -1
      end
   end
   groups_slab.slab = 1

   local paramtype2_slab = "none"
   local paramtype2_stair = "4dir"
   local palette_slab, palette_stair
   local drop_slab, drop_stair
   if nodedef.groups and nodedef.groups.paintable then
      if nodedef.groups.paintable == 1 then
         paramtype2_slab = "color"
         paramtype2_stair = "color4dir"
         -- Base the slab/stair palettes on base node

         -- dark palette
         if nodedef.palette == "rp_paint_palette_256l.png" then
            palette_slab = "rp_paint_palette_256l.png"
            palette_stair = "rp_paint_palette_64l.png"
         -- desaturated palette
         elseif nodedef.palette == "rp_paint_palette_256d.png" then
            palette_slab = "rp_paint_palette_256d.png"
            palette_stair = "rp_paint_palette_64d.png"
         -- default palette
         else
            palette_slab = "rp_paint_palette_256.png"
            palette_stair = "rp_paint_palette_64.png"
         end
      end
      drop_slab = slabname
      drop_stair = stairname
      if string.sub(name, -8, -1) == "_painted" then
         drop_slab = string.sub(drop_slab, 1, -9)
         drop_stair = string.sub(drop_stair, 1, -9)
      end
   else
      drop_slab = slabname
      drop_stair = stairname
   end

   local slabdef =
      {
	 tiles = tiles,
         overlay_tiles = overlay_tiles,
	 groups = groups_slab,
	 sounds = nodedef.sounds,

	 description = desc_slab,
	 drawtype = "nodebox",

	 node_box = {
	    type = "fixed",
	    fixed = {-0.5, -0.5, -0.5, 0.5, 0, 0.5},
	 },

	 paramtype = "light",
	 paramtype2 = paramtype2_slab,
	 use_texture_alpha = nodedef.use_texture_alpha,
	 palette = palette_slab,
	 is_ground_content = nodedef.is_ground_content,
	 drop = drop_slab,
         node_placement_prediction = "",

         -- Slab placement is rather complex and handles many situations,
         -- optimized for convenience and intuitivity.
         -- We determine if we place a normal slab which occupies the bottom side
         -- of the node, or an "up" slab, which occupies the upper side.
         -- We also allow combining slabs into full nodes when placing
         -- into or on a slab of the same type and color.
         -- Holding down the Sneak key while placing a slab ON a slab disables
         -- slab combination and places the slab node normally. Placing a slab
         -- INTO a slab will always attempt to combine them (even with Sneak key).
         on_place = function(itemstack, placer, pointed_thing)
            -- Boilerplace to handle pointed node's rightclick handler
            if not placer or not placer:is_player() then
               return itemstack
            end
            if pointed_thing.type ~= "node" then
               return minetest.item_place_node(itemstack, placer, pointed_thing)
            end
            local bp_node = minetest.get_node(pointed_thing.under)
            local bp_def = minetest.registered_nodes[bp_node.name]
            if bp_def and bp_def.on_rightclick and
                  ((not placer) or (placer and not placer:get_player_control().sneak)) then
               return bp_def.on_rightclick(pointed_thing.under, bp_node, placer, itemstack,
                     pointed_thing) or itemstack
            end

            -- Slab handling stars here
            local function place_slab_up(itemstack, placer, pointed_thing)
               local itemstack_up = ItemStack(itemstack)
               itemstack_up:set_name(slabupname)
               itemstack_up:set_count(1)
               local position
               itemstack_up, position = minetest.item_place(itemstack_up, placer, pointed_thing)
               if position and not minetest.is_creative_enabled(placer:get_player_name()) then
                  itemstack:take_item()
               end
               return itemstack, position
            end

            local old_itemstack = ItemStack(itemstack)
            local olddef = old_itemstack:get_definition()

            local check_paint_match = function(cnode, itemstack)
               local paint_match = true
               if minetest.get_item_group(cnode.name, "paintable") == 1 then
                  local node_paint_index = cnode.param2
                  local imeta = itemstack:get_meta()
                  local item_paint_index = imeta:get_int("palette_index") or 0
                  paint_match = node_paint_index == item_paint_index
               end
               return paint_match
            end

            -- Place at wall: Place "up" slab up pointed at upper half of node side
            if (pointed_thing.above.y == pointed_thing.under.y) then
               local precise = minetest.pointed_thing_to_face_pos(placer, pointed_thing)
               local h = precise.y % 1
               local position
               if h <= 0.5 then
                  -- Place "up" slab
                  itemstack, position = place_slab_up(itemstack, placer, pointed_thing)
               else
                  -- Normal placement
                  itemstack, position = minetest.item_place(itemstack, placer, pointed_thing)
               end
               if position then
                  if olddef and olddef.sounds and olddef.sounds.place then
                     minetest.sound_play(olddef.sounds.place, {pos=position}, false)
                  end
                  return itemstack
               end
            end

            -- Place at floor or ceiling

            local place_dir
            if pointed_thing.above.y > pointed_thing.under.y then
               -- place down
               place_dir = -1
            elseif pointed_thing.above.y < pointed_thing.under.y then
               -- place up
               place_dir = 1
            else
               -- place sideways
               place_dir = 0
            end

            -- Slab on slab placement creates full block
            local pos = pointed_thing.under
            -- If stacking paintable stacks, check for matching color
            local old_node = minetest.get_node(pos)

            local shift = false
            if placer:is_player() then
               -- Place node normally when sneak is pressed
               shift = placer:get_player_control().sneak
            end

            local paint_match = check_paint_match(old_node, itemstack)

            -- Create full block if placing placing slab ON slab if
            -- both slabs are compatible and player is not sneaking
            if (not shift) and ((place_dir == -1 and old_node.name == slabname) or
                  (place_dir == 1 and old_node.name == slabupname)) and
                  itemstack:get_count() >= 1 and paint_match then
               minetest.swap_node(pos, {name = node, param2 = old_node.param2})
               if olddef and olddef.sounds and olddef.sounds.place then
                  minetest.sound_play(olddef.sounds.place, {pos=pos}, false)
               end
	       if not minetest.is_creative_enabled(placer:get_player_name()) then
                   itemstack:take_item()
               end
               return itemstack
            else
               -- Create full block if placing slab INTO slab, if compatible
               local anode = minetest.get_node(pointed_thing.above)
               if ((anode.name == slabname) or (anode.name == slabupname)) and
                     (core.get_item_group(old_node.name, "slab") == 0) and
                     old_itemstack:get_count() >= 1 and
                     check_paint_match(anode, old_itemstack) then
                  minetest.swap_node(pointed_thing.above, {name = node, param2 = anode.param2})
                  if olddef and olddef.sounds and olddef.sounds.place then
                     minetest.sound_play(olddef.sounds.place, {pos=pointed_thing.above}, false)
                  end
                  if not minetest.is_creative_enabled(placer:get_player_name()) then
                      itemstack:take_item()
                  end
                  return itemstack
               end

               -- Emulate default placement (kinda)
               local position
               if place_dir == -1 then
                  itemstack, position = minetest.item_place(itemstack, placer, pointed_thing)
               else
                  itemstack, position = place_slab_up(itemstack, placer, pointed_thing)
               end
               if position then
                  if olddef and olddef.sounds and olddef.sounds.place then
                     minetest.sound_play(olddef.sounds.place, {pos=position}, false)
                  end
               else
                  rp_sounds.play_place_failed_sound(placer)
               end
            end
            return itemstack
         end,

         -- for rp_explosions
         _rp_blast_resistance = nodedef._rp_blast_resistance,
   }

   minetest.register_node(slabname, slabdef)

   local slabdef_up = table.copy(slabdef)
   if slabdef_up.groups then
      slabdef_up.groups.not_in_creative_inventory = 1
   end
   slabdef_up.description = nil
   slabdef_up.on_place = nil
   slabdef_up._tt_help = nil
   slabdef_up.node_box = {
      type = "fixed",
      fixed = {-0.5, 0, -0.5, 0.5, 0.5, 0.5},
   }
   slabdef_up.collision_box = nil
   slabdef_up.selection_box = nil
   slabdef_up._rp_canonical_item = slabname
   minetest.register_node(slabupname, slabdef_up)

   if register_crafts then
      crafting.register_craft({ -- 1 block --> 2 slabs
	 output = slabname .. " 2",
	 items = {
	    node,
	 },
      })

      crafting.register_craft({ -- 2 slabs --> 1 block
	 output = node,
	 items = {
	    slabname .. " 2",
	 },
      })
   end

   local full_node_burntime
   local output = minetest.get_craft_result({
      method = "fuel",
      width = 1,
      items = {node},
   })
   full_node_burntime = output.time

   if is_fuel then
      local burntime
      if full_node_burntime > 0 then
         -- Burntime is based on the origin node (if a fuel recipe was available)
         burntime = math.max(1, math.floor(full_node_burntime * SLAB_BURNTIME_RATIO))
      else
         -- Fallback burntime
         burntime = FALLBACK_BURNTIME
      end
      minetest.register_craft({ -- Fuel
         type = "fuel",
         recipe = slabname,
         burntime = burntime,
      })
   end

   -- Stair

   tiles = parse_stair_tiles(tiles_stair, nodedef.tiles)
   overlay_tiles = parse_stair_tiles(overlay_tiles_stair, nodedef.overlay_tiles)

   local groups_stair
   if not groups then
      groups_stair = table.copy(nodedef.groups)
      if groups_stair.level then
         groups_stair.level = math.max(-32737, groups_stair.level - 1)
      else
         groups_stair.level = -1
      end
   else
      groups_stair = table.copy(groups)
      if not groups_stair.level then
         groups_stair.level = -1
      end
   end
   groups_stair.stair = 1

   local stairdef =
      {
	 tiles = tiles,
         overlay_tiles = overlay_tiles,
	 groups = groups_stair,
	 sounds = nodedef.sounds,

	 description = desc_stair,
	 drawtype = "nodebox",

	 node_box = {
	    type = "fixed",
	    fixed = {
	       {-0.5, -0.5, -0.5, 0.5, 0, 0.5},
	       {-0.5, 0, 0, 0.5, 0.5, 0.5},
	    },
	 },

	 paramtype = "light",
	 paramtype2 = paramtype2_stair,
	 palette = palette_stair,
	 use_texture_alpha = nodedef.use_texture_alpha,
	 is_ground_content = nodedef.is_ground_content,
	 drop = drop_stair,

         on_place = function(itemstack, placer, pointed_thing)
            local function place_stair_up(itemstack, placer, pointed_thing)
               local itemstack_up = ItemStack(itemstack)
               itemstack_up:set_name(stairupname)
               itemstack_up:set_count(1)
               itemstack_up = minetest.item_place(itemstack_up, placer, pointed_thing)
               if itemstack_up:is_empty() and not minetest.is_creative_enabled(placer:get_player_name()) then
                  itemstack:take_item()
               end
               return itemstack
            end

            -- Place at wall: Place "up" stair up if placer pointed at upper half of node side
            if (pointed_thing.above.y == pointed_thing.under.y) then
               local precise = minetest.pointed_thing_to_face_pos(placer, pointed_thing)
               local h = precise.y % 1
               if h <= 0.5 then
                  -- Place "up" stair
                  itemstack = place_stair_up(itemstack, placer, pointed_thing)
                  return itemstack
               else
                  -- Normal placement
                  itemstack = minetest.item_place(itemstack, placer, pointed_thing)
                  return itemstack
               end
            elseif (pointed_thing.above.y < pointed_thing.under.y) then
               -- Place at ceiling: Place "up" stair
               itemstack = place_stair_up(itemstack, placer, pointed_thing)
               return itemstack
            else
               -- Place at floor: Normal placement
               itemstack = minetest.item_place(itemstack, placer, pointed_thing)
               return itemstack
            end
         end,

         -- for rp_explosions
         _rp_blast_resistance = nodedef._rp_blast_resistance,
   }

   minetest.register_node(stairname, stairdef)

   local stairdef_up = table.copy(stairdef)
   if stairdef_up.groups then
      stairdef_up.groups.not_in_creative_inventory = 1
   end
   stairdef_up.description = nil
   stairdef_up.on_place = nil
   stairdef_up._tt_help = nil
   stairdef_up.node_box = {
      type = "fixed",
      fixed = {
         {-0.5, 0, -0.5, 0.5, 0.5, 0.5},
         {-0.5, -0.5, 0, 0.5, 0, 0.5},
      },
   }
   local stiles = table.copy(stairdef.tiles)
   local fill_tiles = function(tiles)
      local last_tile
      for i=1,6 do
         if not tiles[i] then
            if i > 1 and not last_tile then
               last_tile = tiles[i-1]
            end
            tiles[i] = last_tile or ""
         end
      end
      return tiles
   end
   stiles = fill_tiles(stiles)
   local mirror = function(tile)
      if type(tile) == "string" then
         return "("..tile..")^[transformFY"
      elseif type(tile) == "table" and tile.align_style ~= "world" then
         tile.name = "("..tile.name..")^[transformFY"
         return tile
      else
         return tile
      end
   end
   stairdef_up.tiles = {
      stiles[2],
      stiles[1],
      mirror(stiles[3]),
      mirror(stiles[4]),
      stiles[5],
      stiles[6],
   }
   if stairdef.overlay_tiles then
      local sotiles = table.copy(stairdef.overlay_tiles)
      sotiles = fill_tiles(sotiles)
      stairdef_up.overlay_tiles = {
         sotiles[2],
         sotiles[1],
         mirror(sotiles[3]),
         mirror(sotiles[4]),
         sotiles[5],
         sotiles[6],
      }
   end
   stairdef_up.collision_box = nil
   stairdef_up.selection_box = nil
   stairdef_up.drop = drop_stair
   stairdef_up._rp_canonical_item = stairname
   minetest.register_node(stairupname, stairdef_up)


   if register_crafts then
      crafting.register_craft({ -- 3 blocks --> 4 stairs
         output = stairname .. " 4",
         items = {
            node .. " 3",
         },
      })

      crafting.register_craft({ -- 2 stairs --> 3 slabs
         output = slabname .. " 3",
         items = {
            stairname .. " 2",
         },
      })
   end
   if is_fuel then
      local burntime
      if full_node_burntime > 0 then
         -- Burntime is based on the origin node (if a fuel recipe was available)
         burntime = math.max(1, math.floor(full_node_burntime * STAIR_BURNTIME_RATIO))
      else
         -- Fallback burntime
         burntime = FALLBACK_BURNTIME
      end
      minetest.register_craft({ -- Fuel
         type = "fuel",
         recipe = stairname,
         burntime = burntime,
      })
   end
end

