模組:PlatformMap

local p = {}

---@overload fun(x: any): boolean?
local yesno = require('Module:Yesno')

---@type { [string]: { set: string, subset: string } }
local alias_map = {
	['SS']  = { set = 'lr\\lr', subset = 'a\\\\\\e' },
	['SIS'] = { set = 'lr\\\\lr', subset = 'a\\\\ae\\\\e' },
	['I']   = { set = 'lr\\\\\\lr', subset = 'e\\a' }
}

---@param frame frame
---@return string
function p.main(frame)
	local args = frame:getParent().args
	---@module 'PlatformMapShared'
	local util = require('Module:PlatformMap/Shared')
	---@module "PlatformMapStrings"
	local strs = require('Module:PlatformMap/Strings')

	-- Import set inst
	local set = args[1]
	local subset = args[2]
	if not set then
		return '~~ ~~' .. strs.err_nil_arg_set
	end
	if alias_map[set] then
		subset = alias_map[set].subset
		set = alias_map[set].set
	end
	---@type { subsets: { [string]: string[][] }, rows: { tracks: integer[], [integer]: { continue: ('left'|'right')?, to: ('left'|'right')?, trimmable: boolean? } }, switches: { both: { [string]: string[][] }, left: { [string]: string[][] }, right: { [string]: string[][] } } }
	local meta = require('Module:PlatformMap/' .. set)

	-- Init
	local inst
	if meta.subsets[subset] then
		inst = meta.subsets[subset]
	else
		local rows = mw.text.split(subset, ' ')
		if #rows > 1 then
			inst = {}
			for r, row in ipairs(rows) do
				inst[r] = mw.text.split(row, '\\')
			end
		else
			return '~~ ~~' .. mw.ustring.format(strs.err_unknown_subset, set, subset)
		end
	end
	local stats = {
		custom_subset = false, -- TODO
		custom_switches = false, -- TODO
		unknown_switches = {}
	}

	-- Override track connections
	for t, r in ipairs(meta.rows.tracks) do
		if args['continue' .. t] then
			meta.rows[r].continue = args['continue' .. t]
		end
	end

	local i
	-- Left switches
	i = 1
	while (true) do
		local name = args['switchL' .. i]
		if not name then
			break
		end

		local switch = meta.switches.both[name] or meta.switches.left[name]
		if switch then
			util.matrix.prepend(inst, switch)
		else
			local rows = mw.text.split(name, ' ')
			if #rows == #inst then -- direct input
				switch = {}
				for r, row in ipairs(rows) do
					switch[r] = mw.text.split(row, '\\')
				end
				util.matrix.prepend(inst, switch)
			elseif mw.ustring.sub(name, 1, 3) == 'STR' and mw.ustring.sub(name, -1, -1) == '#' then
				local pad = tonumber(mw.ustring.sub(name, 4, -2))
				for r = 1, #inst do
					local info = meta.rows[r]
					if info and info.to and info.continue ~= 'right' then
						for _ = 1, pad do
							table.insert(inst[r], 1, 'uSTRq')
						end
					else
						for _ = 1, pad do
							table.insert(inst[r], 1, '')
						end
					end
				end
			else
				table.insert(stats.unknown_switches, name)
			end
		end

		i = i + 1
	end

	-- Right switches
	i = 1
	while (true) do
		local name = args['switchR' .. i]
		if not name then
			break
		end

		local switch = meta.switches.both[name] or meta.switches.right[name]
		if switch then
			util.matrix.append(inst, switch)
		else
			local rows = mw.text.split(name, ' ')
			if #rows == #inst then -- direct input
				switch = {}
				for r, row in ipairs(rows) do
					switch[r] = mw.text.split(row, '\\')
				end
				util.matrix.append(inst, switch)
			elseif mw.ustring.sub(name, 1, 3) == 'STR' and mw.ustring.sub(name, -1, -1) == '#' then
				local pad = tonumber(mw.ustring.sub(name, 4, -2))
				for r = 1, #inst do
					local info = meta.rows[r]
					if info and info.to and info.continue ~= 'left' then
						for _ = 1, pad do
							table.insert(inst[r], 'uSTRq')
						end
					else
						for _ = 1, pad do
							table.insert(inst[r], '')
						end
					end
				end
			else
				table.insert(stats.unknown_switches, name)
			end
		end

		i = i + 1
	end

	-- Tracks (round I)
	local replace_l = true
	local replace_r = true
	for t, r in ipairs(meta.rows.tracks) do
		-- override orientions
		if args['to' .. t] then meta.rows[r].to = args['to' .. t] end
		-- track end detection
		local row = inst[r]
		local info = meta.rows[r]
		if info.continue ~= 'right' then
			local bound = args['bound' .. t .. 'L'] or args['boundL']
			if info.to == 'right' or (bound ~= nil and bound ~= '') then
				if not util.icon.map[row[1]] then
					replace_l = false
				end
			else
				if string.find(row[1], 'BS') then
					replace_l = false
				end
			end
		end
		if info.continue ~= 'left' then
			local bound = args['bound' .. t .. 'R'] or args['boundR']
			if info.to == 'left' or (bound ~= nil and bound ~= '') then
				if not util.icon.map[row[#row]] then
					replace_r = false
				end
			else
				if string.find(row[#row], 'BS') then
					replace_r = false
				end
			end
		end
		-- outbound text (overrided by manual)
		local nxt = args['next' .. t]
		if nxt then
			local system = args['system' .. t] or args.system
			local line = args['line' .. t] or args.line
			local typ = args['type' .. t] or args.type
			local inverse = yesno(args['inverse' .. t]) or false
			if meta.rows[r].to == 'left' then
				row.textL = util.text.adjacent
					{ system = system, line = line, type = typ, next = nxt, left = true, inverse = inverse } .. '~~'
			else
				row.textR = '~~' ..
					util.text.adjacent
						{ system = system, line = line, type = typ, next = nxt, left = false, inverse = inverse }
			end
		end
	end
	-- Manual texts
	for r, row in ipairs(inst) do
		-- Manual text
		if args['textL' .. r] then
			row.textL = args['textL' .. r]
		end
		if args['textR' .. r] then
			row.textR = args['textR' .. r]
		end
	end
	-- Tracks (round II)
	local noextend_l = (args['boundL'] == 'hard')
	local noextend_r = (args['boundR'] == 'hard')
	for t, r in ipairs(meta.rows.tracks) do
		-- track ends
		local row = inst[r]
		local info = meta.rows[r]
		-- left
		if info.continue ~= 'right' then
			local bound = args['bound' .. t .. 'L'] or args['boundL']
			if bound == 'hard' then
				if replace_l then
					row[1] = util.icon.map[row[1]].hard.on_left
				else
					table.insert(row, 1, util.icon.map['uSTRq'].hard.on_left)
				end
				if not noextend_l then
					table.insert(row, 1, '')
				end
			elseif bound == 'soft' then
				if replace_l then
					row[1] = util.icon.map[row[1]].soft.on_left
				else
					table.insert(row, 1, util.icon.map['uSTRq'].soft.on_left)
				end
				table.insert(row, 1, util.icon.map['uexCONTgq'][info.to])
			else
				if replace_l then
					if util.icon.map[row[1]] then
						row[1] = util.icon.map[row[1]].open.on_left
					end
				else
					table.insert(row, 1, util.icon.map['uSTRq'].open.on_left)
				end
				table.insert(row, 1, util.icon.map['uCONTgq'][info.to])
			end
		else
			if not replace_l then
				table.insert(row, 1, '')
			end
			if not noextend_l then
				table.insert(row, 1, '')
			end
		end
		-- right
		if info.continue ~= 'left' then
			local bound = args['bound' .. t .. 'R'] or args['boundR']
			if bound == 'hard' then
				if replace_r then
					row[#row] = util.icon.map[row[#row]].hard.on_right
				else
					table.insert(row, util.icon.map['uSTRq'].hard.on_right)
				end
				if not noextend_r then
					table.insert(row, '')
				end
			elseif bound == 'soft' then
				if replace_r then
					row[#row] = util.icon.map[row[#row]].soft.on_right
				else
					table.insert(row, util.icon.map['uSTRq'].soft.on_right)
				end
				table.insert(row, util.icon.map['uexCONTfq'][info.to])
			else
				if replace_r then
					if util.icon.map[row[#row]] then
						row[#row] = util.icon.map[row[#row]].open.on_right
					end
				else
					table.insert(row, util.icon.map['uSTRq'].open.on_right)
				end
				table.insert(row, util.icon.map['uCONTfq'][info.to])
			end
		else
			if not replace_r then
				table.insert(row, '')
			end
			if not noextend_r then
				table.insert(row, '')
			end
		end
	end

	-- Fix align
	local nontrack_pad = 0
	if replace_r then nontrack_pad = nontrack_pad - 1 end
	if replace_l then nontrack_pad = nontrack_pad + 1 end
	if noextend_r then nontrack_pad = nontrack_pad - 1 end
	if noextend_l then nontrack_pad = nontrack_pad + 1 end
	if nontrack_pad ~= 0 then
		for r, row in ipairs(inst) do
			local info = meta.rows[r]
			if not info or not info.to then
				if nontrack_pad == -2 then
					table.insert(row, 1, '')
					table.insert(row, 1, '')
				elseif nontrack_pad == -1 then
					table.insert(row, 1, '')
				elseif nontrack_pad == 1 then
					table.insert(row, '')
				elseif nontrack_pad == 2 then
					table.insert(row, '')
					table.insert(row, '')
				end
			end
		end
	end

	-- Trim unused rows
	for r = #inst, 1, -1 do
		local row = inst[r]
		local info = meta.rows[r]
		if info and info.trimmable then
			local trim = true
			for c = 1, #row do
				if row[c] ~= '' then
					trim = false
					break
				end
			end
			if trim then table.remove(inst, r) end
		end
	end

	-- Padding
	if args.pad then
		local pad = tonumber(args.pad)
		if pad < 0 then
			for r = 1, #inst do
				for _ = 1, -pad do
					table.insert(inst[r], 1, '')
				end
			end
		elseif pad > 0 then
			for r = 1, #inst do
				for _ = 1, pad do
					table.insert(inst[r], '')
				end
			end
		end
	end

	-- Error
	if #stats.unknown_switches > 0 then
		local row = inst[#inst]
		row.textR = table.concat {
			mw.ustring.format(strs.err_unknown_switches, table.concat(stats.unknown_switches, ', ')),
			row.textR
		}
	end

	-- Output
	local lines = {}

	for r, row in ipairs(inst) do
		lines[r] = table.concat(row, '\\')
		---@diagnostic disable-next-line: undefined-field
		if row.textL then lines[r] = mw.ustring.format('%s! !%s', row.textL, lines[r]) end
		---@diagnostic disable-next-line: undefined-field
		if row.textR then lines[r] = mw.ustring.format('%s~~%s', lines[r], row.textR) end
	end

	return table.concat(lines, '\n')
end

---@param frame frame
---@return string
function p.switch(frame)
	local args = frame.args
	local set = args[1]
	set = require('Module:PlatformMap/' .. set)
	local switch = args[2]
	local side = args[3] or 'both'
	switch = set.switches[side][switch]
	local lines = {}
	for l = 1, #switch do
		lines[l] = table.concat(switch[l], '\\') .. '~~' .. l .. '~~'
	end
	return table.concat(lines, '\n')
end

return p