--[[
    fmt.lua - Go's fmt.Printf() style formatting for Lua
    
    Supported verbs:
      %v  - default format (tostring)
      %T  - type of the value
      %t  - boolean (true/false)
      %d  - integer (decimal)
      %b  - integer (binary)
      %o  - integer (octal)
      %x  - integer (hex, lowercase)
      %X  - integer (hex, uppercase)
      %e  - float (scientific, lowercase)
      %E  - float (scientific, uppercase)
      %f  - float (decimal point)
      %g  - float (%e for large, %f otherwise)
      %G  - float (%E for large, %f otherwise)
      %s  - string
      %q  - quoted string (Lua-escaped)
      %c  - character (from integer codepoint)
      %%  - literal percent sign

    Width & precision:
      %8d    - right-aligned, width 8
      %-8d   - left-aligned, width 8
      %08d   - zero-padded, width 8
      %8.2f  - width 8, 2 decimal places
      %.5s   - truncate string to 5 chars
      %*d    - width from next argument
      %.*f   - precision from next argument
--]]

local fmt = {}

-- ── helpers ──────────────────────────────────────────────────────────────────

local function to_int(v)
    local n = math.tointeger and math.tointeger(v) or (type(v) == "number" and math.floor(v) or nil)
    if n == nil then
        error(string.format("fmt: expected integer, got %s (%s)", type(v), tostring(v)), 3)
    end
    return n
end

local function to_num(v)
    if type(v) ~= "number" then
        error(string.format("fmt: expected number, got %s (%s)", type(v), tostring(v)), 3)
    end
    return v
end

-- Convert integer to binary string
local function to_binary(n)
    n = to_int(n)
    if n == 0 then return "0" end
    local neg = n < 0
    if neg then n = -n end
    local bits = {}
    while n > 0 do
        table.insert(bits, 1, n % 2)
        n = math.floor(n / 2)
    end
    local s = table.concat(bits)
    return neg and "-" .. s or s
end

