core keeper

Documentation for this module may be created at Module:Obtaining/doc

local Args = require("Module:Args")
local FrameUtils = require("Module:FrameUtils")
local StringUtils = require("Module:StringUtils")
local TableUtils = require("Module:TableUtils")
local MathUtils = require("Module:MathUtils")
local ObjectInfo = require("Module:ObjectInfo")
local ObjectIdFromName = require("Module:ObjectIdFromName")
local SceneInfo = require("Module:SceneInfo")
local DungeonInfo = require("Module:DungeonInfo")

local data = mw.loadData("Module:Obtaining/data")

local SourceOrder = {
	"terrain",
	"oreBoulder",
	"farming",
	"validHouse",
	"breeding",
	"naturalSpawn",
	"crafting",
	"recipe",
	"fishing",
	"trading",
	"merchant",
	"vendor",
	"cattleProduce",
	"loot",
	"drop",
	"lootScene",
	"dropScene",
	"polishing",
	"backgroundPerks",
	"salvage",
	"miscellaneous",
	"plunder"
}
local SourceCate = {
	crafting = "Craftable",
	recipe = "Craftable",
	fishing = "Fished",
	trading = "Traded",
	merchant = "Vendor",
	vendor = "Vendor",
	cattleProduce = "Cattle produce",
	loot = "Loot",
	lootScene = "Loot",
	drop = "Drop",
	dropScene = "Drop",
	farming = "Farmed",
	polishing = "Polished",
	salvage = "Salvage",
	plunder = "Plunder",
	backgroundPerks = "Background perk",
	naturalSpawn = "Natural spawn",
	terrain = "Terrain generation",
	validHouse = "Valid house",
	oreBoulder = "Ore boulder extraction",
	breeding = "Bred"
}
local AltSourceCate = {
	plunder = "Structure contents"
}
local SourceDisplayName = {
	crafting = "Crafting",
	recipe = "Crafting",
	fishing = "Fishing",
	trading = "Trading",
	merchant = "Merchant",
	vendor = "Vendor",
	cattleProduce = "Cattle produce",
	loot = "Loot",
	drop = "Drops",
	lootScene = "Structure-exclusive loot",
	dropScene = "Structure-exclusive drops",
	farming = "Farming",
	polishing = "Polishing",
	salvage = "Salvaging",
	plunder = "Plundering",
	backgroundPerks = "Background perks",
	naturalSpawn = "Natural spawn",
	terrain = "Terrain generation",
	validHouse = "Valid house",
	miscellaneous = "Miscellaneous",
	oreBoulder = "Ore boulder extraction",
	breeding = "Breeding"
}
local AltSourceDisplayName = {
	plunder = "Structure contents"
}
local BiomeNames = {
	Slime = "Undergrounds",
	Larva = "Clay Caves",
	Stone = "Forgotten Ruins",
	Nature = "Azeos' Wilderness",
	Mold = "Mold Dungeon",
	Sea = "Sunken Sea",
	Desert = "Desert of Beginnings",
	Lava = "Molten Quarry",
	Crystal = "Shimmering Frontier",
	Passage = "Passage"
}
local LiquidNames = {
	Dirt = "Normal water",
	LarvaHive = "Acid water",
	Mold = "Mold water",
	Sea = "Sea water",
	Lava = "Lava",
	Crystal = "Shimmering water",
	Passage = "Grimy water"
}
local NoteSeparator = " • "
local MerchantAvailability = {
	None = '<span class="note"><small>Always available.</small></span>',
	LarvaBossStatueActivated = "After the [[Ghorm Statue]] has been activated",
	HiveBossStatueActivated = "After the [[Malugaz Statue]] has been activated",
	CoreActivated = "After [[The Core]] has been activated",
	CoreBossDefeated = "After the [[Core Commander]] has been defeated"
}
local CustomEntityNames = {
	AnyWall = "Any mined block"
}
local Notes = {
	SeasonEaster = "During [[Easter]]",
	SeasonHalloween = "During [[Halloween]]",
	SeasonChristmas = "During [[Christmas]]",
	RarePlant = "3–15%, when planted with the [[Expert gardener]] talent active",
	Polishing = "10–50%, when crafted with the [[Jewelry crafter]] talent active",
	ArcheologistWallLoot = "0.2–1%, when mined with the [[Archeologist]] talent active",
	EventTerminal = "[[Challenge arena]] reward",
	CrystalMerchantSpawnItem = "20% if the [[Brave Merchant]] has moved in",
	PlunderLockedCopperChestSlime = 'Mining [[Dirt Block]] walls, [[Sand Block]] walls or [[Turf Block]] walls in the [[Undergrounds]] <span class="note"><small>(0.27%)</small></span>',
	PlunderLockedCopperChestLarva = 'Mining [[Clay Block]] walls or [[Sand Block]] walls in the [[Clay Caves]] <span class="note"><small>(0.27%)</small></span>',
	PlunderLockedIronChest = 'Mining [[Stone Block]] walls or [[Sand Block]] walls in the [[Forgotten Ruins]] <span class="note"><small>(0.27%)</small></span>',
	PlunderLockedScarletChest = 'Mining [[Grass Block]] walls or [[Stone Block]] walls in [[Azeos\' Wilderness]] <span class="note"><small>(0.27%)</small></span>',
	PlunderLockedOctarineChest = 'Mining [[Beach Block]] walls in the [[Sunken Sea]] <span class="note"><small>(0.27%)</small></span>',
	PlunderLockedGalaxiteChest = 'Mining [[Desert Block]] walls in the [[Desert of Beginnings]] <span class="note"><small>(0.27%)</small></span>',
	PlunderLockedSolariteChest = 'Mining [[Crystal Block]] walls in [[Azeos\' Wilderness]], [[Sunken Sea]], [[Desert of Beginnings]] or [[Shimmering Frontier]] <span class="note"><small>(0.27%)</small></span>',
	PlunderChallengeArenaReward = "[[Challenge arena]] reward container",
	OnlyInNature = "Only nearby [[Azeos' Wilderness]]",
	OnlyInSea = "Only nearby [[Sunken Sea]]",
	OnlyInDesert = "Only nearby [[Desert of Beginnings]]",
	Greggy = 'Creating a character named "Greggy" <span class="note"><small>(10)</small></span>'
}
local BiomeTerrainWallTypes = {
	Slime = {
		"WallDirtBlock",
		"WallTurfBlock",
		"WallSandBlock"
	},
	Larva = {
		"WallClayBlock",
		"WallSandBlock"
	},
	Stone = {
		"WallStoneBlock",
		"WallSandBlock"
	},
	Nature = {
		"WallGrassBlock",
		"WallStoneBlock"
	},
	Sea = {
		"WallLimestoneBlock"
	},
	Desert = {
		"WallDesertBlock"
	},
	Crystal = {
		"WallCrystalBlock"
	},
	Passage = {
		"WallPassageBlock"
	}
}

