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

local mf = require('Module:Mapframe')
local getArgs = require('Module:Arguments').getArgs
local yesno = require('Module:Yesno')
local infoboxImage = require('Module:InfoboxImage').InfoboxImage

-- Defaults
local DEFAULT_FRAME_WIDTH = "270"
local DEFAULT_FRAME_HEIGHT = "200"
local DEFAULT_ZOOM = 10
local DEFAULT_GEOMASK_STROKE_WIDTH = "1"
local DEFAULT_GEOMASK_STROKE_COLOR = "#777777"
local DEFAULT_GEOMASK_FILL = "#888888"
local DEFAULT_GEOMASK_FILL_OPACITY = "0.25"
local DEFAULT_SHAPE_STROKE_WIDTH = "2"
local DEFAULT_SHAPE_STROKE_COLOR = "#FF0000"
local DEFAULT_SHAPE_FILL = "#606060"
local DEFAULT_SHAPE_FILL_OPACITY = "0.1"
local DEFAULT_LINE_STROKE_WIDTH = "5"
local DEFAULT_LINE_STROKE_COLOR = "#FF0000"
local DEFAULT_MARKER_COLOR = "#5E74F3"

local util = {}

function util.noop(info)
	local DEFAULT_NOOP_OUTPUT = ""
	-- uncomment this when debugging
	-- DEFAULT_NOOP_OUTPUT = "debug: mapframe no-op: " .. info
	-- mw.log(DEFAULT_NOOP_OUTPUT)
	return DEFAULT_NOOP_OUTPUT
end

function util.trimArgs(argsTable)
	local cleanArgs = {}
	for key, val in pairs(argsTable) do
		if type(val) == 'string' then
			val = val:match('^%s*(.-)%s*$')
			if val ~= '' then
				cleanArgs[key] = val
			end
		else
			cleanArgs[key] = val
		end
	end
	return cleanArgs
end

function util.shouldAutoRun(frame)
	local pargs = frame.getParent(frame).args
	local explicitlyOn = yesno(mw.text.trim(pargs.mapframe or ""))
	if pargs.coordinates == "{{{coordinates}}}" then explicitlyOn = false end
	if type(pargs.coordinates) == 'string' and string.match(pargs.coordinates, 'coord.missing') then explicitlyOn = false end
	local onByDefault = (explicitlyOn == nil) and yesno(mw.text.trim(frame.args.onByDefault or ""), false)
	return explicitlyOn or onByDefault
end

function util.argsFromAuto(frame)
	local args = getArgs(frame, {parentFirst = true})
	local fixedArgs = {}
	for name, val in pairs(args) do
		local fixedName = string.match(name, "^mapframe%-(.+)$")
		if fixedName then
			fixedArgs[fixedName] = val
		elseif (name == "coordinates" or name == "coord" or name == "coordinate") and not fixedArgs.coord then
			fixedArgs.coord = val
		elseif (name == "id" or name == "qid") and not fixedArgs.id then
			-- kept only so old template calls do not explode; ignored for data lookup
			fixedArgs.id = val
		elseif name == "captionstyle" and not fixedArgs.captionstyle then
			fixedArgs.captionstyle = val
		end
	end
	return fixedArgs
end

function util.parseCustomWikitext(customWikitext)
	return infoboxImage({
		args = {
			image = customWikitext
		}
	})
end

function util.trackAndWarn(trackingCat, warning)
	local title = mw.title.getCurrentTitle()
	local results = title and title.namespace == 0 and trackingCat and '[[Category:'..trackingCat..']]' or ''
	if warning then
		local warn = require('Module:If preview')._warning
		results = results .. warn({warning})
	end
	return results
end

function util.ternary(flag, other)
	other = other or 'other'
	flag = flag == 'none' and 'no' or flag
	local yesNoOut = yesno(flag, other)
	local yes = (yesNoOut == true)
	local no = (yesNoOut == false)
	return yes, no
end

-- True if value looks like an OSM relation id or a Commons Data page, not a Q-id
function util.isLocalGeoId(val)
	if not val or val == '' then return false end
	val = mw.text.trim(val)
	if mw.ustring.match(val, '^[Qq]%d+$') then return false end
	return true
end

local p = {}

p._caption = function(args)
	if args.caption then
		return args.caption
	elseif args.switcher then
		return util.noop("no caption or switcher")
	end
	return util.noop("no caption (Wikidata labels disabled)")
end

local tinyType = {
	landmark = true,
	railwaystation = true,
	edu = true,
	pass = true,
	camera = true
}

