This module adds built-in categorization to our various astronomical object infoboxes based on their grid coordinate fields. It's embedded at the bottom of each infobox, and generates a category using the coordinate value.


local p = {}

local function makeCategoryLink(cat)
	-- "Category" is split out here so that the module isn't put into the
	-- category "%s" when the page is saved.
	return string.format('[[%s:%s]]', 'Category', cat, mw.title.getCurrentTitle().text)
end

local function exists(page)
	local success, title = pcall(mw.title.new, page)
	return success and title and title.exists or false
end

local function yes(v)
	return v ~= nil and v ~= ""
end

local function isCanon(title)
	if title.text:find('/Legends') then
		return 'false'
	elseif title.text:find('/Canon') then
		return 'true'
	elseif exists(title.text..'/Canon') then
		return 'false'
	end
	
    for index, v in ipairs(title.categories) do
    	if v == "Non-canon Legends articles" or v == "Legends cut content" then
    		return 'ncl'
        elseif v == "Legends articles" or v == "Articles from unlicensed sources" then
            return 'false'
        elseif v == "Non-canon articles" or v:sub(0, 11) == "Cut content" then
        	return '*'
        elseif v == "Canon articles" then
        	return 'true'
        end
    end
    return 'true'
end

function separateLines(txt)
	lines = {}
	if txt:find("\r\n") == nil then
		table.insert(lines, txt)
		return lines
	end
	
	for s in txt:gmatch("[^\r\n]+") do
		if s ~= nil then
			if s:find("<ref") ~= nil then
				table.insert(lines, s:sub(0, s:find("<ref") - 1))
			else
				table.insert(lines, s)
			end
		end
	end
	return lines
end

local function char(num)
  return string.char(string.byte("A")+num-1)
end

local function isUnlicensed(page)
	for index, v in ipairs(page.categories) do
        if v == "Articles from unlicensed sources" then
            return true
        end
	end
	return false
end

REGION_ORDER = {
	["Deep Core"] = "0",
	["Hutt Space"] = "7",
	["Wild Space"] = "8",
	["Unknown Regions"] = "9",
	["Core Worlds"] = "1",
	["Colonies"] = "2",
	["Inner Rim"] = "3",
	["Expansion Region"] = "4",
	["Mid Rim"] = "5",
	["Outer Rim"] = "6",
}

local function determineSorting(val)
	for k, v in pairs(REGION_ORDER) do
		if val:find(k) then
			return "|"..v
		end
	end
	return ""
end

local function checkSector(val)
	local CHECK_SECTORS = {"Vak Combine", "Garwian Unity", "Tower Dimension"}
	for _, k in ipairs(CHECK_SECTORS) do
		if val:find(k) then
			return true
		end
	end
	return false
end

local function searchForGrid(txt, lnum, num)
	local s = char(lnum) .. "%-" .. num
	if txt:find(s .. "[0-9]") then
		return false
	else
		return txt:find(s)
	end
end