local function createTable()
	return mw.html.create("table"):addClass("fandom-table sortable"):cssText("font-size: 90%;")
end

local function formatAmountRange(amount)
	if type(amount) == "table" then
		return amount[1] .. "–" .. amount[2]
	else
		return tostring(amount)
	end
end

local function isAmountAboveOne(amount)
	if type(amount) == "table" then
		return amount[1] > 1 or amount[2] > 1
	else
		return amount > 1
	end
end

local function item(name, notes, format)
	assert(name ~= nil)
	local result = FrameUtils.template("Item", { name })

	if notes then
		notes = TableUtils.map(notes, function(note)
			return Notes[note] or note
		end)
		
		if TableUtils.length(notes) > 0 then
			local separator = format == "inline" and " " or "<br/>"
			result = result .. separator .. FrameUtils.note("(" .. table.concat(notes, NoteSeparator) .. ")")
		end
	end

	return result
end

local function entityItem(entity, notes, format)
	-- Item name
	local name
	if entity.custom then
		name = CustomEntityNames[entity.custom] or entity.custom
	else
		name = ObjectInfo.getStat(entity.id, entity.variation, "name")
	end

	-- Notes
	local allNotes = {}
	for _, note in ipairs(notes or {}) do
		TableUtils.push(allNotes, note)
	end

	return item(name, allNotes, format)
end

local function process(entries, functions)
	local result = TableUtils.map(entries, functions.map)
	if functions.sorter then
		table.sort(result, functions.sorter)
	end

	return result
end

----------------------------------------------------------
----------------- Source data formatters -----------------
----------------------------------------------------------
local sourceFormatters = {}

-- Farming
sourceFormatters.farming = {
	section = function(id, variation, entries)
		local table = createTable()

		table:tag("tr")
			:tag("th"):wikitext("Seed")
			:tag("th"):wikitext("Growth time")
		:done()

		entries = process(entries, {
			map = function(entry)
				local name = ObjectInfo.getStat(entry.id, entry.variation, "name")
				return {
					seed = item(name, entry.notes),
					growthTime = string.format("%s minutes", MathUtils.round(entry.growthTime / 60))
				}
			end,
			sorter = function(a, b)
				return a.seed < b.seed
			end
		})

		for _, entry in ipairs(entries) do
			table:tag("tr")
				:tag("td"):wikitext(entry.seed)
				:tag("td"):wikitext(entry.growthTime)
			:done()
		end

		return table
	end,
	inline = function(id, variation, entries)
		entries = process(entries, {
			map = function(entry)
				local name = ObjectInfo.getStat(entry.id, entry.variation, "name")
				return item(name, entry.notes, "inline")
			end,
			sorter = function(a, b)
				return a < b
			end
		})

		return table.concat(entries, NoteSeparator)
	end
}

