ESO_NumberFormats = {}

do
    local g_strArgs = {}

    function zo_strformat(formatString, ...)
        internalassert(formatString ~= nil, "no format string passed to zo_strformat")
        ZO_ClearNumericallyIndexedTable(g_strArgs)

        for i = 1, select("#", ...) do
            local currentArg = select(i, ...)
            local currentArgType = type(currentArg)
            if currentArgType == "number" then
                local str = ""
                local numFmt = "d"
                local num, frac = zo_decimalsplit(currentArg)
                
                local width = 0
                local digits = 1
                local unsigned = false
                if ESO_NumberFormats[formatString] ~= nil and ESO_NumberFormats[formatString][i] ~= nil then
                    width = ESO_NumberFormats[formatString][i].width or width
                    digits = ESO_NumberFormats[formatString][i].digits or digits
                    unsigned = ESO_NumberFormats[formatString][i].unsigned or unsigned
                end

                if width > 0 then
                    str = string.format("0%d", width)
                end

                if frac ~= 0 then
                    numFmt = "f"
                    str = str..string.format(".%d", digits)
                elseif unsigned == true then
                    numFmt = "u"
                end

                str = string.format("%%%s%s", str, numFmt)

                g_strArgs[i] = string.format(str, currentArg)
            elseif currentArgType == "string" then
                g_strArgs[i] = currentArg
            else
                internalassert(false, string.format("Argument %d with invalid type passed to zo_strformat: %s", i, currentArgType))
                g_strArgs[i] = ""
            end
        end

        if type(formatString) == "number" then
            formatString = GetString(formatString)
        end

        return LocalizeString(formatString, unpack(g_strArgs))
    end
end

do
    -- zo_strformat elegantly handles the case where we pass in a param as the "formatter" (e.g.: collectible descriptions).
    -- However, in order to avoid having each string generate its own cache table, the ZO_CachedStrFormat function need to be explicitely told "I have no formatter"
    -- so it can add all of them to one table.  This cuts down on overhead, with the downside that it loses slight parity with zo_strformat.
    -- However, the fact that we do this whole no param thing at all is exploiting a quirk in the grammar to get around a bug in the grammar anyway so
    -- it's a relatively rare scenario

    ZO_CACHED_STR_FORMAT_NO_FORMATTER = ""

    local g_onlyStoreOneByFormatter = { }

    function ZO_SetCachedStrFormatterOnlyStoreOne(formatter)
        internalassert(formatter ~= ZO_CACHED_STR_FORMAT_NO_FORMATTER)
        g_onlyStoreOneByFormatter[formatter] = true
    end

    local g_cachedStringsByFormatter = 
    {
        [ZO_CACHED_STR_FORMAT_NO_FORMATTER] = {} --Used for strings that need to run through grammar without a formatter
    }

    function ZO_CachedStrFormat(formatter, ...)
        formatter = formatter or ZO_CACHED_STR_FORMAT_NO_FORMATTER

        local formatterCache = g_cachedStringsByFormatter[formatter]
        if not formatterCache then
            formatterCache = {}
            g_cachedStringsByFormatter[formatter] = formatterCache
        end
        
        local cachedString
        if formatter == ZO_CACHED_STR_FORMAT_NO_FORMATTER then
            --"No formatter" only works with 1 param
            local rawString = ...
            local hashKey = HashString(rawString)

            cachedString = formatterCache[hashKey]
            if not cachedString then
                cachedString = zo_strformat(rawString)
                formatterCache[hashKey] = cachedString
            end
        else
            local concatParams = table.concat({ ... })
            local hashKey = HashString(concatParams)

            cachedString = formatterCache[hashKey]
            if not cachedString then
                cachedString = zo_strformat(formatter, ...)
                if g_onlyStoreOneByFormatter[formatter] then
                    local existingKey = next(formatterCache)
                    if existingKey then
                        formatterCache[existingKey] = nil
                    end
                end
                formatterCache[hashKey] = cachedString
            end
        end

        return cachedString
    end

    function ZO_ResetCachedStrFormat(formatter)
        if g_cachedStringsByFormatter[formatter] then
            g_cachedStringsByFormatter[formatter] = nil
        end
    end
