-- Load dependencies.
local htmlBuilder = require( 'Module:HtmlBuilder' )
local makeNavbar = require( 'Module:Navbar' ).navbar
local numToRoman = require( 'Module:Roman' ).main
local numToArmenian = require( 'Module:Armenian' ).main
local getRegnal = require( 'Module:British regnal year' ).main
local japaneseEra = require( 'Module:Japanese calendar' ).era

-- Define constants.
local lang = mw.language.getContentLanguage()
local currentYear = tonumber( lang:formatDate( 'Y' ) )

--------------------------------------------------------------------
-- Helper functions
--------------------------------------------------------------------

local function isInteger( num )
    num = tonumber( num )
    if num and math.floor( num ) == num and num ~= math.huge then
        return num
    else
        return false
    end
end

-- Converts strings of the format "n BC" to their corresponding
-- numerical values.
local function BCToNum( s )
    if type( s ) ~= 'string' then
        return nil
    end
    s = mw.ustring.match( s, '^([1-9]%d*)%s*BC$' )
    if not s then
        return nil
    end
    local num = tonumber( s )
    num = ( num - 1 ) * -1
    return num
end

-- For BC years, returns a string with the year name appended with " BC".
-- Otherwise returns the year number.
local function numToBC( num )
    if type( num ) ~= 'number' then
        error( mw.ustring.format( 'Non-number input %s passed to numToBC', tostring( num ) ) )
    end
    if num <= 0 then
        num = -1 * ( num - 1 )
        local BCYear = mw.ustring.format( '%d BC', num )
        return BCYear
    else
        return num
    end
end

-- Gets the display text for a Gregorian year. If gregcal is specified, returns a wikilink.
local function getGregLink( year, gregcal )
    year = tonumber( year )
    if not year then return end
    local yearName = tostring( numToBC( year ) )
    if type( gregcal ) == 'string' and gregcal ~= '' then
        return mw.ustring.format( '[[%s|%s]]', gregcal, yearName )
    else
        return tostring( yearName )
    end
end

-- Get the year text for a Gregorian year. Used in both the Gregorian and Julian calendars.
local function getGregYearText( year, gregcal )
    local link = getGregLink( year, gregcal )
    local ret = htmlBuilder.create()
    ret
        .wikitext( link )
    local romanYear = numToRoman{ year }
    if romanYear then
        ret
            .tag( 'br', { selfClosing = true } )
            .tag( 'span' )
                .css( 'font-family', 'serif' )
                .wikitext( mw.ustring.format( "''%s''", romanYear ) )
    end
    return tostring( ret )
end

--------------------------------------------------------------------
-- Calendar box class definition
--------------------------------------------------------------------

local calendarBox = {}
calendarBox.__index = calendarBox

function calendarBox:new( init )
    init = type( init ) == 'table' and init or {}
    local obj = {}
    
    local pagename = mw.title.getCurrentTitle().text
    
    -- Set the year. If the year is specified as an argument, use that.
    -- Otherwise, use the page name if it is valid. If the pagename isn't
    -- valid, use the current year.
    local initYear = isInteger( init.year )
    if initYear then
        self.year = initYear
    else
        local BCYear = BCToNum( pagename )
        if BCYear then
            self.year = BCYear
        else
            local pagenameNum = isInteger( pagename )
            if pagenameNum then
                self.year = pagenameNum
            else
                self.year = currentYear
            end
        end
    end
    
    -- Set the caption.
    local BCYearName = numToBC( self.year )
    if type( BCYearName ) == 'string' then
        self.caption = BCYearName
    elseif type( BCYearName ) == 'number' then
        self.caption = tostring( BCYearName )
    else
        error( mw.ustring.format( 'Invalid caption "%s" detected', tostring( BCYearName ) ) )
    end
        
    -- Set other fields.
    self.footnotes = init.footnotes
    self.navbar = init.navbar
    
    return setmetatable( obj, {
        __index = self
    })
end    

function calendarBox:addCalendar( obj )
    if not ( type( obj ) == 'table' ) then
        error( 'invalid calendar object "' .. obj .. '" passed to addCalendar' )
    end
    self.calendars = self.calendars or {}
    table.insert( self.calendars, obj )
end

-- We add calendar groups in exactly the same way that we add calendars. It is the export()
-- function that does the work of figuring out which object is which type.
calendarBox.addCalendarGroup = calendarBox.addCalendar