p._main = function(_config)
	local tracking = ''
	local config = util.trimArgs(_config)

	config.coord = config.coord or config.coordinates

	-- Local geo sources only (OSM relation number or Commons Data title)
	local osmId = config.osm or config["osm-id"] or config.relation
	local fromPage = config.from
	if config.id and util.isLocalGeoId(config.id) and not osmId then
		osmId = config.id
	end
	local hasLocalGeo = util.isLocalGeoId(osmId) or (fromPage and fromPage ~= '')

	if not config.coord and not hasLocalGeo and not config.custom then
		return false, util.trackAndWarn('Pages using infobox mapframe with missing coordinates')
	end

	local args = {}
	args.display = "inline"
	args.frame = "yes"
	args.plain = "yes"
	args["frame-width"]  = config["frame-width"] or config.width or DEFAULT_FRAME_WIDTH
	args["frame-height"] = config["frame-height"] or config.height or DEFAULT_FRAME_HEIGHT
	args["frame-align"]  = "center"
	args["frame-coord"] = config["frame-coordinates"] or config["frame-coord"]
	args["frame-lat"]    = config["frame-lat"] or config["frame-latitude"]
	args["frame-long"]   = config["frame-long"] or config["frame-longitude"]

	local zoom = config.zoom
	if not zoom then
		local getZoom = require('Module:Infobox dim')._zoom
		zoom = getZoom({
			length_km = config.length_km, length_mi = config.length_mi,
			width_km = config.width_km, width_mi = config.width_mi,
			area_km2 = config.area_km2, area_mi2 = config.area_mi2,
			area_ha = config.area_ha, area_acre = config.area_acre,
			type = config.type, population = config.population,
			viewport_px = math.min(tonumber(args["frame-width"]) or 270, tonumber(args["frame-height"]) or 200)
		})
	end
	args.zoom = zoom or DEFAULT_ZOOM

	local forcePoint, suppressPoint = util.ternary(config.point)
	local forceMarker, suppressMarker = util.ternary(config.marker, true)
	forcePoint = forcePoint or forceMarker
	suppressPoint = suppressPoint or suppressMarker
	local showMarker = not suppressPoint and (forcePoint or config.coord or not hasLocalGeo)

	local forceGeo, suppressGeo = util.ternary(config.wikidata)
	-- "wikidata=" kept as a compatibility alias meaning "show shape/line from local geo ids"
	local showShape = not suppressGeo and (forceGeo or hasLocalGeo)
	local showLine = showShape

	local forceShape, suppressShape = util.ternary(config.shape)
	showShape = not suppressShape and (forceShape or showShape)
	local shapeType = config.shape == 'inverse' and 'shape-inverse' or 'shape'

	local forceLine, suppressLine = util.ternary(config.line)
	showLine = not suppressLine and (forceLine or showLine)

	local maskItem
	if config.switcher == "zooms" then
		local maxZoom = math.max(tonumber(args.zoom) or DEFAULT_ZOOM, 3)
		local minZoom = 1
		local midZoom = math.floor((maxZoom + minZoom) / 2)
		args.switch = "zoomed in, zoomed midway, zoomed out"
		args.zoom = string.format("SWITCH:%d,%d,%d", maxZoom, midZoom, minZoom)
	elseif config.switcher == "geomasks" and config.geomask then
		args.zoom = nil
		local separator = (mw.ustring.find(config.geomask, "###", 0, true) and "###")
			or (mw.ustring.find(config.geomask, ";", 0, true) and ";")
			or ","
		local pattern = "%s*" .. separator .. "%s*"
		local maskItems = mw.text.split(mw.ustring.gsub(config.geomask, "SWITCH:", ""), pattern)
		local maskLabels = {}
		if config["geomask-labels"] then
			maskLabels = mw.text.split(config["geomask-labels"], "%s*###%s*")
		end
		if #maskItems > 1 then
			if #maskLabels ~= #maskItems then
				maskLabels = {}
				for i, item in ipairs(maskItems) do
					table.insert(maskLabels, item)
				end
			end
			args.switch = table.concat(maskLabels, "###")
			maskItem = "SWITCH:" .. table.concat(maskItems, ",")
		elseif #maskItems == 1 then
			maskItem = maskItems[1]
		end
	elseif config.geomask and util.isLocalGeoId(config.geomask) then
		maskItem = config.geomask
	end

	if (showLine or showShape) and hasLocalGeo and not maskItem
		and not config.zoom and not (config.type and tinyType[config.type]) then
		args.zoom = nil
	end

	if not maskItem and not showShape and not showLine and not showMarker and not config.coord then
		return false, util.trackAndWarn('Pages using infobox mapframe with no geometry', 'No geometry specified for mapframe')
	end

	local argNumber = ''
	local function incrementArgNumber()
		if argNumber == '' then
			argNumber = 2
		else
			argNumber = argNumber + 1
		end
	end

	if maskItem then
		args["type" .. argNumber] = "shape-inverse"
		if mw.ustring.match(tostring(maskItem), '^SWITCH:') or tonumber(maskItem) then
			args["id" .. argNumber] = maskItem
		else
			args["from" .. argNumber] = maskItem
		end
		args["stroke-width" .. argNumber] = config["geomask-stroke-width"] or DEFAULT_GEOMASK_STROKE_WIDTH
		args["stroke-color" .. argNumber] = config["geomask-stroke-color"] or config["geomask-stroke-colour"] or DEFAULT_GEOMASK_STROKE_COLOR
		args["fill" .. argNumber] = config["geomask-fill"] or DEFAULT_GEOMASK_FILL
		args["fill-opacity" .. argNumber] = config["geomask-fill-opacity"] or DEFAULT_SHAPE_FILL_OPACITY
		if not config.zoom and not config.switcher then
			args.zoom = nil
			args["frame-coord"] = nil
			args["frame-lat"] = nil
			args["frame-long"] = nil
		end
		incrementArgNumber()
		if not args.zoom then
			args["type" .. argNumber] = "line"
			if mw.ustring.match(tostring(maskItem), '^SWITCH:') or tonumber(maskItem) then
				args["id" .. argNumber] = maskItem
			else
				args["from" .. argNumber] = maskItem
			end
			args["stroke-width" .. argNumber] = 0
			incrementArgNumber()
		end
	end

	if showShape and hasLocalGeo then
		args["type" .. argNumber] = shapeType
		if fromPage then
			args["from" .. argNumber] = fromPage
		elseif osmId then
			args["id" .. argNumber] = osmId
		end
		args["stroke-width" .. argNumber] = config["shape-stroke-width"] or config["stroke-width"] or DEFAULT_SHAPE_STROKE_WIDTH
		args["stroke-color" .. argNumber] = config["shape-stroke-color"] or config["shape-stroke-colour"] or config["stroke-color"] or config["stroke-colour"] or DEFAULT_SHAPE_STROKE_COLOR
		args["fill" .. argNumber] = config["shape-fill"] or DEFAULT_SHAPE_FILL
		args["fill-opacity" .. argNumber] = config["shape-fill-opacity"] or DEFAULT_SHAPE_FILL_OPACITY
		incrementArgNumber()
	end

	if showLine and hasLocalGeo then
		args["type" .. argNumber] = "line"
		if fromPage then
			args["from" .. argNumber] = fromPage
		elseif osmId then
			args["id" .. argNumber] = osmId
		end
		args["stroke-width" .. argNumber] = config["line-stroke-width"] or config["stroke-width"] or DEFAULT_LINE_STROKE_WIDTH
		args["stroke-color" .. argNumber] = config["line-stroke-color"] or config["line-stroke-colour"] or config["stroke-color"] or config["stroke-colour"] or DEFAULT_LINE_STROKE_COLOR
		incrementArgNumber()
	end

	if showMarker then
		args["type" .. argNumber] = "point"
		if config.coord then args["coord" .. argNumber] = config.coord end
		if config.marker then args["marker" .. argNumber] = config.marker end
		args["marker-color" .. argNumber] = config["marker-color"] or config["marker-colour"] or DEFAULT_MARKER_COLOR
		incrementArgNumber()
	end

	if not maskItem and not showMarker and not hasLocalGeo then
		if config.coord then
			args["frame-coord"] = args["frame-coord"] or config.coord
		end
		tracking = tracking .. util.trackAndWarn('Pages using infobox mapframe with forced centering')
	end

	args["frame-coord"] = args["frame-coord"] or ""
	args["frame-lat"] = args["frame-lat"] or ""
	args["frame-long"] = args["frame-long"] or ""

	local mapframe = args.switch and mf.multi(args) or mf._main(args)
	return true, mapframe .. tracking
