-- This module implements the {{RailGauge}} template.

local p = {}

-- Adds span tags to prevent a string from wrapping. 
local function noWrap( s )
    return mw.ustring.format( '<span class="nowrap">%s</span>', s )
end

-- A slimmed-down version of the {{frac}} template.
local function frac( whole, num, den )
    return mw.ustring.format(
        '<span class="frac nowrap">%s<sup>%s%s</sup>&frasl;<sub>%s</sub></span>',
        whole or '', whole and ' ' or '', num, den
    )
end

-- Formats imperial measurements. Same functionality as {{RailGauge/format imp}}.
local function formatImp( data, link )
    local ret = {}
    local ft = data.ft
    if ft then
        local ftlink = link and '[[Foot (unit)|ft]]' or 'అడుగులు'
        table.insert( ret, mw.ustring.format( '%s&nbsp;%s', ft, ftlink ) )
    end
    local inches = data['in']
    local num = data.num
    local den = data.den
    if inches and not num and not den then
        table.insert( ret, inches )
    elseif num and den then
        table.insert( ret, frac( inches, num, den ) )
    end
    if inches or num and den then
        local incheslink = link and '[[:en:inch|అంగుళం]]' or 'అం'
        table.insert( ret, incheslink )
    end
    return noWrap( table.concat( ret, '&nbsp;' ) )
end

-- Formats metric measurements. Same functionality as {{RailGauge/format met}}
local function formatMet( data, link )
    local m = data.m
    if m then
        local mlink = link and '[[metre|m]]' or 'మీ'
        return noWrap( mw.ustring.format( '%s&nbsp;%s', m, mlink ) )
    else
        local mm = data.mm
        mm = tonumber( mm )
        if mm then
            mm = mw.getContentLanguage():formatNum( mm )
        end
        local mmlink = link and '[[millimetre|mm]]' or 'మిమీ'
        return noWrap( mw.ustring.format( '%s&nbsp;%s', mm, mmlink ) )
    end
end

-- Composes the initial output from the gauge data taken from [[Module:RailGauge/data]].
-- Same functionality as {{RailGauge/compose}}.
local function compose( args, data )
    local imp = formatImp( data, args.unitlink == 'on' )
    local met = formatMet( data, args.unitlink == 'on' )
    local first = args.first or data.dflt1
    if first == 'met' or first == 'metric' then
        first = 'met'
    else
        first = 'imp'
    end
    local ret = {}
    if first == 'met' then
        table.insert( ret, met )
    else
        table.insert( ret, imp )
    end
    local disp = args.disp
    if disp ~= '1' then
        local formatText
        if disp == 's' then
            formatText = '/&#x200b;%s'
        elseif disp == 'or' then
            formatText = ' or %s'
        else
            formatText = ' (%s)'
        end
        if first == 'met' then
            table.insert( ret, mw.ustring.format( formatText, imp ) )
        else
            table.insert( ret, mw.ustring.format( formatText, met ) )
        end
    end
    ret = table.concat( ret )
    if args.wrap == 'y' then
        return ret
    else
        return noWrap( ret )
    end
end

-- The basic data flow of the module.
local function _main( args )
    local gaugeData = mw.loadData( 'Module:RailGauge/data' )
    local searchKey = mw.ustring.lower( args[ 1 ] or '' )
    searchKey = mw.ustring.gsub( searchKey, '%s', '' ) -- Remove all whitespace.
    local title = mw.title.getCurrentTitle()
    
    -- Get the gauge information from the /data subpage.
    local data
    for i, t in ipairs( gaugeData ) do
        for j, alias in ipairs( t.aliases ) do
            if alias == searchKey then
                data = t
            end
        end
    end
    
    -- Categorise the page if no gauge information was found.
    if not data then
        local category = ''
        local unknownAlias = args[ 1 ]
        if title.namespace == 0 then
            category = mw.ustring.format(
                '[[Category:Pages with incorrect use of RailGauge template|%s, %s]]',
                unknownAlias or ' ', title.text
            )
        end
        return ( unknownAlias or '' ) .. category
    end
    
    -- Assemble the output.
    local ret = {}
    table.insert( ret, compose( args, data ) )
    local gaugeName = data.name
    local gaugeLink = data.link
    if args.allk == 'on' and gaugeLink then
        table.insert( ret, ' ' .. gaugeLink )
    elseif args.al == 'on' and gaugeName then
        table.insert( ret, ' ' .. gaugeName )
    end
    
    return table.concat( ret )