function determineGridCategories(txt, catName)
	local ret = {''}
	local mobile, unknown, canonOnly = false, false, false
	local lines = separateLines(txt)
	for i, t in pairs(lines) do
		if not mobile then
			if t and string.len(t) > 0 then
				if txt:find('Mobile') then
					mobile = true
				elseif txt:lower():find('canon%-only') then
					canonOnly = true
				elseif txt:find('N/A') then
					unknown = true
				else
					for lnum=1, 26, 1 do  
						for num=1,26,1 do
							if searchForGrid(txt, lnum, num) then
								ret[#ret + 1] = makeCategoryLink(catName .. " in grid square " .. char(lnum) .. "-" .. num)
							end
						end
					end
				end
			end
		end
	end
	if unknown then
		return makeCategoryLink("Locations with unknown or extragalactic grid coordinates")
	elseif canonOnly then
		return makeCategoryLink("Legends locations with canon-only grid coordinates")
	end
	return table.concat(ret)
end

local function checkCoordinates(gridCats, mobile, hasCoordinates, templateName, c, unlisted, checkGrid)
	if string.len(gridCats) > 0 then
		return gridCats, true
	elseif mobile then
		return '', false
	elseif hasCoordinates then
		return makeCategoryLink('Locations with invalid grid coordinates'), false
	elseif isUnlicensed(mw.title.getCurrentTitle()) then
		return '', false
	elseif unlisted or not checkGrid then
		return '', false
	end
	local catName = 'Locations without grid coordinates'
	if templateName and string.len(templateName) > 0 then
		catName = catName .. '/' .. templateName
	end
	if c == nil then
		c = isCanon(mw.title.getCurrentTitle())
	end
	if c == "false" then
		catName = catName .. '/Legends'
	elseif c == "ncl" then
		return '', false
	end
	return makeCategoryLink(catName), false
end

local function extractLink(cx)
	local c = cx:gsub('%[', ''):gsub('%]', ''):gsub('/Canon', ''):gsub('/Legends', '')
	if c:find('|') then
		c = c:sub(0, c:find('|') - 1)
	end
	return c
end

local function systemSort(args)
	local cx = ""
	if yes(args.system) and args.system ~= "N/A" and args.system ~= "None" then
		cx = extractLink(args.system)
	end
	if not yes(cx) and yes(args.orbited) then
		cx = extractLink(args.orbited)
	end
	if yes(cx) then
		return "|"..cx
	end
	return ""
end

local function validateFields(args, ret)
	local hasAllData = true
	if not yes(args.region) then
		hasAllData = false
		if args.system == "N/A" then
			ret[#ret + 1] = makeCategoryLink("Systems and sectors without region")
		else
			ret[#ret + 1] = makeCategoryLink("Astronomical objects without region"..systemSort(args))
		end
	end
	
	local sx = args.system == "N/A" and "Systems" or "Astronomical objects"
	local i = yes(args.region) and determineSorting(args.region) or ""
	if (args.sector == nil or args.sector == "") then
		if i ~= "|0"  and i ~= "|7" and i ~= "|8" and i ~= "|9" then
			ret[#ret + 1] = makeCategoryLink(sx.." without sector")
		end
		hasAllData = false
	elseif args.sector == "None" then
		if i == "|0"  or i == "|7" or i == "|8" or i == "|9" then
			i = "/Expected"..i
		end
		ret[#ret + 1] = makeCategoryLink(sx.." with unknown sector"..i)
	elseif (args.sector == "N/A" or args.sector:find("Freestanding")) and yes(args.outside) then
		if i ~= "|8" and i ~= "|9" then
			local s1 = args.system == "N/A" and "Star systems" or "Astronomical objects"
			ret[#ret + 1] = makeCategoryLink(s1.." outside of established sectors")
		end
	end
	
	if (args.system == nil or args.system == "") then
		ret[#ret + 1] = makeCategoryLink("Astronomical objects without system")
		hasAllData = false
	end
	return hasAllData
end

local getArgs = require('Module:Arguments').getArgs
function p.main(frame)
	local args = getArgs(frame)
	local categoryName = 'Locations'
	if args.category_name then
		categoryName = args.category_name
	end
	local templateName = ''
	if args.template_name then
		templateName = args.template_name
	end
	if mw.title.getCurrentTitle().namespace ~= 0 then
		return ''
	end
	local checkGrid = true
	if args.no_grid_check then
		checkGrid = false
	end
	
	local gridCats = ''
	local hasCoordinates = false
	local mobile = false
	local vx = ''
	for k, v in pairs(frame:getParent().args) do
		if k == "coord" or k == "coordinates" then
			if v and string.len(v) > 0 then
				hasCoordinates = true
				if v:find('Mobile') then
					mobile = true
				end
				vx = v
				local c = determineGridCategories(v, categoryName)
				if checkGrid and c and string.len(c) > 0 then
					gridCats = gridCats .. c
				end
			end
		end
	end
	
	local c = nil
	local t = mw.title.getCurrentTitle()
	local unidentified = t.text:sub(0, 12) == "Unidentified" or t.text:find('homeworld') or t.text:find('home planet')
	local ret = {}
	if yes(args.unlisted) then
		cx = "|"..args.unlisted
		if args.unlisted == "2" then
			cx = "|*"
		elseif args.unlisted == "1" then
			cx = systemSort(args)
		end
		ret[#ret + 1] = makeCategoryLink("Astronomical objects unlisted in the Appendix"..cx)
	else
		local hasAllData = validateFields(args, ret)
		if not hasAllData then
			c = isCanon(t)
			if unidentified and (c == '*' or c == 'false' or c == 'ncl') then
				if #gridCats == 0 then
					return ''
				end
				ret = {}
			elseif unidentified then
				ret = {makeCategoryLink("Unidentified astronomical objects with incomplete data")}
			elseif c == "*" then
				return makeCategoryLink("Non-canon astronomical objects with incomplete data")
			elseif c == 'false' or c == 'ncl' then
				ret = {}
			end
		end
	end
	
	local gridCat, coordinatesKnown = checkCoordinates(gridCats, mobile, hasCoordinates, templateName, c, args.unlisted, checkGrid)
	if coordinatesKnown then	-- add grid coordinates if they're defined
		ret[#ret + 1] = gridCat
	elseif not unidentified and not yes(args.unlisted) then -- skip the missing-coordinates category for unlisted
		ret[#ret + 1] = gridCat
	end
	
	if yes(args.sector) and checkSector(args.sector) then
		ret[#ret + 1] = makeCategoryLink("Incorrect sector values")
	end
	
	return table.concat(ret)
end

return p