function calendarBox:export()
    local root = htmlBuilder.create( 'table' )
    -- Export the calendar box headers.
    root
        .addClass( 'infobox' )
        .addClass( 'vevent' )
        .css( 'width', '22em' )
        .tag( 'caption' )
            .css( 'font-size', '125%' )
            .tag( 'span' )
                .addClass( 'summary' )
                .addClass( 'dtstart' )
                .wikitext( self.caption )

    -- Define a function to export one calendar object.
    local function exportCalendar( calendar, group )
        if type( calendar ) ~= 'table' or not calendar.getLink or not calendar.calculateYear then return end
        
        -- Process the link.
        local link = calendar.getLink( self.year )
        if type( link ) ~= 'string' then return end -- Exit if no link was specified, for example Unix times before 1970.
        link = ( group and '&nbsp;- ' or '' ) .. link
        
        -- Process the year.
        local yearText
        local year1, year2 = calendar.calculateYear( self.year )
        if year1 and year2 then
            if type( year1 ) == 'number' and type( year2 ) == 'number' and ( year1 < 0 or year2 < 0 ) then -- Leave a gap for negative years to avoid having a minus sign and a dash right next to each other.
                year1 = tostring( year1 ) .. ' '
                year2 = ' ' .. tostring( year2 )
            else
                year1 = tostring( year1 )
                year2 = tostring( year2 )
            end
            yearText = mw.ustring.format( '%s–%s', year1, year2 )
        else
            yearText = year1 and tostring( year1 ) or "''N/A''" -- Automatically return N/A if year1 returns nil or false.
        end
        
        -- Build the table row.
        root
            .tag( 'tr' )
                .tag( 'td' )
                    .wikitext( link )
                    .done()
                .tag( 'td' )
                    .wikitext( yearText )
                    .allDone()
    end

    -- Check whether the objects in self.calendars are calendar objects or calendar group objects,
    -- and export them as appropriate.
    for i, t in ipairs( self.calendars ) do
        if type( t.calendars ) == 'table' then
            -- We are dealing with a calendar group object.
            if type( t.heading ) == 'string' then
                -- Display the heading if it is present.
                root
                    .tag( 'tr' )
                        .tag( 'td' )
                            .wikitext( t.heading )
                            .done()
                        .tag( 'td' ) -- Use a blank tag to make the html look nice.
                            .allDone()
            end
            for j, calendar in ipairs( t.calendars ) do
                exportCalendar( calendar, true )
            end
        else
            -- We are dealing with a calendar object.
            exportCalendar( t )
        end
    end
    
    -- Add footnotes.
    if type( self.footnotes ) == 'string' then
        root
            .tag( 'tr' )
                .tag( 'td' )
                    .attr( 'colspan', '2' )
                    .wikitext( mw.ustring.format( '<small>%s</small>', self.footnotes ) )
                    .allDone()
    end
    
    -- Add navbar.
    if type( self.navbar ) == 'string' then
        root
            .tag( 'tr' )
                .tag( 'td' )
                    .attr( 'colspan', '2' )
                    .css( 'text-align', 'center' )
                    .wikitext( makeNavbar{ self.navbar } )
                    .allDone()
    end
    
    return tostring( root )
end

--------------------------------------------------------------------
-- Calendar group class definition
--------------------------------------------------------------------

--  Calendar groups are used to group different calendars together. 
--  Previously, the template did this by including a table row with
--  no year value. By using objects we can do the same thing more
--  semantically.

local calendarGroup = {}
calendarGroup.__index = calendarGroup
calendarGroup.type = 'calendarGroup'

function calendarGroup:new( init )
    init = type( init ) == 'table' and init or {}
    local obj = {}
    obj.heading = init.heading
    obj.calendars = {}
    if type( init.calendars ) == 'table' then
        for i, v in ipairs( init.calendars ) do
            if type( v ) == 'table' then
                table.insert( obj.calendars, v )
            end
        end
    end
    return setmetatable( obj, {
        __index = self
    })
end

function calendarGroup:addCalendar( calendar )
    self.calendars = self.calendars or {}
    if type( calendar ) == 'table' then
        table.insert( self.calendars, calendar )
    end
end

--------------------------------------------------------------------
-- Calendar class definition
--------------------------------------------------------------------

local calendar = {}
calendar.__index = calendar
calendar.type = 'calendar'

