--- Example usage:
--- {{#invoke:Medical cases chart/data|externalData|page=COVID-19 cases in Santa Clara County, California.tab|recoveries=hospitalized|cases=totalConfirmedCases}}
--- =p._externalData({datapage="COVID-19 cases in Santa Clara County, California.tab",datarecoveries="hospitalized",datacases="totalConfirmedCases"})

local p = {}
local lang = mw.getContentLanguage()
local english = mw.getLanguage("en")

local function round(x)
	return (math.modf(x + (x < 0 and -0.5 or 0.5)))
end

local function formatChange(previous, current)
	if not previous or previous == 0 then
		return
	end
	if previous == current then
		return "="
	end
	
	local change = current / previous * 100 - 100
	local sign = change < 0 and "−" or "+"
	return mw.ustring.format("%s%s%%", sign, lang:formatNum(round(math.abs(change))))
end

function p._externalData(args)
	local data = mw.ext.data.get(args.datapage)
	
	local dateIndex
	local deathsIndex
	local recoveriesIndex
	local casesIndex
	local class4Index
	local class5Index
	for i, field in ipairs(data.schema.fields) do
		if field.name == "date" or field.name == args.datadate then
			dateIndex = i
		elseif field.name == "deaths" or field.name == args.datadeaths then
			deathsIndex = i
		elseif field.name == "recoveries" or field.name == args.datarecoveries then
			recoveriesIndex = i
		elseif field.name == "cases" or field.name == args.datacases then
			casesIndex = i
		elseif field.name == "class4" or field.name == args.dataclass4 then
			class4Index = i
		elseif field.name == "class5" or field.name == args.dataclass5 then
			class5Index = i
		end
	end
	assert(dateIndex, "Date field not found.")
	assert(deathsIndex or not args.datadeaths, "Deaths field not found.")
	assert(recoveriesIndex or not args.datarecoveries, "Recoveries field not found.")
	assert(casesIndex or not args.datacases, "Cases field not found.")
	assert(class4Index or not args.dataclass4, "Class 4 field not found.")
	assert(class5Index or not args.dataclass5, "Class 5 field not found.")
	
	-- Restructure the data as tables with keys.
	local records = {}
	for i, row in ipairs(data.data) do
		local record = {
			date = row[dateIndex],
			deaths = deathsIndex and row[deathsIndex],
			recoveries = recoveriesIndex and row[recoveriesIndex],
			cases = casesIndex and row[casesIndex],
			class4 = class4Index and row[class4Index],
			class5 = class5Index and row[class5Index],
			options = {},
			streak = 1,
		}
		local prevRecord = records[#records] or {}
		if casesIndex and not prevRecord.cases and record.cases and record.cases > 0 then
			record.options.firstright1 = "y"
		end
		if deathsIndex and prevRecord.deaths == 0 and record.deaths and record.deaths > 0 then
			record.options.firstright2 = "y"
		end
		if deathsIndex and (prevRecord.deaths or prevRecord.assumedDeaths) and not record.deaths then
			record.assumedDeaths = prevRecord.deaths or prevRecord.assumedDeaths
		end
		if casesIndex and (prevRecord.cases or prevRecord.assumedCases) and not record.cases then
			record.assumedCases = prevRecord.cases or prevRecord.assumedCases
		end
		if record.deaths == prevRecord.deaths
			and record.recoveries == prevRecord.recoveries
			and record.cases == prevRecord.cases
			and record.class4 == prevRecord.class4
			and record.class5 == prevRecord.class5 then
			record.streak = prevRecord.streak + 1
		end
		table.insert(records, record)
	end
	
	-- Collapse streaks of identical data.
	for i = #records, 1, -1 do
		local record = records[i]
		if record.streak > 3 then
			for j = i, i - record.streak + 3, -1 do
				table.remove(records, j)
			end
			i = i - record.streak + 2
			record = records[i]
			record.options.collapse = "y"
			record.options.id = english:formatDate("M", record.date):lower()
			record.date = nil
		end
	end
	
	-- Stringify the data.
	local rows = {}
	for i, record in ipairs(records) do
		local prevRecord = records[i - 1] or {}
		local row = {
			record.date or "",
			tostring(record.deaths or record.assumedDeaths or ""),
			tostring(record.recoveries or ""),
			tostring(record.cases or record.assumedCases or ""),
			tostring(record.class4 or ""),
			tostring(record.class5 or ""),
			record.cases and lang:formatNum(record.cases) or "",
			record.cases and formatChange(prevRecord.cases or prevRecord.assumedCases, record.cases) or "",
			record.deaths and lang:formatNum(record.deaths) or "",
			record.deaths and formatChange(prevRecord.deaths or prevRecord.assumedDeaths, record.deaths) or "",
		}
		for k, v in pairs(record.options) do
			table.insert(row, string.format("%s=%s", k, v))
		end
		table.insert(rows, table.concat(row, ";"))
	end
	return table.concat(rows, "\n")
end

function p.externalData(frame)
	local args = {}
	for k,v in pairs(frame.args) do
		if (v or '') ~= '' then
			args['data'..k] = v
		end
	end
	return p._externalData(args)
end
return p