Module:ItemDistribution

local CargoUtil = require 'Module:CargoUtil' local Datetime = require 'Module:DatetimeUtil' local escq = require 'Module:EscQ'.main1 local List = require 'Module:ListUtil' local Hash = require 'Module:HashUtil'

local DATA = mw.loadData 'Module:ItemDistribution/data' local REWARD_SOURCES = DATA.sources local SOURCES_ORDER = DATA.sourceOrder local ITEM_KINDS = DATA.kinds

local monthRanges = function (kind, y)	local months = 12 local having = ("DATE_FORMAT(t0,'%%Y')='%d'"):format(y) local sinceTable = os.date("*t", kind.since.month)

local o = {} function o:cellCount return months end function o:hasCell (i) return y > sinceTable.year or i >= sinceTable.month --return os.time {year = y, month = i, day = 1, hour = 0} >= kind.since.month end function o:headerText (i) return Datetime.MONTHS_SHORT[i] end function o:havingClause return having end return o end

local dayRanges = function (kind, y, m)	local days = os.difftime(os.time {year = y, month = m + 1, day = 1, hour = 0}, os.time {year = y, month = m, day = 1, hour = 0}) / 86400 local having = ("DATE_FORMAT(t0,'%%Y-%%m')='%04d-%02d'"):format(y, m)	local sinceTable = os.date("*t", kind.since.day)

local o = {} function o:cellCount return days end function o:hasCell (i) return y > sinceTable.year or m > sinceTable.month or i >= sinceTable.day --return os.time {year = y, month = m, day = i, hour = 0} >= kind.since.day end function o:headerText (i) return ('%02d'):format(i) end function o:havingClause return having end return o end

local makeRange = function (itemKind, args, frame) if tonumber(args.year) then return monthRanges(itemKind, tonumber(args.year)) end

if args.ym then local ym = frame:callParserFunction('#time', 'Y-m', args.ym) local y, m = mw.ustring.match(ym, '^([^-]+)%-([^-]+)$') y, m = tonumber(y), tonumber(m) if y and m then return dayRanges(itemKind, y, m)		end end end

local headerRow = function (args, frame) local itemKind = ITEM_KINDS[args.kind] if not itemKind then return require 'Module:Error'.error('TODO: support other item kinds') end

local ranges = makeRange(itemKind, args, frame) if not ranges then return require 'Module:Error'.error('Missing "year" or "ym" argument.') end

local tr = mw.html.create('tr') tr:tag('th'):wikitext('Reason') for i = 1, ranges:cellCount do		if ranges:hasCell(i) then tr:tag('th'):wikitext(ranges:headerText(i)) end end tr:tag('th'):attr('rowspan', 0) tr:tag('th'):wikitext('Total') return tostring(tr) end

local separatorRow = function (args, frame) local itemKind = ITEM_KINDS[args.kind] if not itemKind then return require 'Module:Error'.error('TODO: support other item kinds') end

local ranges = makeRange(itemKind, args, frame) if not ranges then return require 'Module:Error'.error('Missing "year" or "ym" argument.') end

local actualCellCount = 0 -- TODO: refactor for i = 1, ranges:cellCount do		if ranges:hasCell(i) then actualCellCount = actualCellCount + 1 end end local tr = mw.html.create('tr') tr:tag('th'):attr('colspan', actualCellCount + 1) tr:tag('th') return tostring(tr) end

local itemQuery = function (rewardSource, itemKind, ranges, args, frame) local cargoTables = {('%s=R'):format(rewardSource.rewardTable)} local cargoJoin = {} local cargoWhere = {itemKind.cond, rewardSource.cond} local xfield = 'R.StartTime'

