# This file is a part of Julia. License is MIT: https://julialang.org/license

module Printf

using Base.Ryu

export @printf, @sprintf

# format specifier categories
const Ints = Union{Val{'d'}, Val{'i'}, Val{'u'}, Val{'x'}, Val{'X'}, Val{'o'}}
const Floats = Union{Val{'e'}, Val{'E'}, Val{'f'}, Val{'F'}, Val{'g'}, Val{'G'}, Val{'a'}, Val{'A'}}
const Chars = Union{Val{'c'}, Val{'C'}}
const Strings = Union{Val{'s'}, Val{'S'}}
const Pointer = Val{'p'}
const HexBases = Union{Val{'x'}, Val{'X'}, Val{'a'}, Val{'A'}}
const PositionCounter = Val{'n'}

const MAX_FRACTIONAL_PART_WIDTH = 17  # max significant decimals + 1: `ceil(Int, log10(1 / eps(Float64))) + 1`
const MAX_INTEGER_PART_WIDTH = 309  # max exponent: `ceil(Int, log10(prevfloat(typemax(Float64))))`
const MAX_FMT_CHARS_WIDTH = 5  # hash | sign +/- | decimal dot | exponent e/E | exponent sign

"""
Typed representation of a format specifier.

`T` is a `Val{'_'}`, where `_` is a valid format specifier character.

Fields are the various modifiers allowed for various format specifiers.
"""
struct Spec{T} # T => %type => Val{'type'}
    leftalign::Bool
    plus::Bool
    space::Bool
    zero::Bool
    hash::Bool
    width::Int
    precision::Int
end

# recreate the format specifier string from a typed Spec
Base.string(f::Spec{T}; modifier::String="") where {T} =
    string("%", f.leftalign ? "-" : "", f.plus ? "+" : "", f.space ? " " : "",
        f.zero ? "0" : "", f.hash ? "#" : "", f.width > 0 ? f.width : "",
        f.precision == 0 ? ".0" : f.precision > 0 ? ".$(f.precision)" : "", modifier, char(T))
Base.show(io::IO, f::Spec) = print(io, string(f))

floatfmt(s::Spec{T}) where {T} =
    Spec{Val{'f'}}(s.leftalign, s.plus, s.space, s.zero, s.hash, s.width, 0)
ptrfmt(s::Spec{T}, x) where {T} =
    Spec{Val{'x'}}(s.leftalign, s.plus, s.space, s.zero, true, s.width, sizeof(x) == 8 ? 16 : 8)

"""
    Printf.Format(format_str)

Create a C printf-compatible format object that can be used for formatting values.

The input `format_str` can include any valid format specifier character and modifiers.

A `Format` object can be passed to `Printf.format(f::Format, args...)` to produce a
formatted string, or `Printf.format(io::IO, f::Format, args...)` to print the
formatted string directly to `io`.

For convenience, the `Printf.format"..."` string macro form can be used for building
a `Printf.Format` object at macro-expansion-time.

!!! compat "Julia 1.6"
    `Printf.Format` requires Julia 1.6 or later.
"""
struct Format{S, T}
    str::S # original full format string as CodeUnits
    # keep track of non-format specifier strings to print
    # length(substringranges) == length(formats) + 1
    # so when printing, we start with printing
      # str[substringranges[1]], then formats[1] + args[1]
      # then str[substringranges[2]], then formats[2] + args[2]
      # and so on, then at the end, str[substringranges[end]]
    substringranges::Vector{UnitRange{Int}}
    formats::T # Tuple of Specs
end

# what number base should be used for a given format specifier?
base(T) = T <: HexBases ? 16 : T <: Val{'o'} ? 8 : 10
char(::Type{Val{c}}) where {c} = c

struct InvalidFormatStringError <: Exception
    message::String
    format::String
    start_color::Int
    end_color::Int
end

function Base.showerror(io::IO, err::InvalidFormatStringError)
    io_has_color = get(io, :color, false)::Bool

    println(io, "InvalidFormatStringError: ", err.message)
    print(io, "    \"", @view(err.format[begin:prevind(err.format, err.start_color)]))
    invalid_text = @view err.format[err.start_color:err.end_color]

    printstyled(io, invalid_text, color=:red)

    # +1 is okay, since all format characters are single bytes
    println(io, @view(err.format[err.end_color+1:end]), "\"")

    arrow_error = '-'^(length(invalid_text)-1)
    arrow = "    " * ' '^err.start_color * arrow_error * "^\n"
    if io_has_color
        printstyled(io, arrow, color=:red)
    else
        print(io, arrow)
    end