end

function zo_strtrim(str)
    -- The extra parentheses are used to discard the additional return value (which is the total number of matches)
    return(zo_strgsub(str, "^%s*(.-)%s*$", "%1"))
end

do
    -- Before passing to a formatting function, use english separators. zo_strformat() (with a <<f:1>> or <<F:1>>) and ZO_FastFormatDecimalNumber() will automatically replace these separators with language-appropriate ones.
    local ENGLISH_DIGIT_GROUP_REPLACER = ","
    local ENGLISH_DIGIT_GROUP_DECIMAL_REPLACER = "."
    local DIGIT_GROUP_REPLACER_THRESHOLD = zo_pow(10, GetDigitGroupingSize())
    
    function ZO_CommaDelimitNumber(amount)
        if amount < DIGIT_GROUP_REPLACER_THRESHOLD then
            return tostring(amount)
        end

        return FormatIntegerWithDigitGrouping(amount, ENGLISH_DIGIT_GROUP_REPLACER, GetDigitGroupingSize())
    end

    function ZO_CommaDelimitDecimalNumber(amount)
        -- Guards against negative 0 as a displayed numeric value
        if amount == 0 then
            amount = 0
        end

        if amount < DIGIT_GROUP_REPLACER_THRESHOLD then
            -- No commas needed
            return tostring(amount)
        end

        local wholeAmount = zo_floor(amount)
        if wholeAmount == amount then
            -- This is an integer, safe to pass to ZO_CommaDelimitNumber
            return ZO_CommaDelimitNumber(amount)
        end

        -- Comma delimit whole part and then concatenate the decimal part as-is
        local amountString = tostring(amount)
        local LITERAL_MATCH = true
        local decimalSeparatorIndex = zo_strfind(amountString, ENGLISH_DIGIT_GROUP_DECIMAL_REPLACER, 1, LITERAL_MATCH)
        local decimalPartString = zo_strsub(amountString, decimalSeparatorIndex)
        return ZO_CommaDelimitNumber(wholeAmount)..decimalPartString
    end

    -- This is a replacement for zo_strformat(SI_NUMBER_FORMAT, decimalNumberString) that avoids the slow call to grammar.
    -- decimalNumberString should be a number string that contains ASCII digits, commas, periods, and/or a negative sign. It can have a non number suffix, so we can continue to support abbreviations, but it really shouldn't contain any more than that.
    -- The return value should not be used as a component of larger format strings, and an amount string should not be run through it more than once.
    local ENGLISH_DIGIT_SEPARATOR_PATTERN = "[,.]"
    local ENGLISH_DIGIT_SEPARATOR_TO_LOCALIZED_SEPARATOR =
    {
        [ENGLISH_DIGIT_GROUP_REPLACER] = GetString(SI_DIGIT_GROUP_SEPARATOR),
        [ENGLISH_DIGIT_GROUP_DECIMAL_REPLACER] = GetString(SI_DIGIT_DECIMAL_SEPARATOR),
    }
    function ZO_FastFormatDecimalNumber(decimalNumberString)
        local firstNonNumberCharacter = zo_strfind(decimalNumberString, "[^%d%.,-]")
        if firstNonNumberCharacter then
            local numberPrefix = zo_strsub(decimalNumberString, 1, firstNonNumberCharacter - 1)
            local nonNumberSuffix = zo_strsub(decimalNumberString, firstNonNumberCharacter, -1)
            return zo_strgsub(numberPrefix, ENGLISH_DIGIT_SEPARATOR_PATTERN, ENGLISH_DIGIT_SEPARATOR_TO_LOCALIZED_SEPARATOR) .. nonNumberSuffix
        else
            return zo_strgsub(decimalNumberString, ENGLISH_DIGIT_SEPARATOR_PATTERN, ENGLISH_DIGIT_SEPARATOR_TO_LOCALIZED_SEPARATOR)
        end
    end