if rewardSource.eventTable then cargoTables[#cargoTables + 1] = ('%s=EV'):format(rewardSource.eventTable) cargoJoin[#cargoJoin + 1] = rewardSource.useWikiName and 'R.WikiName=EV.WikiName' or 'R._pageName=EV._pageName' if rewardSource.timeTable then cargoTables[#cargoTables + 1] = ('%s=T'):format(rewardSource.timeTable) cargoJoin[#cargoJoin + 1] = rewardSource.useWikiName and 'EV.WikiName=T.WikiName' or 'EV._pageName=T._pageName' xfield = 'IFNULL(R.StartTime,T.StartTime)' else xfield = 'IFNULL(R.StartTime,EV.StartTime)' end end

local rewards = CargoUtil.full_query(table.concat(cargoTables, ','), ("R.Amount=count,MIN(%s)=t0"):format(xfield), {		join = #cargoJoin > 0 and table.concat(cargoJoin, ',') or nil,		where = table.concat(List.map(cargoWhere, function (v) return '(' .. v .. ')' end), ' AND '),		groupBy = 'R._ID',		having = ranges:havingClause,	}) for _, v in ipairs(rewards) do		v.t0 = os.date('!*t', Datetime.from_cargo(v.t0)) end local rewardsByIndex = List.group_by(rewards, function (v)		return v.t0[args.year and 'month' or 'day']	end) return Hash.map(rewardsByIndex, function (vs) return List.sum(List.map(vs, function (v) return tonumber(v.count) end)) end) end

local itemTableRow = function (args, frame) local itemKind = ITEM_KINDS[args.kind] if not itemKind then return require 'Module:Error'.error('TODO: support other item kinds') end local ranges = makeRange(itemKind, args, frame) if not ranges then return require 'Module:Error'.error('Missing "year" or "ym" argument.') end

local rewardTotals = {} if args.reason then local rewardSource = REWARD_SOURCES[args.reason] if not rewardSource then return require 'Module:Error'.error('TODO: support other reward sources') end rewardTotals = itemQuery(rewardSource, itemKind, ranges, args, frame) else for _, rewardSource in pairs(REWARD_SOURCES) do			Hash.merge_self(rewardTotals, itemQuery(rewardSource, itemKind, ranges, args, frame), function (v1, v2) return v1 + v2 end) end end

local grandTotal = List.sum(Hash.values(rewardTotals))

local tr = mw.html.create('tr') tr:tag('td'):wikitext(args.title and args.title or args.reason and ( .. args.reason .. ) or 'Total') for i = 1, ranges:cellCount do		if ranges:hasCell(i) then local td = tr:tag('td') if rewardTotals[i] then td:wikitext(rewardTotals[i]) elseif args.year then td:wikitext(0) end end end tr:tag('td'):tag('b'):wikitext(grandTotal)

if grandTotal == 0 then tr:css('display', 'none') end return tostring(tr) end

local itemQueryDetail = function (rewardSource, itemKind, year, month, args, frame) local cargoTables = {('%s=R'):format(rewardSource.rewardTable)} local cargoJoin = {} local cargoWhere = {itemKind.cond, rewardSource.cond} local timefield = 'R.StartTime' local cargoField = rewardSource.descField or "CONCAT(, R._pageName, )"

if rewardSource.eventTable then cargoTables[#cargoTables + 1] = ('%s=EV'):format(rewardSource.eventTable) cargoJoin[#cargoJoin + 1] = rewardSource.useWikiName and 'R.WikiName=EV.WikiName' or 'R._pageName=EV._pageName' if rewardSource.timeTable then cargoTables[#cargoTables + 1] = ('%s=T'):format(rewardSource.timeTable) cargoJoin[#cargoJoin + 1] = rewardSource.useWikiName and 'EV.WikiName=T.WikiName' or 'EV._pageName=T._pageName' timefield = 'IFNULL(R.StartTime,T.StartTime)' else timefield = 'IFNULL(R.StartTime,EV.StartTime)' end end

local rewards = CargoUtil.full_query(table.concat(cargoTables, ','), ("%s=field,MIN(%s)=t0"):format(cargoField, timefield), {		join = #cargoJoin > 0 and table.concat(cargoJoin, ',') or nil,		where = table.concat(List.map(cargoWhere, function (v) return '(' .. v .. ')' end), ' AND '),		orderBy = 't0,R._ID',		groupBy = 'R._ID',		having = dayRanges(itemKind, year, month):havingClause,	})

rewards = Hash.map(List.group_by(rewards, function (v) return v.field end), function (vs)		return List.uniq(List.reduce(vs, function (l, v)			l[#l + 1] = string.sub(v.t0, 1, 10)			return l		end, {}))	end) rewards = List.zip(Hash.keys(rewards), Hash.values(rewards), function (key, value)		return { field = key, times = value }	end) table.sort(rewards, function (v1, v2)		local reducer = function (s, t) return s .. ',' .. t end		return List.reduce(v1.times, reducer, ) < List.reduce(v2.times, reducer, )	end)

return List.map(rewards, function (v)		v.times = '[' .. List.reduce(v.times, function (s, v1) if #s == 0 then s = Datetime.MONTHS_SHORT[month] .. ' '			else s = s .. ', '			end return s .. tostring(tonumber(string.sub(v1, 9, 10))) end, '') .. ']'		return v	end) end

local detailItem; detailItem = function (args, frame) local itemKind = ITEM_KINDS[args.kind] if not itemKind then return require 'Module:Error'.error('TODO: support other item kinds') end if not args.ym then return require 'Module:Error'.error('Missing "ym" argument.') end local ym = frame:callParserFunction('#time', 'Y-m', args.ym) local y, m = mw.ustring.match(ym, '^([^-]+)%-([^-]+)$') y, m = tonumber(y), tonumber(m)

local rewardTotals = {} if args.reason then local rewardSource = REWARD_SOURCES[args.reason] if not rewardSource then return require 'Module:Error'.error('TODO: support other reward sources') end rewardTotals = itemQueryDetail(rewardSource, itemKind, y, m, args, frame) else rows = {} for _, reason in ipairs(SOURCES_ORDER) do			if REWARD_SOURCES[reason] then args.reason = reason rows[#rows + 1] = detailItem(args, frame) end end return '' .. (List.reduce(rows, function (s1, s2) return s1 and s2 and (s1 .. '\n' .. s2) or s1 or s2 end) or '') .. '' end

if #rewardTotals ~= 0 then return List.reduce(rewardTotals, function (s, v)			if string.sub(s, -2, -2) == ':' then				return s .. v.times .. ' ' .. v.field			else				return s .. ', ' .. v.times .. ' ' .. v.field			end		end, '' .. args.reason .. ': ') .. '.' end end

return require 'Module:MakeMWModule'.makeMWModule { headerRow = headerRow, separatorRow = separatorRow, itemTableRow = itemTableRow, detailItem = detailItem, }