-- Merchant
sourceFormatters.merchant = {
	section = function(id, variation, entries)
		local table = createTable()

		table:tag("tr")
			:tag("th"):wikitext("Merchant")
			:tag("th"):wikitext("Stock")
			:tag("th"):wikitext("Cost")
			:tag("th"):wikitext("Availability")
		:done()

		entries = process(entries, {
			map = function(entry)
				local name = ObjectInfo.getStat(entry.id, entry.variation, "name")
				return {
					merchant = item(name),
					stock = entry.stock,
					cost = ObjectInfo.getStat(id, variation, "buy"),
					availability = MerchantAvailability[entry.requirement]
				}
			end,
			sorter = function(a, b)
				return a.merchant < b.merchant
			end
		})

		for _, entry in ipairs(entries) do
			table:tag("tr")
				:tag("td"):wikitext(entry.merchant)
				:tag("td"):attr("align", "center"):wikitext(entry.stock)
				:tag("td"):attr("align", "center"):wikitext(entry.cost)
				:tag("td"):wikitext(entry.availability)
			:done()
		end

		return table
	end,
	inline = function(id, variation, entries)
		entries = process(entries, {
			map = function(entry)
				local name = ObjectInfo.getStat(entry.id, entry.variation, "name")
				return item(name, { ObjectInfo.getStat(id, variation, "buy") }, "inline")
			end,
			sorter = function(a, b)
				return a < b
			end
		})

		return table.concat(entries, NoteSeparator)
	end
}

-- Vendor --
sourceFormatters.vendor = {
	section = function(id, variation, entries)
		local table = createTable()

		table:tag("tr")
			:tag("th"):wikitext("Vendor")
			:tag("th"):wikitext("Cost")
		:done()

		entries = process(entries, {
			map = function(entry)
				local name = ObjectInfo.getStat(entry.id, entry.variation, "name")
				return {
					vendor = item(name),
					cost = ObjectInfo.getStat(id, variation, "buy")
				}
			end,
			sorter = function(a, b)
				return a.vendor < b.vendor
			end
		})

		for _, entry in ipairs(entries) do
			table:tag("tr")
				:tag("td"):wikitext(entry.vendor)
				:tag("td"):attr("align", "center"):wikitext(entry.cost)
			:done()
		end

		return table
	end,
	inline = function(id, variation, entries)
		entries = process(entries, {
			map = function(entry)
				local name = ObjectInfo.getStat(entry.id, entry.variation, "name")
				return item(name, { ObjectInfo.getStat(id, variation, "buy") }, "inline")
			end,
			sorter = function(a, b)
				return a < b
			end
		})

		return table.concat(entries, NoteSeparator)
	end
}

-- Crafting
sourceFormatters.crafting = {
	section = function(id, variation)
		return FrameUtils.template("Recipes", {
			result = ObjectInfo.getStat(id, variation, "name"),
			noresultunlessneeded = true
		})
	end,
	inline = function(id, variation)
		if not Recipes then
			Recipes = require("Module:Recipes")
		end
		return Recipes.extract(id)
	end
}

-- Recipe
sourceFormatters.recipe = sourceFormatters.crafting