end

do
    local NON_DIGIT = "%D"
    function ZO_CountDigitsInNumber(amount)
        local amountString = tostring(amount)
        if amountString then
            amountString = zo_strgsub(amountString, NON_DIGIT, "")
            return ZoUTF8StringLength(amountString)
        end
        return 0
    end
end

function ZO_GenerateDelimiterSeparatedListWithCustomFinalDelimiter(argumentTable, delimiter, finalDelimiter, finalDelimiterIfListLengthIsTwo)
    if argumentTable ~= nil and #argumentTable > 0 then
        local numArguments = #argumentTable
        -- If there's only one item in the list, the string is just the first item
        if numArguments == 1 then
            return argumentTable[1]
        else
            -- loop through the first through the second to last element adding the delimiter in between
            -- don't add the last since we will use a different separator for it
            local listString = table.concat(argumentTable, delimiter, 1, numArguments - 1)

            -- add the last element of the array to the list using finalDelimiter or finalDelimiterIfListLengthIsTwo as appropriate
            local finalSeparator = finalDelimiter
            if numArguments == 2 and finalDelimiterIfListLengthIsTwo then
                finalSeparator = finalDelimiterIfListLengthIsTwo
            end
            listString = string.format('%s%s%s', listString, finalSeparator, argumentTable[numArguments])
            return listString
        end
    else
        return ""
    end
end

function ZO_GenerateCommaSeparatedListWithAnd(argumentTable)
    return ZO_GenerateDelimiterSeparatedListWithCustomFinalDelimiter(argumentTable, GetString(SI_LIST_COMMA_SEPARATOR), GetString(SI_LIST_COMMA_AND_SEPARATOR), GetString(SI_LIST_AND_SEPARATOR))
end

function ZO_GenerateCommaSeparatedListWithOr(argumentTable)
    return ZO_GenerateDelimiterSeparatedListWithCustomFinalDelimiter(argumentTable, GetString(SI_LIST_COMMA_SEPARATOR), GetString(SI_LIST_COMMA_OR_SEPARATOR), GetString(SI_LIST_OR_SEPARATOR))
end

function ZO_GenerateDelimiterSeparatedList(argumentTable, delimiter)
    if argumentTable ~= nil then
        return table.concat(argumentTable, delimiter)
    else
        return ""
    end
end

function ZO_GenerateCommaSeparatedListWithoutAnd(argumentTable)
    return ZO_GenerateDelimiterSeparatedList(argumentTable, GetString(SI_LIST_COMMA_SEPARATOR))
end

function ZO_GenerateSpaceSeparatedList(argumentTable)
    return ZO_GenerateDelimiterSeparatedList(argumentTable, GetString(SI_LIST_SPACE_SEPARATOR))
end

function ZO_GenerateNewlineSeparatedList(argumentTable)
    return ZO_GenerateDelimiterSeparatedList(argumentTable, "\n")
end

function ZO_GenerateParagraphSeparatedList(argumentTable)
    return ZO_GenerateDelimiterSeparatedList(argumentTable, "\n\n")
end

function ZO_FormatFraction(numerator, denominator)
    return string.format("%d/%d", numerator, denominator)
end

-- Takes a given text and generates a string with the text broken into segments of
-- the specified length and separated by the given delimiter
-- For example a text of "12345678" with a space delimiter and a segment length of 4
-- would yield "1234 5678"
function ZO_GenerateDelimiterSegmentedString(text, segmentLength, delimiter)
    local stringLength = zo_strlen(text)

    local textSegments = {}
    for i = 1, stringLength, segmentLength do
        table.insert(textSegments, zo_strsub(text, i, i + segmentLength - 1))
    end

    local formattedText = table.concat(textSegments, delimiter)
    return formattedText
end