end

# parse format string
function Format(f::AbstractString)
    isempty(f) && throw(InvalidFormatStringError("Format string must not be empty", f, 1, 1))
    bytes = codeunits(f)
    len = length(bytes)
    pos = 1
    b = 0x00
    local last_percent_pos

    # skip ahead to first format specifier
    while pos <= len
        b = bytes[pos]
        pos += 1
        if b == UInt8('%')
            last_percent_pos = pos-1
            pos > len && throw(InvalidFormatStringError("Format specifier is incomplete", f, last_percent_pos, last_percent_pos))
            if bytes[pos] == UInt8('%')
                # escaped '%'
                b = bytes[pos]
                pos += 1
            else
                break
            end
        end
    end
    strs = [1:pos - 1 - (b == UInt8('%'))]
    fmts = []
    while pos <= len
        b = bytes[pos]
        pos += 1
        # positioned at start of first format str %
        # parse flags
        leftalign = plus = space = zero = hash = false
        while true
            if b == UInt8('-')
                leftalign = true
            elseif b == UInt8('+')
                plus = true
            elseif b == UInt8(' ')
                space = true
            elseif b == UInt8('0')
                zero = true
            elseif b == UInt8('#')
                hash = true
            else
                break
            end
            pos > len && throw(InvalidFormatStringError("Format specifier is incomplete", f, last_percent_pos, pos-1))
            b = bytes[pos]
            pos += 1
        end
        if leftalign
            zero = false
        end
        # parse width
        width = 0
        while b - UInt8('0') < 0x0a
            width = 10 * width + (b - UInt8('0'))
            b = bytes[pos]
            pos += 1
            pos > len && break
        end
        # parse precision
        precision = 0
        parsedprecdigits = false
        if b == UInt8('.')
            pos > len && throw(InvalidFormatStringError("Precision specifier is missing precision", f, last_percent_pos, pos-1))
            parsedprecdigits = true
            b = bytes[pos]
            pos += 1
            if pos <= len
                while b - UInt8('0') < 0x0a
                    precision = 10precision + (b - UInt8('0'))
                    b = bytes[pos]
                    pos += 1
                    pos > len && break
                end
            end
        end
        # parse length modifier (ignored)
        if b == UInt8('h') || b == UInt8('l')
            prev = b
            pos > len && throw(InvalidFormatStringError("Length modifier is missing type specifier", f, last_percent_pos, pos-1))
            b = bytes[pos]
            pos += 1
            if b == prev
                pos > len && throw(InvalidFormatStringError("Length modifier is missing type specifier", f, last_percent_pos, pos-1))
                b = bytes[pos]
                pos += 1
            end
        elseif b in b"Ljqtz" # q was a synonym for ll above, see `man 3 printf`. Not to be used.
            pos > len && throw(InvalidFormatStringError("Length modifier is missing type specifier", f, last_percent_pos, pos-1))
            b = bytes[pos]
            pos += 1
        end
        # parse type
        !(b in b"diouxXDOUeEfFgGaAcCsSpn") && throw(InvalidFormatStringError("'$(Char(b))' is not a valid type specifier", f, last_percent_pos, pos-1))
        type = Val{Char(b)}
        if type <: Ints && precision > 0
            zero = false
        elseif (type <: Strings || type <: Chars) && !parsedprecdigits
            precision = -1
        elseif type <: Union{Val{'a'}, Val{'A'}} && !parsedprecdigits
            precision = -1
        elseif type <: Floats && !parsedprecdigits
            precision = 6
        end
        push!(fmts, Spec{type}(leftalign, plus, space, zero, hash, width, precision))
        start = pos
        while pos <= len
            b = bytes[pos]
            pos += 1
            if b == UInt8('%')
                last_percent_pos = pos-1
                pos > len && throw(InvalidFormatStringError("Format specifier is incomplete", f, last_percent_pos, last_percent_pos))
                if bytes[pos] == UInt8('%')
                    # escaped '%'
                    b = bytes[pos]
                    pos += 1
                else
                    break
                end
            end
        end
        push!(strs, start:pos - 1 - (b == UInt8('%')))
    end
    return Format(bytes, strs, Tuple(fmts))