function calendar:new()
    local obj = {}
    return setmetatable( obj, {
        __index = self
    })
end

function calendar:setLink( link, display, italics )
    if not type( link ) == 'string' then
        return nil
    end
    display = type( display ) == 'string' and display 
    local ret
    if display then
        ret = mw.ustring.format( '[[%s|%s]]', link, display )
    else
        ret = mw.ustring.format( '[[%s]]', link )
    end
    if italics then
        ret = mw.ustring.format( "''%s''", ret )
    end
    self.getLink = function()
        return ret
    end
end

function calendar:setLinkFunction( func )
    self.getLink = func
end

-- This function sets the year. It can return one value for a single year, or
-- two values to specify a year range.
function calendar:setYearFunction( func )
    self.calculateYear = func
end

--------------------------------------------------------------------
-- Build the box
--------------------------------------------------------------------

local function _main( args )
    args.navbar = 'Year in other calendars'
    local box = calendarBox:new( args )

    -- Gregorian calendar

    local gregorian = calendar:new()
    gregorian:setLink( 'Gregorian calendar' )
    gregorian:setYearFunction(
        function( year )
            return getGregYearText( year, args.gregcal )
        end
    )
    box:addCalendar( gregorian )
            
    -- Ab urbe condita
    
    local abUrbe = calendar:new()
    abUrbe:setLink( 'Ab urbe condita' )
    abUrbe:setYearFunction(
        function( year )
            return year + 753
        end
    )
    box:addCalendar( abUrbe )

    -- Armenian calendar
    
    local armenian = calendar:new()
    armenian:setLink( 'Armenian calendar' )
    armenian:setYearFunction(
        function( year )
            if year - 550 > 0 then
                local armenianYear = year - 551
                return mw.ustring.format( '%s<br />ԹՎ %s', armenianYear, numToArmenian( armenianYear ) )
            end
        end
    )
    box:addCalendar( armenian )

    -- Assyrian calendar

    local assyrian = calendar:new()
    assyrian:setLink( 'Assyrian calendar' )
    assyrian:setYearFunction(
        function( year )
            return year + 4750
        end
    )
    box:addCalendar( assyrian )

    -- Bahá'í calendar

    local bahai = calendar:new()
    bahai:setLink( "Bahá'í calendar" )
    bahai:setYearFunction(
        function( year )
            return year - 1844, year - 1843
        end
    )
    box:addCalendar( bahai )

    -- Bengali calendar
    
    local bengali = calendar:new()
    bengali:setLink( 'Bengali calendar' )
    bengali:setYearFunction(
        function( year )
            return year - 593
        end
    )
    box:addCalendar( bengali )

    -- Berber calendar
    
    local berber = calendar:new()
    berber:setLink( 'Berber calendar' )
    berber:setYearFunction(
        function( year )
            return year + 950
        end
    )
    box:addCalendar( berber )

    -- Regnal year
    
    local regnal = calendar:new()
    regnal:setLinkFunction(
        function( year )
            local regnalName
            if year > 1706 then
                regnalName = 'British'
            else
                regnalName = 'English'
            end
            return mw.ustring.format( '[[Regnal years of English monarchs|%s Regnal year]]', regnalName )
        end
    )
    regnal:setYearFunction(
        function( year )
            return getRegnal( year )
        end
    )
    box:addCalendar( regnal )

    -- Buddhist calendar

    local buddhist = calendar:new()
    buddhist:setLink( 'Buddhist calendar' )
    buddhist:setYearFunction(
        function( year )
            return year + 544
        end
    )
    box:addCalendar( buddhist )

    -- Burmese calendar

    local burmese = calendar:new()
    burmese:setLink( 'Traditional Burmese calendar', 'Burmese calendar' )
    burmese:setYearFunction(
        function( year )
            return year - 638
        end
    )
    box:addCalendar( burmese )

    -- Byzantine calendar

    local byzantine = calendar:new()
    byzantine:setLink( 'Byzantine calendar' )
    byzantine:setYearFunction(
        function( year )
            return year + 5508, year + 5509
        end
    )
    box:addCalendar( byzantine )
    
    -- Chinese calendar
    
    local chinese = calendar:new()
    chinese:setLink( 'Chinese calendar' )
    chinese:setYearFunction(
        function( year )
            -- Define the information for the "heavenly stems" and "earthly branches" year cycles.
            -- See [[Chinese calendar#Cycle of years]] for information.
            local heavenlyStems = {
                { '甲', 'Wood' },   -- 1
                { '乙', 'Wood' },   -- 2
                { '丙', 'Fire' },   -- 3
                { '丁', 'Fire' },   -- 4
                { '戊', 'Earth' },  -- 5
                { '己', 'Earth' },  -- 6
                { '庚', 'Metal' },  -- 7
                { '辛', 'Metal' },  -- 8
                { '壬', 'Water' },  -- 9
                { '癸', 'Water' }   -- 10
            }
            local earthlyBranches = {
                { '子', '[[Rat (zodiac)|Rat]]' },           -- 1
                { '丑', '[[Ox (zodiac)|Ox]]' },             -- 2
                { '寅', '[[Tiger (zodiac)|Tiger]]' },       -- 3
                { '卯', '[[Rabbit (zodiac)|Rabbit]]' },     -- 4
                { '辰', '[[Dragon (zodiac)|Dragon]]' },     -- 5
                { '巳', '[[Snake (zodiac)|Snake]]' },       -- 6
                { '午', '[[Horse (zodiac)|Horse]]' },       -- 7
                { '未', '[[Goat (zodiac)|Goat]]' },         -- 8
                { '申', '[[Monkey (zodiac)|Monkey]]' },     -- 9
                { '酉', '[[Rooster (zodiac)|Rooster]]' },   -- 10
                { '戌', '[[Dog (zodiac)|Dog]]' },           -- 11
                { '亥', '[[Pig (zodiac)|Pig]]' }            -- 12
            }
            -- Calculate the cycle numbers from the year. The first sexagenary year corresponds to the ''previous'' year's entry
            -- in [[Chinese calendar correspondence table]], as the Chinese New Year doesn't happen until Jan/Feb in
            -- Gregorian years.
            local sexagenaryYear1 = ( year - 4 ) % 60
            local sexagenaryYear2 = ( year - 3 ) % 60
            local heavenlyNum1 = sexagenaryYear1 % 10
            local heavenlyNum2 = sexagenaryYear2 % 10
            local earthlyNum1 = sexagenaryYear1 % 12
            local earthlyNum2 = sexagenaryYear2 % 12
            -- If the value is 0 increase it by one cycle so that we can use it with Lua arrays.
            if heavenlyNum1 == 0 then
                heavenlyNum1 = 10
            end
            if heavenlyNum2 == 0 then
                heavenlyNum2 = 10
            end
            if earthlyNum1 == 0 then
                earthlyNum1 = 12
            end
            if earthlyNum2 == 0 then
                earthlyNum2 = 12
            end
            -- Get the data tables for each permutation.
            local heavenlyTable1 = heavenlyStems[ heavenlyNum1 ]
            local heavenlyTable2 = heavenlyStems[ heavenlyNum2 ]
            local earthlyTable1 = earthlyBranches[ earthlyNum1 ]
            local earthlyTable2 = earthlyBranches[ earthlyNum2 ]
            -- Work out the continously-numbered year. (See [[Chinese calendar#Continuously numbered years]].)
            local year1 = year + 2696
            local year2 = year + 2697
            local year1Alt = year1 - 60
            local year2Alt = year2 - 60
            -- Return all of that data in a (hopefully) reader-friendly format.
            return mw.ustring.format(
                "[[Sexagenary cycle|%s%s]]年 (%s %s)<br />%d or %d<br />&nbsp;&nbsp;&nbsp;&nbsp;''—&nbsp;to&nbsp;—''<br />%s%s年 (%s %s)<br />%d or %d",
                heavenlyTable1[ 1 ],
                earthlyTable1[ 1 ],
                heavenlyTable1[ 2 ],
                earthlyTable1[ 2 ],
                year1,
                year1Alt,
                heavenlyTable2[ 1 ],
                earthlyTable2[ 1 ],
                heavenlyTable2[ 2 ],
                earthlyTable2[ 2 ],
                year2,
                year2Alt
            )
        end
    )
    box:addCalendar( chinese )
    
    -- Coptic calendar

    local coptic = calendar:new()
    coptic:setLink( 'Coptic calendar' )
    coptic:setYearFunction(
        function( year )
            return year - 284, year - 283
        end
    )
    box:addCalendar( coptic )

    -- Ethiopian calendar

    local ethiopian = calendar:new()
    ethiopian:setLink( 'Ethiopian calendar' )
    ethiopian:setYearFunction(
        function( year )
            return year - 8, year - 7
        end
    )
    box:addCalendar( ethiopian )

    -- Hebrew calendar

    local hebrew = calendar:new()
    hebrew:setLink( 'Hebrew calendar' )
    hebrew:setYearFunction(
        function( year )
            return year + 3760, year + 3761
        end
    )
    box:addCalendar( hebrew )

    -- Hindu calendars

    local hindu = calendarGroup:new{ heading = '[[Hindu calendar]]s' }
    -- Vikram Samvat
    local vikramSamvat = calendar:new()
    vikramSamvat:setLink( 'Vikram Samvat', false, true ) -- use italics
    vikramSamvat:setYearFunction(
        function( year )
            return year + 56, year + 57
        end
    )
    hindu:addCalendar( vikramSamvat )
    -- Shaka Samvat
    local shakaSamvat = calendar:new()
    shakaSamvat:setLink( 'Indian national calendar', 'Shaka Samvat', true ) -- display "Shaka Samvat" and use italics
    shakaSamvat:setYearFunction(
        function( year )
            if year - 76 > 0 then
                return year - 78, year - 77
            end
        end
    )
    hindu:addCalendar( shakaSamvat )
    -- Kali Yuga
    local kaliYuga = calendar:new()
    kaliYuga:setLink( 'Kali Yuga', false, true ) -- use italics
    kaliYuga:setYearFunction(
        function( year )
            return year + 3101, year + 3102
        end
    )
    hindu:addCalendar( kaliYuga )
    box:addCalendarGroup( hindu )

    -- Holocene calendar

    local holocene = calendar:new()
    holocene:setLink( 'Human Era', 'Holocene calendar' )
    holocene:setYearFunction(
        function( year )
            return year + 10000
        end
    )
    box:addCalendar( holocene )

    -- Igbo calendar
    
    -- In the old template this was a calendar group with just one calendar; intentionally adding this as a single
    -- calendar here, as the previous behaviour looked like a mistake.
    local igbo = calendar:new()
    igbo:setLink( 'Igbo calendar' )
    igbo:setYearFunction(
        function( year )
            return year - 1000, year - 999
        end
    )
    box:addCalendar( igbo )

    -- Iranian calendar

    local iranian = calendar:new()
    iranian:setLink( 'Iranian calendars', 'Iranian calendar' )
    iranian:setYearFunction(
        function( year )
            if year - 621 > 0 then
                return mw.ustring.format( '%d–%d', year - 622, year - 621 )
            else
                return mw.ustring.format( '%d BP&nbsp;– %d BP', -(year - 622), -(year - 621) )
            end
        end
    )
    box:addCalendar( iranian )

    -- Islamic calendar

    local islamic = calendar:new()
    islamic:setLink( 'Islamic calendar' )
    islamic:setYearFunction(
        function( year )
            local mult = 1.030684 -- the factor to multiply by
            local sub = 621.5643 -- the factor to subtract by
            if year - 621 > 0 then
                local year1 = math.floor( mult * ( year - sub ) )
                local year2 = math.floor( mult * ( year - sub + 1 ) )
                return mw.ustring.format( '%d–%d', year1, year2 )
            else
                local year1 = math.ceil( -mult * ( year - sub ) )
                local year2 = math.ceil( -mult * ( year - sub + 1 ) )
                return mw.ustring.format( '%d BH&nbsp;– %d BH', year1, year2 )
            end
        end
    )
    box:addCalendar( islamic )

    -- Japanese calendar

    local japanese = calendar:new()
    japanese:setLink( 'Japanese calendar' )
    japanese:setYearFunction(
        function( year )
            local thisEra = japaneseEra:new{ year = year }
            if not thisEra then return end
            local ret = {}
            local oldEra = thisEra:getOldEra()
            if oldEra and oldEra.eraYear and thisEra.article ~= oldEra.article then
                local oldText = mw.ustring.format( '%s %d', oldEra.link, oldEra.eraYear )
                table.insert( ret, oldText )
                table.insert( ret, ' / ' )
            end
            if thisEra.eraYear then
                table.insert( ret, mw.ustring.format( '%s %d', thisEra.link, thisEra.eraYear ) )
            end
            table.insert( ret, mw.ustring.format( '<br /><small>(%s%s年)</small>', thisEra.kanji, thisEra.eraYearKanji ) )
            return table.concat( ret )
        end
    )
    box:addCalendar( japanese )
    
    -- Juche calendar
    
    local juche = calendar:new()
    juche:setLink( 'North Korean calendar', 'Juche calendar' )
    juche:setYearFunction(
        function( year )
            if year > 1911 then
                return year - 1911
            end
        end
    )
    box:addCalendar( juche )
    
    -- Julian calendar
    
    local julian = calendar:new()
    julian:setLink( 'Julian calendar' )
    julian:setYearFunction(
        function( year )
            local yearFuncs = {
                { 1901, function() return 'Gregorian minus 13 days' end },
                { 1900, function() return 'Gregorian minus 12 or 13 days' end },
                { 1801, function() return 'Gregorian minus 12 days' end },
                { 1800, function() return 'Gregorian minus 11 or 12 days' end },
                { 1701, function() return 'Gregorian minus 11 days' end },
                { 1700, function() return 'Gregorian minus 10 or 11 days' end },
                { 1582, function() return 'Gregorian minus 10 days' end },
                { -45, function( year ) return getGregYearText( year, args.gregcal ) end }
            }
            for i, t in ipairs( yearFuncs ) do
                if year >= t[ 1 ] then
                    return t[ 2 ]( year )
                end
            end
        end
    )
    box:addCalendar( julian )
    
    -- Korean calendar
    
    local korean = calendar:new()
    korean:setLink( 'Korean calendar' )
    korean:setYearFunction(
        function( year )
            return year + 2333
        end
    )
    box:addCalendar( korean )
    
    -- Minguo calendar
    
    local minguo = calendar:new()
    minguo:setLink( 'Minguo calendar' )
    minguo:setYearFunction(
        function( year )
            if year > 1911 then
                year = year - 1911
                return mw.ustring.format( '[[Taiwan|ROC]] %d<br /><small>民國%d年</small>', year, year )
            else
                year = 1911 - year + 1
                return mw.ustring.format( '%d before [[Republic of China|ROC]]<br /><small>民前%d年</small>', year, year )
            end
        end
    )
    box:addCalendar( minguo )
    
    -- Thai solar calendar
    
    local thai = calendar:new()
    thai:setLink( 'Thai solar calendar' )
    thai:setYearFunction(
        function( year )
            return year + 543
        end
    )
    box:addCalendar( thai )
    
    -- Unix time
    
    local function getUnixTime( year )
        if year < 1970 then return end
        local noError, unixTime = pcall( lang.formatDate, lang, 'U', '1 Jan ' .. tostring( year ) )
        if not noError or noError and not unixTime then return end
        unixTime = tonumber( unixTime )
        if unixTime and unixTime >= 0 then
            return unixTime
        end
    end
    local unix = calendar:new()
    unix:setLinkFunction(
        function( year )
            local unixTimeThis = getUnixTime( year )
            local unixTimeNext = getUnixTime( year + 1 )
            if unixTimeThis and unixTimeNext then
                return '[[Unix time]]'
            end
        end
    )
    unix:setYearFunction(
        function( year )
            local unixTimeThis = tonumber( getUnixTime( year ) )
            local unixTimeNext = tonumber( getUnixTime( year + 1 ) )
            if unixTimeThis and unixTimeNext then
                return unixTimeThis, unixTimeNext - 1
            end
        end
    )
    box:addCalendar( unix )
    
    return box:export()
end

--------------------------------------------------------------------
-- Output the box and the class definitions
--------------------------------------------------------------------

local p = {}

function p.main( frame )
    -- If called via #invoke, use the args passed into the invoking
    -- template, or the args passed to #invoke if any exist. Otherwise
    -- assume args are being passed directly in from the debug console
    -- or from another Lua module.
    local origArgs
    if frame == mw.getCurrentFrame() then
        origArgs = frame:getParent().args
        for k, v in pairs( frame.args ) do
            origArgs = frame.args
            break
        end
    else
        origArgs = frame
    end
    -- Trim whitespace and remove blank arguments.
    local args = {}
    for k, v in pairs( origArgs ) do
        if type( v ) == 'string' then
            v = mw.text.trim( v )
        end
        if v ~= '' then
            args[k] = v
        end
    end
    return _main( args )
end

p.calendarBox = calendarBox
p.calendarGroup = calendarGroup
p.calendar = calendar

return p