local mod = {}
local log = voxelgarden.get_logger("subnodes")

-- All registered subnode kinds, indexed by name ("modname:subname")
mod.registered_kinds = {}
-- All registered subnodes, indexed by name ("modname:kind_nodename")
mod.registered_subnodes = {}
-- All registered subnode definitions, indexed by node name ("modname:nodename")
mod.subnode_definitions = {}

mod.NODEDEF_WHITELIST = {
	"visual_scale",
	"move_resistance",
	"damage_per_second",
	"light_source",
	"sunlight_propagates",
	"use_texture_alpha",
	"walkable",
	"pointable",
	"diggable",
	"climbable",
	"buildable_to",
	"floodable",
	"sounds",
}

mod.GROUPS_WHITELIST = {
	"crumbly", "cracky", "snappy", "choppy", "fleshy", "explody",
	"oddly_breakable_by_hand", "dig_immediate", "bouncy", "disable_jump",
	"disable_descend", "fall_damage_add_percent", "falling_node", "float",
	"slippery", "flammable", "not_in_creative_inventory",
}

function mod.is_suitable(node_def)
	return (
		not node_def._subnodes_ignore and
		node_def.drawtype == "normal" and
		node_def.paramtype == "none" and
		node_def.paramtype2 == "none" and
		not node_def.on_rightclick
	)
end

-- "technical_namey_name" -> "Technical Namey Name"
local function make_readable(str)
	local words = str:split("_")
	for _, word in ipairs(words) do
		word:gsub("^%l", string.upper)
	end
	return table.concat(words, " ")
end
mod.make_readable = make_readable

-- "nodes:stone", "subnodes:block" -> "stone", "nodes:block_stone", "subnodes:block_stone"
function mod.make_name_subname(node_name, kind_name)
	local node_modname, node_subname = unpack(node_name:split(":"))
	local kind_modname, kind_subname = unpack(kind_name:split(":"))
	local subnode_name = table.concat{node_modname, ":", kind_subname, "_", node_subname}
	local subnode_name_alt = table.concat{kind_modname, ":", kind_subname, "_", node_subname} -- for aliases/LBMs

	return node_subname, subnode_name, subnode_name_alt
end

-- Create generator for `drop`
function mod.create_drop_kind(kind_name) return function(_, node_name)
	local _, subnode_name = subnodes.make_name_subname(node_name, kind_name)
	return subnode_name
end end

local function default_get_tiles(tiles, align_style)
	local new_tiles = {}
	for i, tile in ipairs(tiles) do
		new_tiles[i] = type(tile) == "string" and {name = tile} or table.copy(tile)
		if new_tiles[i].backface_culling == nil then
			new_tiles[i].backface_culling = true
		end
		new_tiles[i].align_style = align_style
	end
	return new_tiles
end

function mod.register_kind(name, def)
	log:assert(name and type(name) == "string", "Invalid name passed to register_kind")
	log:assert(
		def and type(def) == "table" and next(def) ~= nil,
		"Invalid definition table passed to register_kind"
	)

	do -- Validate `name` syntax
		local split = name:split(":")
		assert(split and split[1] and split[2],
			"Name passed to register_kind must be in the format of \"modname:subname\"")
	end

	-- Fill in defaults
	def.get_tiles = def.get_tiles or default_get_tiles

	mod.registered_kinds[name] = def
end