end

macro format_str(str)
    Format(str)
end

const hex = b"0123456789abcdef"
const HEX = b"0123456789ABCDEF"

# write out a single arg according to format options
# char
@inline function writechar(buf, pos, c)
    u = bswap(reinterpret(UInt32, c))
    while true
        buf[pos] = u % UInt8
        pos += 1
        (u >>= 8) == 0 && break
    end
    return pos
end

@inline function fmt(buf, pos, arg, spec::Spec{T}) where {T <: Chars}
    leftalign, width = spec.leftalign, spec.width
    c = Char(first(arg))
    w = textwidth(c)
    if !leftalign && width > w
        for _ = 1:(width - w)
            buf[pos] = UInt8(' ')
            pos += 1
        end
    end
    pos = writechar(buf, pos, c)
    if leftalign && width > w
        for _ = 1:(width - w)
            buf[pos] = UInt8(' ')
            pos += 1
        end
    end
    return pos
end

# strings
@inline function fmt(buf, pos, arg, spec::Spec{T}) where {T <: Strings}
    leftalign, hash, width, prec = spec.leftalign, spec.hash, spec.width, spec.precision
    str = string(arg)
    slen = textwidth(str)::Int + (hash ? arg isa AbstractString ? 2 : 1 : 0)
    op = p = prec == -1 ? slen : min(slen, prec)
    if !leftalign && width > p
        for _ = 1:(width - p)
            buf[pos] = UInt8(' ')
            pos += 1
        end
    end
    if hash
        if arg isa Symbol
            buf[pos] = UInt8(':')
            pos += 1
            p -= 1
        elseif arg isa AbstractString
            buf[pos] = UInt8('"')
            pos += 1
            p -= 1
        end
    end
    for c in str
        p -= textwidth(c)
        p < 0 && break
        pos = writechar(buf, pos, c)
    end
    if hash && arg isa AbstractString && p > 0
        buf[pos] = UInt8('"')
        pos += 1
    end
    if leftalign && width > op
        for _ = 1:(width - op)
            buf[pos] = UInt8(' ')
            pos += 1
        end
    end
    return pos
end

# integers
toint(x) = x
toint(x::Rational) = Integer(x)

fmt(buf, pos, arg::AbstractFloat, spec::Spec{T}) where {T <: Ints} =
    fmt(buf, pos, arg, floatfmt(spec))

@inline function fmt(buf, pos, arg, spec::Spec{T}) where {T <: Ints}
    leftalign, plus, space, zero, hash, width, prec =
        spec.leftalign, spec.plus, spec.space, spec.zero, spec.hash, spec.width, spec.precision
    bs = base(T)
    arg2 = toint(arg)
    n = i = ndigits(arg2, base=bs, pad=1)
    neg = arg2 < 0
    x = arg2 isa Base.BitSigned ? unsigned(abs(arg2)) : abs(arg2)
    arglen = n + (neg || (plus | space)) +
        (T == Val{'o'} && hash ? 1 : 0) +
        (T == Val{'x'} && hash ? 2 : 0) + (T == Val{'X'} && hash ? 2 : 0)
    arglen2 = arglen < width && prec > 0 ? arglen + min(max(0, prec - n), width - arglen) : arglen
    if !leftalign && !zero && arglen2 < width
        # pad left w/ spaces
        for _ = 1:(width - arglen2)
            buf[pos] = UInt8(' ')
            pos += 1
        end
    end
    if neg
        buf[pos] = UInt8('-'); pos += 1
    elseif plus # plus overrides space
        buf[pos] = UInt8('+'); pos += 1
    elseif space
        buf[pos] = UInt8(' '); pos += 1
    end
    if T == Val{'o'} && hash
        buf[pos] = UInt8('0')
        pos += 1
    elseif T == Val{'x'} && hash
        buf[pos] = UInt8('0')
        buf[pos + 1] = UInt8('x')
        pos += 2
    elseif T == Val{'X'} && hash
        buf[pos] = UInt8('0')
        buf[pos + 1] = UInt8('X')
        pos += 2
    end
    if zero && arglen2 < width
        for _ = 1:(width - arglen2)
            buf[pos] = UInt8('0')
            pos += 1
        end
    elseif n < prec
        for _ = 1:(prec - n)
            buf[pos] = UInt8('0')
            pos += 1
        end
    elseif arglen < arglen2
        for _ = 1:(arglen2 - arglen)
            buf[pos] = UInt8('0')
            pos += 1
        end
    end
    while i > 0
        @inbounds buf[pos + i - 1] = bs == 16 ?
            (T == Val{'x'} ? hex[(x & 0x0f) + 1] : HEX[(x & 0x0f) + 1]) :
            (48 + (bs == 8 ? (x & 0x07) : rem(x, 10)))
        if bs == 8
            x >>= 3
        elseif bs == 16
            x >>= 4
        else
            x = oftype(x, div(x, 10))
        end
        i -= 1
    end
    pos += n
    if leftalign && arglen2 < width
        # pad right
        for _ = 1:(width - arglen2)
            buf[pos] = UInt8(' ')
            pos += 1
        end
    end
    return pos