-- Drop
sourceFormatters.drop = {
	section = function(id, variation, entries)
		local output = createTable()
		local isSceneLoot = entries[1].dungeons or entries[1].scenes
		local showQuantity = TableUtils.any(entries, function(entry)
			return isAmountAboveOne(entry.amount)
		end)
		local showChanceForOne = TableUtils.any(entries, function(entry)
			return MathUtils.round(entry.chance * 100) ~= MathUtils.round(entry.chanceAtLeastOne * 100)
		end)

		local header = output:tag("tr")
		if isSceneLoot then
			header:tag("th"):wikitext("Structure"):done()
		end
		header:tag("th"):wikitext("Source"):done()
		if showQuantity then
			header:tag("th"):wikitext("Quantity"):done()
		end
		if showChanceForOne then
			header:tag("th"):cssText("min-width: 100px;"):wikitext("Chance<br/><small>for one</small>"):done()
			header:tag("th"):cssText("min-width: 100px;"):wikitext("Chance<br/><small>per roll</small>"):done()
		else
			header:tag("th"):wikitext("Chance"):done()
		end
		header:done()

		entries = process(entries, {
			map = function(entry)
				local structures = {}
				
				if isSceneLoot then
					local scenes = TableUtils.map(entry.scenes or {}, function(scene)
						return string.format("[[%s|%s]]", SceneInfo.getLink(scene), SceneInfo.getDisplayName(scene, true))
					end)
					local dungeons = TableUtils.map(entry.dungeons or {}, function(dungeon)
						return string.format("[[%s|%s]]", DungeonInfo.getLink(dungeon), DungeonInfo.getDisplayName(dungeon, true))
					end)
					
					table.sort(scenes, function(a, b) return a < b end)
					table.sort(dungeons, function(a, b) return a < b end)

					for _, dungeon in ipairs(dungeons) do
						TableUtils.push(structures, dungeon)
					end
					for _, scene in ipairs(scenes) do
						TableUtils.push(structures, scene)
					end
				end
				
				local pools = {}
				if entry.guaranteedChance then
					TableUtils.push(pools, {
						amount = formatAmountRange(entry.guaranteedAmount),
						rolls = formatAmountRange(entry.guaranteedRolls or 1),
						chance = MathUtils.round(entry.guaranteedChance * 100) .. "%",
						chanceAtLeastOne = MathUtils.round(entry.guaranteedChanceAtLeastOne * 100) .. "%",
					})	
				end
				TableUtils.push(pools, {
					amount = formatAmountRange(entry.amount),
					rolls = formatAmountRange(entry.rolls or 1),
					chance = MathUtils.round(entry.chance * 100) .. "%",
					chanceAtLeastOne = MathUtils.round(entry.chanceAtLeastOne * 100) .. "%",
				})

				return {
					entity = entityItem(entry.entity, entry.notes),
					pools = pools,
					structures = #structures > 0 and table.concat(structures, "<hr/>") or nil
				}
			end,
			sorter = function(a, b)
				return a.entity < b.entity
			end
		})

		for _, entry in ipairs(entries) do
			local amounts = TableUtils.map(entry.pools, function(x)
				return x.amount
			end)
			local chances = TableUtils.map(entry.pools, function(x)
				return x.chance .. " " .. FrameUtils.note("(" .. x.rolls .. ")")
			end)
			local chanceAtLeastOnes = TableUtils.map(entry.pools, function(x)
				return x.chanceAtLeastOne
			end)
			
			local row = output:tag("tr")
			-- Structure
			if isSceneLoot then
				row:tag("td"):wikitext(entry.structures)
			end
			-- Source
			row:tag("td"):wikitext(entry.entity)
			-- Quantity
			if showQuantity then
				row:tag("td"):attr("align", "center"):wikitext(table.concat(amounts, "<hr/>"))
			end
			-- Chances
			if showChanceForOne then
				row:tag("td"):attr("align", "center"):wikitext(table.concat(chanceAtLeastOnes, "<hr/>"))
				row:tag("td"):attr("align", "center"):wikitext(table.concat(chances, "<hr/>"))
			else
				row:tag("td"):attr("align", "center"):wikitext(table.concat(chanceAtLeastOnes, "<hr/>"))
			end
			row:done()
		end

		return output
	end,
	inline = function(id, variation, entries)
		entries = process(entries, {
			map = function(entry)
				local notes = {
					-- Chance
					MathUtils.round(entry.chanceAtLeastOne * 100) .. "%"
				}
				for _, note in ipairs(entry.notes) do
					TableUtils.push(notes, note)
				end
				for _, dungeon in ipairs(entry.dungeons or {}) do
					TableUtils.push(notes, string.format("Found in the [[%s|%s]] dungeon", DungeonInfo.getLink(dungeon), DungeonInfo.getDisplayName(dungeon)))
				end
				for _, scene in ipairs(entry.scenes or {}) do
					TableUtils.push(notes, string.format("Found in the [[%s|%s]] scene", SceneInfo.getLink(scene), SceneInfo.getDisplayName(scene)))
				end
				return entityItem(entry.entity, notes, "inline")
			end,
			sorter = function(a, b)
				return a < b
			end
		})

		return table.concat(entries, NoteSeparator)
	end
}
sourceFormatters.dropScene = sourceFormatters.drop