end
 
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, make lower-case and remove blank arguments for all arguments but [1].
    -- [1] is trimmed and blank values are removed, but capitalization is preserved when
    -- when no gauge data is found.
    local args = {}
    for k, v in pairs( origArgs ) do
        v = mw.text.trim( v )
        if k == 1 and v ~= '' then
            args[ 1 ] = v
        elseif v ~= '' then
            args[ k ] = mw.ustring.lower( v )
        end
    end
    return _main( args )
end

-- Performs various checks on the /data subpage.
function p.checkData( frame )
    local dataPage = frame and frame.args and frame.args[1] or 'Module:RailGauge/data'
    local data = mw.loadData( dataPage )
    local exists, dupes, dupeSort, ret = {}, {}, {}, {}
    -- Check for duplicate aliases.
    for ti, t in ipairs( data ) do
        for ai, alias in ipairs( t.aliases or {} ) do
            if not exists[ alias ] then
                exists[ alias ] = { ti, ai }
            else
                if not dupes[ alias ] then
                    dupes[ alias ] = { exists[ alias ] }
                end
                table.insert( dupes[ alias ], { ti, ai } )
            end
        end
    end
    for alias in pairs( dupes ) do
        table.insert( dupeSort, alias )
    end
    table.sort( dupeSort )
    for i1, alias in ipairs( dupeSort ) do
        local positions = {}
        for i2, aliasKeys in ipairs( dupes[ alias ] ) do
            local position = mw.ustring.format( 'gauge %d, alias %d (gauge id: <code>%s</code>)', aliasKeys[ 1 ], aliasKeys[ 2 ], data[ aliasKeys[ 1 ] ].id or '' )
            table.insert( positions, position )
        end
        local aliasText = mw.ustring.format( 'Duplicate aliases "%s" detected at the following positions: %s.', alias, mw.text.listToText( positions, '; ' ) )
        table.insert( ret, aliasText )
    end
    -- Check for numerators without denominators.
    for ti, t in ipairs( data ) do
        local num = t.num
        local den = t.den
        if num and not den then
            table.insert( ret, mw.ustring.format( 'Numerator "%s" with no denominator detected at gauge %d (id: <code>%s</code>).', num, ti, t.id or '' ) )
        elseif den and not num then
            table.insert( ret, mw.ustring.format( 'Denominator "%s" with no numerator detected at gauge %d (id: <code>%s</code>).', den, ti, t.id or '' ) )
        end
    end
    -- Check for gauges with no imperial or no metric measurements.
    for ti, t in ipairs( data ) do
        if not ( t.ft or t['in'] or t.num or t.den ) then
            table.insert( ret, mw.ustring.format( 'No imperial measurements found for gauge %d (id: <code>%s</code>).', ti, t.id or '' ) )
        end
        if not ( t.m or t.mm ) then
            table.insert( ret, mw.ustring.format( 'No metric measurements found for gauge %d (id: <code>%s</code>).', ti, t.id or '' ) )
        end
    end
    -- Check for non-numeric measurements.
    local measurements = { 'ft', 'in', 'num', 'den', 'm', 'mm' }
    for ti, t in ipairs( data ) do
        for mi, measurement in ipairs( measurements ) do
            local measurementVal = t[ measurement ]
            if measurementVal and not tonumber( measurementVal ) then
                table.insert( ret, mw.ustring.format( 'Non-numeric <code>%s</code> measurement ("%s") found for gauge %d (id: <code>%s</code>).', measurement, measurementVal, ti, t.id or '' ) )
            end
        end
    end
    -- Check for gauges with no id.
    for ti, t in ipairs( data ) do
        if not t.id then
            local aliases = {}
            for i, alias in ipairs( t.aliases ) do
                table.insert( aliases, mw.ustring.format( '<code>%s</code>', alias ) )
            end
            aliases = mw.ustring.format( ' (aliases: %s)', mw.text.listToText( aliases ) )
            table.insert( ret, mw.ustring.format( 'No id found for gauge %d%s.', ti, aliases or '' ) )
        end
    end
    -- Check for gauges with no aliases.
    for ti, t in ipairs( data ) do
        if type( t.aliases ) ~= 'table' then
            table.insert( ret, mw.ustring.format( 'No aliases found for gauge %d (id: <code>%s</code>).', ti, t.id or '' ) )
        else
            local isAlias = false
            for ai, alias in ipairs( t.aliases ) do
                isAlias = true
                break
            end
            if not isAlias then
                table.insert( ret, mw.ustring.format( 'No aliases found for gauge %d (id: <code>%s</code>).', ti, t.id or '' ) )
            end
        end
    end
    -- Check for named gauges with no links and gauges with links but no names.
    for ti, t in ipairs( data ) do
        if t.name and not t.link then
            table.insert( ret, mw.ustring.format( 'No link found for the named gauge "%s" at position %d (id: <code>%s</code>).', t.name, ti, t.id or '' ) )
        elseif t.link and not t.name then
            table.insert( ret, mw.ustring.format( 'No name found for the gauge with link "%s" at position %d (id: <code>%s</code>).', t.link, ti, t.id or '' ) )
        end
    end
    -- Check for invalid dflt1 values.
    for ti, t in ipairs( data ) do
        local dflt1 = t.dflt1
        if dflt1 ~= 'imp' and dflt1 ~= 'met' then
            table.insert( ret, mw.ustring.format( 'Invalid dflt1 value "%s" found for gauge %d (id: <code>%s</code>).', dflt1 or '', ti, t.id or '' ) )
        end
    end
    -- Check for unwanted whitespace.
    for ti, t in ipairs( data ) do
        for tkey, tval in pairs( t ) do
            if tkey == 'aliases' and type( tval ) == 'table' then
                for ai, alias in ipairs( tval ) do
                    if mw.ustring.find( alias, '%s' ) then
                        table.insert( ret, mw.ustring.format( 'Unwanted whitespace detected in gauge %d alias %d ("%s", gauge id: <code>%s</code>).', ti, ai, alias, t.id or '' ) )
                    end
                end
            elseif tkey == 'name' or tkey == 'link' then
                if tval ~= mw.text.trim( tval ) then
                    table.insert( ret, mw.ustring.format( 'Unwanted whitespace detected in <code>%s</code> field of gauge %d ("%s", gauge id: <code>%s</code>).', tkey, ti, tval, t.id or '' ) )
                end
            elseif mw.ustring.find( tval, '%s' ) then
                table.insert( ret, mw.ustring.format( 'Unwanted whitespace detected in <code>%s</code> field of gauge %d ("%s", gauge id: <code>%s</code>).', tkey, ti, tval, t.id or '' ) )
            end
        end
    end
    -- Return any errors found.
    for i, msg in ipairs( ret ) do
        ret[ i ] = mw.ustring.format( '<span class="error">%s</span>', msg )
    end
    if #ret > 0 then
        return mw.ustring.format( 'Found the following errors in %s:\n* %s', dataPage, table.concat( ret, '\n* ' ) )
    else
        return mw.ustring.format( 'No errors found in %s.', dataPage )
    end
end

return p