end

# floats
"""
    Printf.tofloat(x)

Convert an argument to a Base float type for printf formatting.
By default, arguments are converted to `Float64` via `Float64(x)`.
Custom numeric types that have a conversion to a Base float type
that wish to hook into printf formatting can extend this method like:

```julia
Printf.tofloat(x::MyCustomType) = convert_my_custom_type_to_float(x)
```

For arbitrary precision numerics, you might extend the method like:

```julia
Printf.tofloat(x::MyArbitraryPrecisionType) = BigFloat(x)
```

!!! compat "Julia 1.6"
    This function requires Julia 1.6 or later.
"""
tofloat(x) = Float64(x)
tofloat(x::Base.IEEEFloat) = x
tofloat(x::BigFloat) = x

_snprintf(ptr, siz, str, arg) =
    @ccall "libmpfr".mpfr_snprintf(ptr::Ptr{UInt8}, siz::Csize_t, str::Ptr{UInt8};
                                   arg::Ref{BigFloat})::Cint

# Arbitrary constant for a maximum number of bytes we want to output for a BigFloat.
# 8KiB seems like a reasonable default. Larger BigFloat representations should probably
# use a custom printing routine. Printing values with results larger than this ourselves
# seems like a dangerous thing to do.
const __BIG_FLOAT_MAX__ = 8192