end

p.main = function(frame)
	local parent = frame.getParent(frame)
	local _, mapframe = p._main(parent.args)
	return frame:preprocess(mapframe)
end

p.auto = function(frame)
	if not util.shouldAutoRun(frame) then
		return util.noop("auto should not autorun")
	end
	local args = util.argsFromAuto(frame)
	if args.custom then
		return frame:preprocess(util.parseCustomWikitext(args.custom))
	end
	local _, mapframe = p._main(args)
	return frame:preprocess(mapframe)
end

p.autocaption = function(frame)
	if not util.shouldAutoRun(frame) then
		return util.noop("autocaption should not autorun")
	end
	local args = util.argsFromAuto(frame)
	return p._caption(args)
end

p.autoWithCaption = function(frame)
	if not util.shouldAutoRun(frame) then
		return util.noop("autoWithCaption should not autorun")
	end
	local args = util.argsFromAuto(frame)
	local wikitext
	local ok
	if args.custom then
		ok = true
		wikitext = util.parseCustomWikitext(args.custom)
	else
		ok, wikitext = p._main(args)
	end
	if not ok then return wikitext end
	wikitext = frame:preprocess(wikitext)
	local caption = p._caption(args)
	local data = mw.html.create():wikitext(wikitext)
	data:tag('div')
		:addClass('infobox-caption')
		:cssText(args.captionstyle)
		:wikitext(caption)
	return tostring(data)
end

return p