-- Loot
sourceFormatters.loot = {
	section = function(id, variation, entries)
		local output = createTable()
		local isSceneLoot = entries[1].dungeons or entries[1].scenes
		local showQuantity = TableUtils.any(entries, function(entry)
			return isAmountAboveOne(entry.amount)
		end)
		local showChanceForOne = TableUtils.any(entries, function(entry)
			return MathUtils.round(entry.chance * 100) ~= MathUtils.round(entry.chanceAtLeastOne * 100)
		end)

		local header = output:tag("tr")
		if isSceneLoot then
			header:tag("th"):wikitext("Structure"):done()
		end
		header:tag("th"):wikitext("Container"):done()
		if showQuantity then
			header:tag("th"):wikitext("Quantity"):done()
		end
		if showChanceForOne then
			header:tag("th"):cssText("min-width: 100px;"):wikitext("Chance<br/><small>for one</small>"):done()
			header:tag("th"):cssText("min-width: 100px;"):wikitext("Chance<br/><small>per roll</small>"):done()
		else
			header:tag("th"):wikitext("Chance"):done()
		end
		header:done()

		entries = process(entries, {
			map = function(entry)
				local structures = {}
				
				if isSceneLoot then
					local scenes = TableUtils.map(entry.scenes or {}, function(scene)
						return string.format("[[%s|%s]]", SceneInfo.getLink(scene), SceneInfo.getDisplayName(scene, true))
					end)
					local dungeons = TableUtils.map(entry.dungeons or {}, function(dungeon)
						return string.format("[[%s|%s]]", DungeonInfo.getLink(dungeon), DungeonInfo.getDisplayName(dungeon, true))
					end)
					
					table.sort(scenes, function(a, b) return a < b end)
					table.sort(dungeons, function(a, b) return a < b end)
					
					for _, dungeon in ipairs(dungeons) do
						TableUtils.push(structures, dungeon)
					end
					for _, scene in ipairs(scenes) do
						TableUtils.push(structures, scene)
					end
				end
				
				local pools = {}
				if entry.guaranteedChance then
					TableUtils.push(pools, {
						amount = formatAmountRange(entry.guaranteedAmount),
						rolls = formatAmountRange(entry.guaranteedRolls or 1),
						chance = MathUtils.round(entry.guaranteedChance * 100) .. "%",
						chanceAtLeastOne = MathUtils.round(entry.guaranteedChanceAtLeastOne * 100) .. "%",
					})	
				end
				TableUtils.push(pools, {
					amount = formatAmountRange(entry.amount),
					rolls = formatAmountRange(entry.rolls or 1),
					chance = MathUtils.round(entry.chance * 100) .. "%",
					chanceAtLeastOne = MathUtils.round(entry.chanceAtLeastOne * 100) .. "%",
				})

				return {
					object = entityItem(entry.entity, entry.notes),
					pools = pools,
					structures = #structures > 0 and table.concat(structures, "<hr/>") or nil
				}
			end,
			sorter = function(a, b)
				return a.object < b.object
			end
		})

		for _, entry in ipairs(entries) do
			local amounts = TableUtils.map(entry.pools, function(x)
				return x.amount
			end)
			local chances = TableUtils.map(entry.pools, function(x)
				return x.chance .. " " .. FrameUtils.note("(" .. x.rolls .. ")")
			end)
			local chanceAtLeastOnes = TableUtils.map(entry.pools, function(x)
				return x.chanceAtLeastOne
			end)
			
			local row = output:tag("tr")
			-- Structure
			if isSceneLoot then
				row:tag("td"):wikitext(entry.structures)
			end
			-- Container
			row:tag("td"):wikitext(entry.object)
			-- Quantity
			if showQuantity then
				row:tag("td"):attr("align", "center"):wikitext(table.concat(amounts, "<hr/>"))
			end
			-- Chances
			if showChanceForOne then
				row:tag("td"):attr("align", "center"):wikitext(table.concat(chanceAtLeastOnes, "<hr/>"))
				row:tag("td"):attr("align", "center"):wikitext(table.concat(chances, "<hr/>"))
			else
				row:tag("td"):attr("align", "center"):wikitext(table.concat(chanceAtLeastOnes, "<hr/>"))
			end
			row:done()
		end

		return output
	end,
	inline = function(id, variation, entries)
		entries = process(entries, {
			map = function(entry)
				local notes = {
					-- Chance
					MathUtils.round(entry.chanceAtLeastOne * 100) .. "%"
				}
				for _, note in ipairs(entry.notes or {}) do
					TableUtils.push(notes, note)
				end
				for _, dungeon in ipairs(entry.dungeons or {}) do
					TableUtils.push(notes, string.format("Found in the [[%s|%s]] dungeon", DungeonInfo.getLink(dungeon), DungeonInfo.getDisplayName(dungeon)))
				end
				for _, scene in ipairs(entry.scenes or {}) do
					TableUtils.push(notes, string.format("Found in the [[%s|%s]] scene", SceneInfo.getLink(scene), SceneInfo.getDisplayName(scene)))
				end
				return entityItem(entry.entity, notes, "inline")
			end,
			sorter = function(a, b)
				return a < b
			end
		})

		return table.concat(entries, NoteSeparator)
	end
}
sourceFormatters.lootScene = sourceFormatters.loot