@inline function fmt(buf, pos, arg, spec::Spec{T}) where {T <: Floats}
    leftalign, plus, space, zero, hash, width, prec =
        spec.leftalign, spec.plus, spec.space, spec.zero, spec.hash, spec.width, spec.precision
    x = tofloat(arg)
    if x isa BigFloat
        if isfinite(x)
            GC.@preserve buf begin
                siz = length(buf) - pos + 1
                str = string(spec; modifier="R")
                required_length = _snprintf(pointer(buf, pos), siz, str, x)
                if required_length > siz
                    required_length > __BIG_FLOAT_MAX__ &&
                        throw(ArgumentError("The given BigFloat requires $required_length bytes to be printed, which is more than the maximum of $__BIG_FLOAT_MAX__ bytes supported."))
                    resize!(buf, required_length + 1)
                    required_length = _snprintf(pointer(buf, pos), required_length + 1, str, x)
                end
                required_length > 0 || throw(ArgumentError("The given BigFloat would produce less than the maximum allowed number of bytes $__BIG_FLOAT_MAX__, but still couldn't be printed fully for an unknown reason."))
                return pos + required_length
            end
        end
        x = Float64(x)
    end
    if T == Val{'e'} || T == Val{'E'}
        newpos = Ryu.writeexp(buf, pos, x, prec, plus, space, hash, char(T), UInt8('.'))
    elseif T == Val{'f'} || T == Val{'F'}
        newpos = Ryu.writefixed(buf, pos, x, prec, plus, space, hash, UInt8('.'))
    elseif T == Val{'g'} || T == Val{'G'}
        if isinf(x) || isnan(x)
            newpos = Ryu.writeshortest(buf, pos, x, plus, space)
        else
            # C11-compliant general format
            prec = prec == 0 ? 1 : prec
            # format the value in scientific notation and parse the exponent part
            exp = let p = Ryu.writeexp(buf, pos, x, prec)
                b1, b2, b3, b4 = buf[p-4], buf[p-3], buf[p-2], buf[p-1]
                Z = UInt8('0')
                if b1 == UInt8('e')
                    # two-digit exponent
                    sign = b2 == UInt8('+') ? 1 : -1
                    exp = 10 * (b3 - Z) + (b4 - Z)
                else
                    # three-digit exponent
                    sign = b1 == UInt8('+') ? 1 : -1
                    exp = 100 * (b2 - Z) + 10 * (b3 - Z) + (b4 - Z)
                end
                flipsign(exp, sign)
            end
            if -4 ≤ exp < prec
                newpos = Ryu.writefixed(buf, pos, x, prec - (exp + 1), plus, space, hash, UInt8('.'), !hash)
            else
                newpos = Ryu.writeexp(buf, pos, x, prec - 1, plus, space, hash, T == Val{'g'} ? UInt8('e') : UInt8('E'), UInt8('.'), !hash)
            end
        end
    elseif T == Val{'a'} || T == Val{'A'}
        x, neg = x < 0 || x === -Base.zero(x) ? (-x, true) : (x, false)
        newpos = pos
        if neg
            buf[newpos] = UInt8('-')
            newpos += 1
        elseif plus
            buf[newpos] = UInt8('+')
            newpos += 1
        elseif space
            buf[newpos] = UInt8(' ')
            newpos += 1
        end
        if isnan(x)
            buf[newpos] = UInt8('N')
            buf[newpos + 1] = UInt8('a')
            buf[newpos + 2] = UInt8('N')
            newpos += 3
        elseif !isfinite(x)
            buf[newpos] = UInt8('I')
            buf[newpos + 1] = UInt8('n')
            buf[newpos + 2] = UInt8('f')
            newpos += 3
        else
            buf[newpos] = UInt8('0')
            newpos += 1
            buf[newpos] = T <: Val{'a'} ? UInt8('x') : UInt8('X')
            newpos += 1
            if arg == 0
                buf[newpos] = UInt8('0')
                newpos += 1
                if prec > 0
                    buf[newpos] = UInt8('.')
                    newpos += 1
                    while prec > 0
                        buf[newpos] = UInt8('0')
                        newpos += 1
                        prec -= 1
                    end
                end
                buf[newpos] = T <: Val{'a'} ? UInt8('p') : UInt8('P')
                buf[newpos + 1] = UInt8('+')
                buf[newpos + 2] = UInt8('0')
                newpos += 3
            else
                if prec > -1
                    s, p = frexp(x)
                    sigbits = 4 * min(prec, 13)
                    s = 0.25 * round(ldexp(s, 1 + sigbits))
                    # ensure last 2 exponent bits either 01 or 10
                    u = (reinterpret(UInt64, s) & 0x003f_ffff_ffff_ffff) >> (52 - sigbits)
                    i = n = (sizeof(u) << 1) - (leading_zeros(u) >> 2)
                else
                    s, p = frexp(x)
                    s *= 2.0
                    u = (reinterpret(UInt64, s) & 0x001f_ffff_ffff_ffff)
                    t = (trailing_zeros(u) >> 2)
                    u >>= (t << 2)
                    i = n = 14 - t
                end
                frac = u > 9 || hash || prec > 0
                while i > 1
                    buf[newpos + i] = T == Val{'a'} ? hex[(u & 0x0f) + 1] : HEX[(u & 0x0f) + 1]
                    u >>= 4
                    i -= 1
                    prec -= 1
                end
                if frac
                    buf[newpos + 1] = UInt8('.')
                end
                buf[newpos] = T == Val{'a'} ? hex[(u & 0x0f) + 1] : HEX[(u & 0x0f) + 1]
                newpos += n + frac
                while prec > 0
                    buf[newpos] = UInt8('0')
                    newpos += 1
                    prec -= 1
                end
                buf[newpos] = T <: Val{'a'} ? UInt8('p') : UInt8('P')
                newpos += 1
                p -= 1
                buf[newpos] = p < 0 ? UInt8('-') : UInt8('+')
                p = p < 0 ? -p : p
                newpos += 1
                n = i = ndigits(p, base=10, pad=1)
                while i > 0
                    buf[newpos + i - 1] = 48 + rem(p, 10)
                    p = oftype(p, div(p, 10))
                    i -= 1
                end
                newpos += n
            end
        end
    end
    if newpos - pos < width
        # need to pad
        if leftalign
            # easy case, just pad spaces after number
            for _ = 1:(width - (newpos - pos))
                buf[newpos] = UInt8(' ')
                newpos += 1
            end
        else
            # right aligned
            n = width - (newpos - pos)
            if zero
                ex = (arg < 0 || (plus | space)) + (T <: Union{Val{'a'}, Val{'A'}} ? 2 : 0)
                so = pos + ex
                len = (newpos - pos) - ex
                copyto!(buf, so + n, buf, so, len)
                for i = so:(so + n - 1)
                    buf[i] = UInt8('0')
                end
                newpos += n
            else
                copyto!(buf, pos + n, buf, pos, newpos - pos)
                for i = pos:(pos + n - 1)
                    buf[i] = UInt8(' ')
                end
                newpos += n
            end
        end
    end
    return newpos