-- Apply width/alignment padding
local function apply_width(s, width, left_align, zero_pad, is_numeric)
    if not width or width == 0 or #s >= width then return s end
    local pad_char = (zero_pad and is_numeric) and "0" or " "
    local pad = string.rep(pad_char, width - #s)
    if zero_pad and is_numeric then
        -- keep sign before zeros: "-007"
        if s:sub(1,1) == "-" then
            return "-" .. pad .. s:sub(2)
        end
        return pad .. s
    end
    return left_align and (s .. pad) or (pad .. s)
end

-- Apply precision to a string (truncation)
local function apply_str_precision(s, prec)
    if prec and #s > prec then
        return s:sub(1, prec)
    end
    return s
end

-- Quoted / escaped string  (like Go %q)
local function quote_string(s)
    s = s:gsub('\\', '\\\\')
    s = s:gsub('"',  '\\"')
    s = s:gsub('\n', '\\n')
    s = s:gsub('\r', '\\r')
    s = s:gsub('\t', '\\t')
    s = s:gsub('%z', '\\0')
    return '"' .. s .. '"'
end

-- ── core formatter ────────────────────────────────────────────────────────────

--[[
    fmt.Sprintf(format, ...)  →  string
    Returns the formatted string without printing it.
--]]
function fmt.Sprintf(format, ...)
    local args  = { ... }
    local ai    = 1          -- argument index
    local out   = {}         -- output buffer

    local i = 1
    local len = #format

    while i <= len do
        local c = format:sub(i, i)

        if c ~= "%" then
            out[#out+1] = c
            i = i + 1
        else
            i = i + 1
            if i > len then
                error("fmt: trailing '%' in format string", 2)
            end

            -- %% literal
            if format:sub(i, i) == "%" then
                out[#out+1] = "%"
                i = i + 1
            else
                -- ── parse flags ───────────────────────────────────────────
                local left_align = false
                local zero_pad   = false
                local plus_sign  = false
                local space_sign = false
                local hash_flag  = false

                while i <= len do
                    local f = format:sub(i, i)
                    if     f == "-" then left_align = true
                    elseif f == "0" then zero_pad   = true
                    elseif f == "+" then plus_sign  = true
                    elseif f == " " then space_sign = true
                    elseif f == "#" then hash_flag  = true
                    else break
                    end
                    i = i + 1
                end

                -- ── parse width ───────────────────────────────────────────
                local width = nil
                if i <= len and format:sub(i, i) == "*" then
                    width = to_int(args[ai]); ai = ai + 1
                    i = i + 1
                else
                    local w = format:match("^%d+", i)
                    if w then width = tonumber(w); i = i + #w end
                end

                -- ── parse precision ───────────────────────────────────────
                local prec = nil
                if i <= len and format:sub(i, i) == "." then
                    i = i + 1
                    if i <= len and format:sub(i, i) == "*" then
                        prec = to_int(args[ai]); ai = ai + 1
                        i = i + 1
                    else
                        local p = format:match("^%d*", i)
                        prec = tonumber(p) or 0
                        i = i + #p
                    end
                end

                -- ── parse verb ────────────────────────────────────────────
                if i > len then
                    error("fmt: missing verb in format string", 2)
                end
                local verb = format:sub(i, i)
                i = i + 1

                local arg = args[ai]; ai = ai + 1
                local s   = ""
                local is_numeric = false

                -- %v  default
                if verb == "v" then
                    s = tostring(arg)

                -- %T  type
                elseif verb == "T" then
                    s = type(arg)

                -- %t  boolean
                elseif verb == "t" then
                    s = arg and "true" or "false"

                -- %d  decimal integer
                elseif verb == "d" then
                    is_numeric = true
                    local n = to_int(arg)
                    s = tostring(math.abs(n))
                    if prec then s = string.rep("0", math.max(0, prec - #s)) .. s end
                    if n < 0 then s = "-" .. s
                    elseif plus_sign then s = "+" .. s
                    elseif space_sign then s = " " .. s
                    end

                -- %b  binary
                elseif verb == "b" then
                    is_numeric = true
                    s = to_binary(arg)
                    if hash_flag then s = "0b" .. s end

                -- %o  octal
                elseif verb == "o" then
                    is_numeric = true
                    local n = to_int(arg)
                    local neg = n < 0
                    s = string.format("%o", math.abs(n))
                    if prec then s = string.rep("0", math.max(0, prec - #s)) .. s end
                    if hash_flag and s:sub(1,1) ~= "0" then s = "0" .. s end
                    if neg then s = "-" .. s end

                -- %x  hex lowercase
                elseif verb == "x" then
                    is_numeric = true
                    local n = to_int(arg)
                    local neg = n < 0
                    s = string.format("%x", math.abs(n))
                    if prec then s = string.rep("0", math.max(0, prec - #s)) .. s end
                    if hash_flag then s = "0x" .. s end
                    if neg then s = "-" .. s end

                -- %X  hex uppercase
                elseif verb == "X" then
                    is_numeric = true
                    local n = to_int(arg)
                    local neg = n < 0
                    s = string.format("%X", math.abs(n))
                    if prec then s = string.rep("0", math.max(0, prec - #s)) .. s end
                    if hash_flag then s = "0X" .. s end
                    if neg then s = "-" .. s end

                -- %e  scientific lowercase
                elseif verb == "e" then
                    is_numeric = true
                    local p = prec ~= nil and prec or 6
                    s = string.format("%." .. p .. "e", to_num(arg))
                    if plus_sign and arg >= 0 then s = "+" .. s end

                -- %E  scientific uppercase
                elseif verb == "E" then
                    is_numeric = true
                    local p = prec ~= nil and prec or 6
                    s = string.format("%." .. p .. "E", to_num(arg))
                    if plus_sign and arg >= 0 then s = "+" .. s end

                -- %f  decimal float
                elseif verb == "f" then
                    is_numeric = true
                    local p = prec ~= nil and prec or 6
                    s = string.format("%." .. p .. "f", to_num(arg))
                    if plus_sign and arg >= 0 then s = "+" .. s end
                    if space_sign and arg >= 0 then s = " " .. s end

                -- %g / %G  shortest float representation
                elseif verb == "g" or verb == "G" then
                    is_numeric = true
                    local p = prec ~= nil and prec or -1
                    local n = to_num(arg)
                    local abs_n = math.abs(n)
                    if p == -1 then
                        -- mimic Go: use %e if exponent < -4 or >= precision(default 6)
                        if abs_n ~= 0 and (abs_n < 1e-4 or abs_n >= 1e6) then
                            s = string.format(verb == "G" and "%.6E" or "%.6e", n)
                        else
                            s = string.format("%.6g", n)
                            if verb == "G" then s = s:upper() end
                        end
                    else
                        s = string.format("%." .. p .. (verb == "G" and "G" or "g"), n)
                    end
                    if plus_sign and n >= 0 then s = "+" .. s end

                -- %s  string
                elseif verb == "s" then
                    s = tostring(arg)
                    s = apply_str_precision(s, prec)

                -- %q  quoted string
                elseif verb == "q" then
                    s = quote_string(tostring(arg))

                -- %c  character
                elseif verb == "c" then
                    local n = to_int(arg)
                    -- utf8.char available in Lua 5.3+
                    if utf8 then
                        s = utf8.char(n)
                    else
                        s = string.char(n)   -- ASCII only fallback
                    end

                else
                    error(string.format("fmt: unknown verb %%%s", verb), 2)
                end

                -- Apply width padding
                s = apply_width(s, width, left_align, zero_pad, is_numeric)
                out[#out+1] = s
            end
        end
    end

    return table.concat(out)
end

--[[
    fmt.Printf(format, ...)
    Prints to stdout (no trailing newline, just like Go).
--]]
function fmt.Printf(format, ...)
    io.write(fmt.Sprintf(format, ...))
end

--[[
    fmt.Println(...)
    Prints args separated by spaces with a trailing newline.
--]]
function fmt.Println(...)
    local parts = {}
    for i = 1, select("#", ...) do
        parts[i] = tostring(select(i, ...))
    end
    print(table.concat(parts, " "))
end

--[[
    fmt.Fprintf(file, format, ...)
    Writes formatted output to a file handle.
--]]
function fmt.Fprintf(file, format, ...)
    file:write(fmt.Sprintf(format, ...))
end

--[[
    fmt.Errorf(format, ...)  →  string
    Returns a formatted error string (for use with error()).
--]]
function fmt.Errorf(format, ...)
    return fmt.Sprintf(format, ...)
end

return fmt