-- Fishing
sourceFormatters.fishing = {
	section = function(id, variation, entries)
		local output = createTable()

		output:tag("tr")
			:tag("th"):wikitext("Source")
			:tag("th"):wikitext("Catch type")
			:tag("th"):wikitext("Chance")
		:done()

		entries = process(entries, {
			map = function(entry)
				local lines = {}
				local normalWaterItem = item(LiquidNames.Dirt)
				
				for _, liquid in ipairs(entry.liquids) do
					TableUtils.push(lines, item(LiquidNames[liquid] or liquid))	
				end
				
				for _, biome in ipairs(entry.biomes) do
					TableUtils.push(lines, string.format("%s + %s", item(BiomeNames[biome] or biome), normalWaterItem))	
				end
				
				return {
					source = table.concat(lines, "<hr/>"),
					type = entry.type,
					chance = MathUtils.round(entry.chance * 100) .. "%"
				}
			end
		})

		for _, entry in ipairs(entries) do
			output:tag("tr")
				:tag("td"):wikitext(entry.source)
				:tag("td"):attr("align", "center"):wikitext(entry.type)
				:tag("td"):attr("align", "center"):wikitext(entry.chance)
			:done()
		end

		return output
	end
}

-- Polishing
sourceFormatters.polishing = {
	section = function(id, variation, entries)
		local table = createTable()

		table:tag("tr")
			:tag("th"):wikitext("Item crafted")
		:done()

		entries = process(entries, {
			map = function(entry)
				local name = ObjectInfo.getStat(entry.id, entry.variation, "name")
				return item(name, { Notes.Polishing })
			end,
			sorter = function(a, b)
				return a < b
			end
		})

		for _, entry in ipairs(entries) do
			table:tag("tr")
				:tag("td"):wikitext(entry)
			:done()
		end

		return table
	end,
	inline = function(id, variation, entries)
		entries = process(entries, {
			map = function(entry)
				local name = ObjectInfo.getStat(entry.id, entry.variation, "name")
				return string.format("Crafting %s", item(name, { Notes.Polishing }, "inline"))
			end,
			sorter = function(a, b)
				return a < b
			end
		})

		return table.concat(entries, NoteSeparator)
	end
}

-- Miscellaneous
sourceFormatters.miscellaneous = {
	section = function(id, variation, entries)
		local table = createTable()

		table:tag("tr")
			:tag("th"):wikitext("Source")
		:done()

		entries = process(entries, {
			map = function(entry)
				return Notes[entry.source]
			end,
			sorter = function(a, b)
				return a < b
			end
		})

		for _, entry in ipairs(entries) do
			table:tag("tr")
				:tag("td"):wikitext(entry)
			:done()
		end

		return table
	end,
	inline = function(id, variation, entries)
		entries = process(entries, {
			map = function(entry)
				return Notes[entry.source]
			end,
			sorter = function(a, b)
				return a < b
			end
		})

		return table.concat(entries, NoteSeparator)
	end
}

-- Background perks
sourceFormatters.backgroundPerks = {
	section = function(id, variation, entries)
		local table = createTable()

		table:tag("tr")
			:tag("th"):wikitext("Background")
			:tag("th"):wikitext("Quantity")
		:done()

		entries = process(entries, {
			map = function(entry)
				return {
					background = item(entry.background),
					amount = entry.amount
				}
			end,
			sorter = function(a, b)
				return a.background < b.background
			end
		})

		for _, entry in ipairs(entries) do
			table:tag("tr")
				:tag("td"):wikitext(entry.background)
				:tag("td"):attr("align", "center"):wikitext(entry.amount)
			:done()
		end

		return table
	end,
	inline = function(id, variation, entries)
		entries = process(entries, {
			map = function(entry)
				return string.format("%s perks", item(entry.background))
			end,
			sorter = function(a, b)
				return a < b
			end
		})

		return table.concat(entries, NoteSeparator)
	end
}

-- Salvage
sourceFormatters.salvage = {
	section = function(id, variation, entries)
		local table = createTable()

		table:tag("tr")
			:tag("th"):wikitext("Item")
			:tag("th"):wikitext("Quantity")
		:done()

		entries = process(entries, {
			map = function(entry)
				local name = ObjectInfo.getStat(entry.id, entry.variation, "name")
				return {
					item = item(name),
					amount = formatAmountRange(entry.amount)
				}
			end,
			sorter = function(a, b)
				return a.item < b.item
			end
		})

		for _, entry in ipairs(entries) do
			table:tag("tr")
				:tag("td"):wikitext(entry.item)
				:tag("td"):attr("align", "center"):wikitext(entry.amount)
			:done()
		end

		return table
	end
	-- No inline support
}

-- Trading
sourceFormatters.trading = {
	section = function(id, variation)
		return FrameUtils.template("Recipes", {
			result = ObjectInfo.getStat(id, variation, "name"),
			noresultunlessneeded = true
		})
	end,
	inline = function(id, variation)
		if not Recipes then
			Recipes = require("Module:Recipes")
		end
		return Recipes.extract(id)
	end
}

-- Cattle produce
sourceFormatters.cattleProduce = {
	section = function(id, variation)
		return FrameUtils.template("Recipes", {
			result = ObjectInfo.getStat(id, variation, "name"),
			noresultunlessneeded = true
		})
	end,
	inline = function(id, variation)
		if not Recipes then
			Recipes = require("Module:Recipes")
		end
		return Recipes.extract(id)
	end
}