end

# pointers
fmt(buf, pos, arg, spec::Spec{Pointer}) = fmt(buf, pos, UInt64(arg), ptrfmt(spec, arg))

# position counters
function fmt(buf, pos, arg::Ref{<:Integer}, ::Spec{PositionCounter})
    arg[] = pos - 1
    pos
end

# old Printf compat
function fix_dec end
function ini_dec end

# generic fallback
function fmtfallback(buf, pos, arg, spec::Spec{T}) where {T}
    leftalign, plus, space, zero, hash, width, prec =
        spec.leftalign, spec.plus, spec.space, spec.zero, spec.hash, spec.width, spec.precision
    buf2 = Base.StringVector(
        MAX_INTEGER_PART_WIDTH + MAX_FRACTIONAL_PART_WIDTH + MAX_FMT_CHARS_WIDTH
    )
    ise = T <: Union{Val{'e'}, Val{'E'}}
    isg = T <: Union{Val{'g'}, Val{'G'}}
    isf = T <: Val{'f'}
    if isg
        prec = prec == 0 ? 1 : prec
        arg = round(arg, sigdigits=prec)
    end
    n, pt, neg = isf ? fix_dec(arg, prec, buf2) : ini_dec(arg, min(prec + ise, length(buf2) - 1), buf2)
    if isg && !hash
        while buf2[n] == UInt8('0')
            n -= 1
        end
    end
    expform = ise || (isg && !(-4 < pt <= prec))
    n2 = n + (expform ? 4 : 0) + (prec > 0 || hash) + (neg || (plus | space)) +
        (isf && pt >= n ? prec + 1 : 0)
    if !leftalign && !zero && n2 < width
        # pad left w/ spaces
        for _ = 1:(width - n2)
            buf[pos] = UInt8(' ')
            pos += 1
        end
    end
    if neg
        buf[pos] = UInt8('-'); pos += 1
    elseif plus # plus overrides space
        buf[pos] = UInt8('+'); pos += 1
    elseif space
        buf[pos] = UInt8(' '); pos += 1
    end
    if zero && n2 < width
        for _ = 1:(width - n2)
            buf[pos] = UInt8('0')
            pos += 1
        end
    end
    if expform
        buf[pos] = buf2[1]
        pos += 1
        if n > 1 || hash
            buf[pos] = UInt8('.')
            pos += 1
            for i = 2:n
                buf[pos] = buf2[i]
                pos += 1
            end
        end
        buf[pos] = T <: Val{'e'} || T <: Val{'g'} ? UInt8('e') : UInt8('E')
        pos += 1
        exp = pt - 1
        buf[pos] = exp < 0 ? UInt8('-') : UInt8('+')
        pos += 1
        exp = abs(exp)
        if exp < 10
            buf[pos] = UInt8('0')
            buf[pos + 1] = 48 + exp
            pos += 2
        else
            buf[pos] = 48 + div(exp, 10)
            buf[pos + 1] = 48 + rem(exp, 10)
            pos += 2
        end
    elseif pt <= 0
        buf[pos] = UInt8('0')
        buf[pos + 1] = UInt8('.')
        pos += 2
        while pt < 0
            buf[pos] = UInt8('0')
            pos += 1
            pt += 1
        end
        for i = 1:n
            buf[pos] = buf2[i]
            pos += 1
        end
    elseif pt >= n
        for i = 1:n
            buf[pos] = buf2[i]
            pos += 1
        end
        while pt > n
            buf[pos] = UInt8('0')
            pos += 1
            n += 1
        end
        if hash || (isf && prec > 0)
            buf[pos] = UInt8('.')
            pos += 1
            while prec > 0
                buf[pos] = UInt8('0')
                pos += 1
                prec -= 1
            end
        end
    else
        for i = 1:pt
            buf[pos] = buf2[i]
            pos += 1
        end
        buf[pos] = UInt8('.')
        pos += 1
        for i = pt+1:n
            buf[pos] = buf2[i]
            pos += 1
        end
    end

    if leftalign && n2 < width
        # pad right
        for _ = 1:(width - n2)
            buf[pos] = UInt8(' ')
            pos += 1
        end
    end
    return pos