local function register_subnode(node_name, node_def, kind_name, kind_def, subnode_override)
	local node_subname, subnode_name = mod.make_name_subname(node_name, kind_name)
	local kind_subname = mod.make_name_subname(kind_name, node_name) -- 2nd arg doesn't matter in this case

	-- Subnodes should not be ground content
	local subnode_def = {is_ground_content = false}

	-- Save Subnodes metadata in the definition
	subnode_def._subnodes_node_name = node_name
	subnode_def._subnodes_kind_name = kind_name

	-- `description` fallback
	if not subnode_override or not subnode_override.description then
		subnode_def.description = kind_def.get_readable_name and kind_def.get_readable_name(
			node_def.description or make_readable(node_subname)
		) or make_readable(subnode_name)
	end

	-- `tiles` fallback
	if not subnode_override or not subnode_override.tiles then
		subnode_def.tiles = kind_def.get_tiles(
			node_def.tiles or {"no_texture.png"},
			subnode_override and subnode_override._subnodes_align_style or
				node_def._subnodes_align_style or "world"
		)
	end

	-- `drop` fallback
	if not subnode_override or not subnode_override.drop then
		subnode_def.drop = (kind_def.drop and (
			type(kind_def.drop) == "string" and kind_def.drop or kind_def.drop(subnode_name, node_name)
		)) or nil
	end

	-- Make sure `groups` can allow for "group:subnode" and "group:<kind>" usage
	subnode_def.groups = {subnode = 1, [kind_subname] = 1, [kind_name] = 1}
	-- "group:modname:kind" is more preferable than "group:kind";
	-- use it unless you have a good reason not to.

	-- `_dyestr` and `_dyemap` support
	local dyestr, dyemap = node_def._dyestr, node_def._dyemap
	if dyestr and type(dyestr) == "string" then
		local _, new_dyestr = subnodes.make_name_subname(dyestr, kind_name)
		subnode_def._dyestr = new_dyestr
	end
	if dyemap and type(dyemap) == "table" then
		local new_dyemap = {}
		for color, to_node in pairs(dyemap) do
			new_dyemap[color] = ({subnodes.make_name_subname(to_node, kind_name)})[2]
		end
		subnode_def._dyemap = new_dyemap
	end

	-- Add all whitelisted groups from node definition
	for _, whitelisted in ipairs(mod.GROUPS_WHITELIST) do
		if node_def.groups[whitelisted] then
			subnode_def.groups[whitelisted] = node_def.groups[whitelisted]
		end
	end

	-- Add all whitelisted fields from node definition
	for _, whitelisted in ipairs(mod.NODEDEF_WHITELIST) do
		if node_def[whitelisted] then
			subnode_def[whitelisted] = node_def[whitelisted]
		end
	end

	-- Apply the node overrides
	if kind_def.node_override then
		table.meld(subnode_def, kind_def.node_override)
	end
	if subnode_override then
		table.meld(subnode_def, subnode_override)
	end

	-- Call pre-register hook
	if kind_def.on_register then
		kind_def.on_register(subnode_name, subnode_def, node_name, node_def)
	end

	-- Register the subnode
	core.register_node(":" .. subnode_name, subnode_def)
	mod.registered_subnodes[subnode_name] = subnode_def

	-- Register the recipes
	if kind_def.get_crafts then
		local crafts = {kind_def.get_crafts(subnode_name, node_name)}
		for _, craft in ipairs(crafts) do
			core.register_craft(craft)
		end
	end

	-- Call post-register hook
	if kind_def.after_register then
		kind_def.after_register(subnode_name, node_name)
	end
end

function mod.register_subnodes(node_name, node_subnodes_def)
	local node_def = log:assert(
		core.registered_nodes[node_name],
		"Node passed to register_subnodes must be registered at the time of calling"
	)
	log:assert(
		node_subnodes_def and type(node_subnodes_def) == "table",
		"Invalid definition table passed to register_subnodes"
	)

	if mod.subnode_definitions[node_name] then -- allow redefinition (conflict resolve)
		for kind_name, subnode_override in pairs(node_subnodes_def) do
			if mod.subnode_definitions[node_name][kind_name] then
				local _, subnode_name = mod.make_name_subname(node_name, kind_name)
				core.override_item(subnode_name, subnode_override)
			else
				register_subnode(node_name, node_def, kind_name, mod.registered_kinds[kind_name],
					subnode_override)
			end
		end
		return
	end
	mod.subnode_definitions[node_name] = node_subnodes_def

	for kind_name, kind_def in pairs(mod.registered_kinds) do
		register_subnode(node_name, node_def, kind_name, kind_def, node_subnodes_def[kind_name])
	end
end

core.after(0, function()
	local count = 0
	for k, v in pairs(mod.registered_subnodes) do
		count = count + 1
	end
	log:debug("Registered " .. count .. " subnodes")
end)

-- Export global variable
subnodes = mod