-- Natural spawn
sourceFormatters.naturalSpawn = {
	section = function(id, variation, entries)
		local displayInitial = entries.initial
		local displayRespawn = entries.respawn
		local displayObject = entries.object

		local result = FrameUtils.template("Flex start")

		if displayRespawn then
			result = result .. FrameUtils.template("Spawns", {
				ObjectInfo.getStat(id, variation, "name"),
				type = "respawn"
			})
		end
		if displayInitial then
			result = result .. FrameUtils.template("Spawns", {
				ObjectInfo.getStat(id, variation, "name"),
				type = "initial"
			})
		end
		if displayObject then
			result = result .. FrameUtils.template("Spawns", {
				ObjectInfo.getStat(id, variation, "name"),
				type = "object"
			})
		end

		return result .. FrameUtils.template("Flex end")
	end
}

-- Terrain generation
sourceFormatters.terrain = {
	section = function(id, variation, entries)
		local output = createTable()
		
		local tileType = ObjectInfo.getInfo(id, variation).tileType
		local isContainedResource = tileType == "ore" or tileType == "ancientCrystal"
		
		local header = output:tag("tr")
		header:tag("th"):wikitext("Biome"):done()
		if isContainedResource then
			header:tag("th"):wikitext("Embedded within"):done()
		end
		header:done()
		
		for _, entry in ipairs(entries) do
			local row = output:tag("tr")
			row:tag("td"):wikitext(item(BiomeNames[entry.biome] or entry.biome)):done()
			
			if isContainedResource then
				local embeddedWithin = TableUtils.map(BiomeTerrainWallTypes[entry.biome] or {}, function(x)
					return item(ObjectInfo.getStat(x, 0, "name"))
				end)
				row:tag("td"):wikitext(table.concat(embeddedWithin, "<br/>")):done()
			end
			
			row:done()
		end

		return output
	end
}

-- Scene/dungeon plunder
sourceFormatters.plunder = {
	section = function(id, variation, entries)
		local table = createTable()

		table:tag("tr")
			:tag("th"):cssText("min-width: 220px;"):wikitext("Structure")
			:tag("th"):wikitext("Amount")
		:done()

		local hasDungeon = false
		local hasScene = false

		entries = process(entries, {
			map = function(entry)
				if not hasDungeon and entry.dungeon then
					hasDungeon = true
				end
				if not hasScene and entry.scene then
					hasScene = true
				end

				return {
					scene = entry.scene and string.format("[[%s|%s]]", SceneInfo.getLink(entry.scene), SceneInfo.getDisplayName(entry.scene, true)),
					dungeon = entry.dungeon and string.format("[[%s|%s]]", DungeonInfo.getLink(entry.dungeon), DungeonInfo.getDisplayName(entry.dungeon, true)),
					amount = entry.amount or "Varies"
				}
			end,
			sorter = function(a, b)
				if a.dungeon and not b.dungeon then
					return 1
				end
				if a.dungeon and b.dungeon then
					return a.dungeon < b.dungeon
				end
				if a.scene and b.scene then
					return a.amount == b.amount and (a.scene < b.scene) or a.amount > b.amount
				end
			end
		})

		local addedDungeonHeader = false
		local addedSceneHeader = false

		for _, entry in ipairs(entries) do
			if hasDungeon and hasScene then
				if entry.dungeon and not addedDungeonHeader then
					addedDungeonHeader = true
					table:tag("tr")
						:tag("th"):attr("colspan", "2"):wikitext("Dungeons")
					:done()
				end
				if entry.scene and not addedSceneHeader then
					addedSceneHeader = true
					table:tag("tr")
						:tag("th"):attr("colspan", "2"):wikitext("Scenes")
					:done()
				end
			end

			table:tag("tr")
				:tag("td"):wikitext(entry.dungeon or entry.scene)
				:tag("td"):attr("align", "center"):wikitext(entry.amount)
			:done()
		end

		return table
	end
	-- No inline support
}

-- Valid house (merchants)
sourceFormatters.validHouse = {
	section = function(id, variation, entries)
		local output = createTable()

		output:tag("tr")
			:tag("th"):wikitext("Spawning item"):done()
		:done()
		
		for _, entry in ipairs(entries) do
			output:tag("tr")
				:tag("td"):wikitext(item(ObjectInfo.getStat(entry.item, 0, "name"))):done()
			:done()
		end

		return output
	end
}