end

const UNROLL_UPTO = 16
# if you have your own buffer + pos, write formatted args directly to it
@inline function format(buf::Vector{UInt8}, pos::Integer, f::Format, args...)
    # write out first substring
    escapechar = false
    for i in f.substringranges[1]
        b = f.str[i]
        if !escapechar
            buf[pos] = b
            pos += 1
            escapechar = b === UInt8('%')
        else
            escapechar = false
        end
    end
    # for each format, write out arg and next substring
    # unroll up to 16 formats
    N = length(f.formats)
    Base.@nexprs 16 i -> begin
        if N >= i
            pos = fmt(buf, pos, args[i], f.formats[i])
            for j in f.substringranges[i + 1]
                b = f.str[j]
                if !escapechar
                    buf[pos] = b
                    pos += 1
                    escapechar = b === UInt8('%')
                else
                    escapechar = false
                end
            end
        end
    end
    if N > 16
        for i = 17:length(f.formats)
            pos = fmt(buf, pos, args[i], f.formats[i])
            for j in f.substringranges[i + 1]
                b = f.str[j]
                if !escapechar
                    buf[pos] = b
                    pos += 1
                    escapechar = b === UInt8('%')
                else
                    escapechar = false
                end
            end
        end
    end
    return pos
end

function plength(f::Spec{T}, x) where {T <: Chars}
    c = Char(first(x))
    w = textwidth(c)
    return max(f.width, w) + (ncodeunits(c) - w)
end
plength(f::Spec{Pointer}, x) = max(f.width, 2 * sizeof(x) + 2)

function plength(f::Spec{T}, x) where {T <: Strings}
    str = string(x)
    sw = textwidth(str)
    p = f.precision == -1 ? (sw + (f.hash ? (x isa Symbol ? 1 : 2) : 0)) : f.precision
    return max(f.width, p) + (sizeof(str) - sw)
end

function plength(f::Spec{T}, x) where {T <: Ints}
    x2 = toint(x)
    return max(
        f.width,
        f.precision + ndigits(x2, base=base(T), pad=1) + MAX_FMT_CHARS_WIDTH
    )
end

plength(f::Spec{T}, x::AbstractFloat) where {T <: Ints} =
    max(f.width, f.hash + MAX_INTEGER_PART_WIDTH + 0 + MAX_FMT_CHARS_WIDTH)
plength(f::Spec{T}, x) where {T <: Floats} =
    max(f.width, f.hash + MAX_INTEGER_PART_WIDTH + f.precision + MAX_FMT_CHARS_WIDTH)
plength(::Spec{PositionCounter}, x) = 0

@inline function computelen(substringranges, formats, args)
    len = sum(length, substringranges)
    N = length(formats)
    # unroll up to 16 formats
    Base.@nexprs 16 i -> begin
        if N >= i
            len += plength(formats[i], args[i])
        end
    end
    if N > 16
        for i = 17:length(formats)
            len += plength(formats[i], args[i])
        end
    end
    return len
end

@noinline argmismatch(a, b) =
    throw(ArgumentError("Number of format specifiers and number of provided args differ: $a != $b"))

"""
    Printf.format(f::Printf.Format, args...) => String
    Printf.format(io::IO, f::Printf.Format, args...)

Apply a printf format object `f` to provided `args` and return the formatted string
(1st method), or print directly to an `io` object (2nd method). See [`@printf`](@ref)
for more details on C `printf` support.
"""
function format end

function format(io::IO, f::Format, args...) # => Nothing
    length(f.formats) == length(args) || argmismatch(length(f.formats), length(args))
    buf = Base.StringVector(computelen(f.substringranges, f.formats, args))
    pos = format(buf, 1, f, args...)
    write(io, resize!(buf, pos - 1))
    return
end

function format(f::Format, args...) # => String
    length(f.formats) == length(args) || argmismatch(length(f.formats), length(args))
    buf = Base.StringVector(computelen(f.substringranges, f.formats, args))
    pos = format(buf, 1, f, args...)
    return String(resize!(buf, pos - 1))
end

"""
    @printf([io::IO], "%Fmt", args...)

Print `args` using C `printf` style format specification string.
Optionally, an `IO` may be passed as the first argument to redirect output.

# Examples
```jldoctest
julia> @printf "Hello %s" "world"
Hello world

julia> @printf "Scientific notation %e" 1.234
Scientific notation 1.234000e+00

julia> @printf "Scientific notation three digits %.3e" 1.23456
Scientific notation three digits 1.235e+00

julia> @printf "Decimal two digits %.2f" 1.23456
Decimal two digits 1.23

julia> @printf "Padded to length 5 %5i" 123
Padded to length 5   123

julia> @printf "Padded with zeros to length 6 %06i" 123
Padded with zeros to length 6 000123

julia> @printf "Use shorter of decimal or scientific %g %g" 1.23 12300000.0
Use shorter of decimal or scientific 1.23 1.23e+07
```

For a systematic specification of the format, see [here](https://www.cplusplus.com/reference/cstdio/printf/).
See also [`@sprintf`](@ref) to get the result as a `String` instead of it being printed.

# Caveats
`Inf` and `NaN` are printed consistently as `Inf` and `NaN` for flags `%a`, `%A`,
`%e`, `%E`, `%f`, `%F`, `%g`, and `%G`. Furthermore, if a floating point number is
equally close to the numeric values of two possible output strings, the output
string further away from zero is chosen.

# Examples
```jldoctest
julia> @printf("%f %F %f %F", Inf, Inf, NaN, NaN)
Inf Inf NaN NaN

julia> @printf "%.0f %.1f %f" 0.5 0.025 -0.0078125
0 0.0 -0.007812
```

!!! compat "Julia 1.8"
    Starting in Julia 1.8, `%s` (string) and `%c` (character) widths are computed
    using [`textwidth`](@ref), which e.g. ignores zero-width characters
    (such as combining characters for diacritical marks) and treats certain
    "wide" characters (e.g. emoji) as width `2`.
"""
macro printf(io_or_fmt, args...)
    if io_or_fmt isa String
        fmt = Format(io_or_fmt)
        return esc(:($Printf.format(stdout, $fmt, $(args...))))
    else
        io = io_or_fmt
        isempty(args) && throw(ArgumentError("No format string provided to `@printf` - use like `@printf [io] <format string> [<args...>]."))
        fmt_str = first(args)
        fmt_str isa String || throw(ArgumentError("First argument to `@printf` after `io` must be a format string"))
        fmt = Format(fmt_str)
        return esc(:($Printf.format($io, $fmt, $(Base.tail(args)...))))
    end
end

"""
    @sprintf("%Fmt", args...)

Return [`@printf`](@ref) formatted output as string.

# Examples
```jldoctest
julia> @sprintf "this is a %s %15.1f" "test" 34.567
"this is a test            34.6"
```
"""
macro sprintf(fmt, args...)
    fmt isa String || throw(ArgumentError("First argument to `@sprintf` must be a format string."))
    f = Format(fmt)
    return esc(:($Printf.format($f, $(args...))))
end

end # module