-- Ore boulder extraction
sourceFormatters.oreBoulder = {
	section = function(id, variation, entries)
		local table = createTable()

		table:tag("tr")
			:tag("th"):wikitext("Ore boulder")
			:tag("th"):wikitext("Total ore")
		:done()

		entries = process(entries, {
			map = function(entry)
				return {
					boulder = item(ObjectInfo.getStat(entry.id, entry.variation, "name")),
					total = entry.total
				}
			end,
			sorter = function(a, b)
				return a.boulder < b.boulder
			end
		})

		for _, entry in ipairs(entries) do
			table:tag("tr")
				:tag("td"):wikitext(entry.boulder)
				:tag("td"):attr("align", "center"):wikitext(entry.total)
			:done()
		end

		return table
	end
}

-- Cattle breeding
sourceFormatters.breeding = {
	section = function(id, variation, entries)
		local output = createTable()

		output:tag("tr")
			:tag("th"):wikitext("Source"):done()
		:done()
		
		for _, entry in ipairs(entries) do
			local parent = item(ObjectInfo.getStat(entry.id, 0, "name"))

			output:tag("tr")
				:tag("td"):wikitext(string.format("%s + %s", parent, parent)):done()
			:done()
		end

		return output
	end
}

local outputFormatters = {}

outputFormatters.section = function(objects, shouldAddCategories)
	local createSection = function(object)
		local result = ""

		local info = ObjectInfo.getInfo(object.id, object.variation)
		local isCreature = info.type == "Creature" or info.type == "Critter"
		local isNonObtainable = info.type == "NonObtainable" or info.indestructible or info.destructible or isCreature

		for _, source in ipairs(SourceOrder) do
			local sourceFormatter = sourceFormatters[source]

			if sourceFormatter and sourceFormatter.section then
				local entries = object.data[source]

				if entries then
					local sourceDisplayName = (isNonObtainable and AltSourceDisplayName[source]) or SourceDisplayName[source]
					local sourceCate = (isNonObtainable and AltSourceCate[source]) or SourceCate[source]

					result = result .. "<h3>" .. sourceDisplayName .. "</h3>"
					result = result .. tostring(sourceFormatter.section(object.id, object.variation, entries))

					if shouldAddCategories and sourceCate then
						if isCreature then
							sourceCate = sourceCate .. " creatures"	
						else
							sourceCate = sourceCate .. " items"	
						end
						
						result = result .. string.format("[[Category:%s]]", sourceCate)
					end
				end
			end
		end

		return result
	end

	if #objects > 1 then
		local sections = {}
		local namesubs = Args.getArg("namesub") and StringUtils.explode("/", Args.getArg("namesub")) or {}

		for _, object in ipairs(objects) do
			local name = ObjectInfo.getStat(object.id, object.variation, "name")
			for _, namesub in ipairs(namesubs) do
				name = string.gsub(name, " " .. namesub, "")
				name = string.gsub(name, namesub .. " ", "")
			end

			sections[#sections + 1] = string.format('%s=<div class="mobileonly"><h3>%s</h3></div>%s', name, name, createSection(object))
		end

		return FrameUtils.parserFunction("#tag", { "tabber", table.concat(sections, "|-|") })
	else
		return createSection(objects[1])
	end
end

outputFormatters.inline = function(objects)
	-- Currently doesn't support multiple objects
	local object = objects[1]

	local result = mw.html.create("div"):addClass("obtaining-inline")
	local sections = {}

	for _, source in ipairs(SourceOrder) do
		local sourceFormatter = sourceFormatters[source]

		if sourceFormatter and sourceFormatter.inline then
			local entries = object.data[source]

			if entries then
				local tag = mw.html.create("div")
					:tag("div"):wikitext(sourceFormatter.inline(object.id, object.variation, entries))

				sections[#sections + 1] = tostring(tag)
			end
		end
	end

	result:wikitext(table.concat(sections, "<hr/>"))

	return tostring(result)
end

local p = {}

function p.getSources(id, variation, type)
	if data[id] and data[id][variation] then
		if type then
			return data[id][variation][type] or {}
		else
			return data[id][variation]
		end
	end
	return {}
end

function p._main()
	local title = mw.title.getCurrentTitle()

	local format = Args.getArg("mode") or "section"
	local objectNames = StringUtils.explode("/", Args.getArg(1) or title.baseText)
	local shouldAddCategories = title.namespace == 0 and not Args.getFlag("nocat")

	local objects = TableUtils.map(objectNames, function(name)
		local id, variation = ObjectIdFromName.get(name, name)
		if not id then return end

		local sources = p.getSources(id, variation)
		if TableUtils.length(sources) == 0 then return end

		return {
			id = id,
			variation = variation,
			data = sources
		}
	end)

	assert(#objects > 0, "No sources to display (unknown objects or they have no sources)")

	local outputFormatter = outputFormatters[format]
	assert(outputFormatter, "Unknown format " .. tostring(format))

	return outputFormatter(objects, shouldAddCategories)
end

return p