## Description #############################################################################
#
# Private functions for the Typst back end.
#
############################################################################################

# == Typstry.jl Integration ================================================================

"""
    _typst__is_raw_typst_cell(cell::Any) -> Bool

Return whether the `cell` content must be treated as a raw Typst component. This function is
overloaded by the Typstry.jl extension for `TypstString` cells.
"""
_typst__is_raw_typst_cell(::Any) = false

"""
    _typst__display(output_str::String) -> Bool

Try to display the rendered table `output_str` using the available displays. This function
returns whether the table was displayed. If Typstry.jl is loaded, the extension renders the
table as an image, provided that the current display supports it.
"""
function _typst__display(output_str::String)
    ext = Base.get_extension(PrettyTables, :PrettyTablesTypstryExt)
    isnothing(ext) && return false
    return ext._typst__display(output_str)::Bool
end

# == Alignment =============================================================================

"""
    _typst__alignment(a::Symbol) -> String

Get the Typst alignment string corresponding to the given alignment symbol `a`. If the
alignment symbol is not recognized, it defaults to "right".
"""
_typst__alignment(a::Symbol) = get(_TYPST__ALIGNMENT_MAP, a, "right")

"""
    _typst__alignment_configuration(td::TableData) -> String

Create the Typst alignment configuration string for the given `td::TableData`.
"""
function _typst__alignment_configuration(td::TableData)
    num_columns = td.num_columns

    alignment_str = IOBuffer(; sizehint = 8num_columns)

    # == Row Number Column =================================================================

    td.show_row_number_column &&
        print(alignment_str, _typst__alignment(_row_number_column_alignment(td)) * ", ")

    # == Row Labels ========================================================================

    _has_row_labels(td) &&
        print(alignment_str, _typst__alignment(_row_label_column_alignment(td)) * ", ")

    # == Data Columns ======================================================================

    nc = _number_of_printed_data_columns(td)

    for i in 1:nc
        print(alignment_str, _typst__alignment(_data_column_alignment(td, i)) * ", ")
    end

    # == Continuation Column ===============================================================

    nc < num_columns && print(alignment_str, _typst__alignment(:c) * ", ")

    return String(take!(alignment_str)) |> rstrip
end

# == Attributes ============================================================================

"""
    _typst__cell_and_text_properties(
        vproperties::Vector{TypstPair}
    ) -> Tuple{Vector{TypstPair}, Vector{TypstPair}}

Split `vproperties` into the properties that must be applied to the Typst `table.cell` and
those that must be applied to the text content.

The first returned vector contains only attributes listed in `_TYPST__CELL_ATTRIBUTES`. The
second returned vector contains only valid text properties prefixed with `text-`, with this
prefix removed.
"""
function _typst__cell_and_text_properties(vproperties::Vector{TypstPair})
    # Most data cells have no properties at all. Hence, we return shared empty vectors to
    # avoid two allocations per cell. Notice that the callers must not mutate the returned
    # vectors.
    isempty(vproperties) && return _TYPST__EMPTY_PROPERTIES, _TYPST__EMPTY_PROPERTIES

    # Separate cell and text attributes in a single pass to reduce allocations.
    cell_properties = TypstPair[]
    text_properties = TypstPair[]
    sizehint!(cell_properties, length(vproperties))
    sizehint!(text_properties, length(vproperties))

    for (k, v) in vproperties
        if k ∈ _TYPST__CELL_ATTRIBUTES
            push!(cell_properties, k => v)
        end

        if startswith(k, "text-")
            text_key = SubString(k, 6)
            text_key ∈ _TYPST__TEXT_ATTRIBUTES &&
                push!(text_properties, String(text_key) => v)
        end
    end

    return cell_properties, text_properties
end

# == Cells =================================================================================

"""
    _typst__print_cell(
        io::IO,
        cell::String,
        first_column::Bool,
        il::Int,
        ns::Int,
        minify::Bool
    )

Print a table cell to the output stream in Typst format.

# Arguments

- `io::IO`: The output stream where the cell will be printed.
- `cell::String`: The content of the cell to be printed.
- `first_column::Bool`: Whether this cell is in the first column.
- `il::Int`: The indentation level for formatting.
- `ns::Int`: The number of spaces for indentation.
- `minify::Bool`: If `true`, prints the cell in minified format.
"""
function _typst__print_cell(
    io::IO, cell::String, first_column::Bool, il::Int, ns::Int, minify::Bool
)
    # Notice that the trailing comma is printed separately to avoid allocating a new string
    # per cell.
    if !minify
        _aprint(io, cell, il, ns)
        println(io, ',')
        return nothing
    end

    first_column || print(io, " ")
    print(io, cell)
    print(io, ',')
    return nothing
end

"""
    _typst__table_cell(
        content::String[, properties::Vector{TypstPair}];
        kwargs...
    ) -> String

Create a Typst table cell with `content` and optional `properties`. If `properties` is not
provided or empty, a basic cell is created with the content. If `properties` are provided, a
`table.cell` component is created with the content and properties.

# Keywords

- `il::Int`: Indentation level for formatting.
    (**Default**: 2)
- `ns::Int`: Number of spaces for indentation.
    (**Default**: 2)
- `wrap_column::Int`: Column width threshold for wrapping content.
    (**Default**: 92)
"""
function _typst__table_cell(
    content::String,
    properties::Vector{TypstPair};
    il::Int = 2,
    ns::Int = 2,
    wrap_column::Int = 92,
)
    isempty(properties) && return _typst__table_cell(content; il, ns, wrap_column)
    return _typst__create_component("table.cell", content, properties; il, ns, wrap_column)
end

function _typst__table_cell(
    content::String; il::Int = 2, ns::Int = 2, wrap_column::Int = 92
)
    cl = length(content)
    id_str = repeat(" ", ns)

    _typst__should_wrap(cl, il, ns, wrap_column) && return "[\n$id_str$content\n]"
    return "[$content]"
end

# == Components ============================================================================

"""
    _typst__create_component(component::String, content::String; kwargs...) -> String

Create a Typst `component` with the `content`.

# Keywords

- `il::Int`: Indentation level for formatting.
    (**Default**: 0)
- `ns::Int`: Number of spaces in each indentation level.
    (**Default**: 2)
- `wrap_column::Int`: Column width threshold for wrapping the content.
    (**Default**: 92)
"""
function _typst__create_component(
    component::String,
    content::String,
    properties::Union{Nothing, Vector{TypstPair}} = nothing;
    il::Int = 0,
    ns::Int = 2,
    wrap_column::Int = 92,
)
    open_tag = _typst__open_component(component, properties)

    line_length = length(open_tag) + length(content) + 1

    !_typst__should_wrap(line_length, il, ns, wrap_column) &&
        return open_tag * content * "]"

    buf = IOBuffer()

    println(buf, open_tag)

    # Notice that we must print at the first indentation level because this text will also
    # be indented when printing to the output buffer.
    _aprintln(buf, content, 1, ns)
    print(buf, "]")

    return String(take!(buf))
end

"""
    _typst__open_component(
        component::String,
        properties::Union{Nothing, Vector{TypstPair}} = nothing
    ) -> String

Create the string that opens the Typst `component` with the given `properties`.
"""
function _typst__open_component(
    component::String, properties::Union{Nothing, Vector{TypstPair}} = nothing
)
    prop_str = isnothing(properties) ? "" : "($(_typst__property_list(properties)))"
    return "$component$prop_str["
end

"""
    _typst__property_list(properties::Vector{TypstPair}) -> String

Create a Typst property list string from the given vector of `properties`.
"""
function _typst__property_list(properties::Vector{TypstPair})
    buf = IOBuffer(; sizehint = length(properties) * 32)
    first_prop = true

    for (k, v) in properties
        !first_prop && print(buf, " ")
        first_prop = false

        print(buf, k)

        if !isempty(v)
            starts_with_digit = isdigit(first(v))
            needs_quote = !starts_with_digit && (k ∈ _TYPST__STRING_ATTRIBUTES)

            # The property values are emitted in Typst code mode. Hence, we must not apply
            # the markup escaping here. If the value is emitted inside a string literal, we
            # only need to escape the backslash and the double quote.
            v_str = needs_quote ? _typst__escape_string_literal(v) : v

            print(buf, ": ")
            needs_quote && print(buf, "\"")
            print(buf, v_str)
            needs_quote && print(buf, "\"")
        end

        print(buf, ",")
    end

    return String(take!(buf))
end

"""
    _typst__process_caption(c::TypstCaption, il::Int) -> String

Convert a `TypstCaption` into a string with the corresponding Typst configuration. The `il`
parameter indicates the indentation level for the generated string.
"""
function _typst__process_caption(c::TypstCaption, il::Int)
    (; caption, kind, supplement, gap, position) = c

    ind = " "^max(il, 0)
    buf = IOBuffer()

    @_println(buf, "caption: figure.caption(")

    !isnothing(position) && @_println(buf, ind, "position: ", position, ",")

    @_println(buf, ind, "[", _typst__escape_str(caption), "]")
    @_println(buf, "),")

    if kind ∉ ("table", "auto", "image")
        @_println(buf, "kind: \"", _typst__escape_string_literal(kind), "\",")
        @_println(
            buf,
            "supplement: [",
            _typst__escape_str(something(supplement, titlecase(kind))),
            "],"
        )
    else
        @_println(buf, "kind: ", kind, ",")
        !isnothing(supplement) &&
            @_println(buf, "supplement: [", _typst__escape_str(supplement), "],")
    end

    gap != "auto" && @_println(buf, "gap: ", gap, ",")

    return String(take!(buf))
end

"""
    _typst__text(content::String, properties::Union{Vector{TypstPair}, Nothing}) -> String

Convert the `content` to Typst format with optional styling `properties`. If `properties`
is empty or `nothing` (default), the function returns the content unchanged.  Otherwise, it
creates a Typst `#text` component with the specified properties.
"""
_typst__text(content::String, ::Nothing) = content

function _typst__text(content::String, properties::Vector{TypstPair})
    (isempty(properties) || isempty(content)) && return _typst__text(content, nothing)
    return _typst__create_component("#text", content, properties; wrap_column = -1)
end

# == Lines =================================================================================

"""
    _typst__vertical_lines!(
        buf::IO,
        td::TableData,
        tf::TypstTableFormat,
        total_inner_table_lines::Int,
        vertical_lines_at_data_columns::AbstractVector{Int},
        il::Int,
        ns::Int
    ) -> Nothing

Print to `buf` the Typst vertical lines configurations considering the table data `td` and
table format `tf`. `total_inner_table_lines` is the total number of rows in the inner table
(excluding title, subtitle, footnote, and source note rows).
`vertical_lines_at_data_columns` contains the indices of data columns that require a
vertical line on their right side. `il` and `ns` are the indentation level and number of
spaces per indentation level, respectively.
"""
function _typst__vertical_lines!(
    buf::IO,
    td::TableData,
    tf::TypstTableFormat,
    total_inner_table_lines::Int,
    vertical_lines_at_data_columns::AbstractVector{Int},
    il::Int,
    ns::Int,
)
    num_columns = td.num_columns
    borders     = tf.borders

    current_typst_column = 0

    # We must skip the title and subtitles since they must not have vertical lines.
    has_title    = !isempty(td.title)
    has_subtitle = !isempty(td.subtitle)

    vs = has_title + has_subtitle
    nr = vs + total_inner_table_lines

    # Pre-compute padding once to avoid repeated allocation in each vline call.
    padding = " "^max(il * ns, 0)

    # Print a vline entry directly to avoid intermediate string and split allocations.
    _vline(x, stroke) = begin
        # Using only one argument in `print` to avoid intermediate string allocations.
        if vs == 0
            @_println(
                buf,
                padding,
                "table.vline(x: ",
                x,
                ", end: ",
                nr,
                ", stroke: ",
                stroke,
                "),"
            )
            return nothing
        end

        @_println(
            buf,
            padding,
            "table.vline(x: ",
            x,
            ", start: ",
            vs,
            ", end: ",
            nr,
            ", stroke: ",
            stroke,
            "),"
        )
        return nothing
    end

    # == Table Beginning ===================================================================

    tf.vertical_line_at_beginning && _vline(current_typst_column, borders.left_line)

    # == Row Number Column =================================================================

    if td.show_row_number_column
        current_typst_column += 1
        tf.vertical_line_after_row_number_column &&
            _vline(current_typst_column, borders.center_line)
    end

    # == Row Labels ========================================================================

    if _has_row_labels(td)
        current_typst_column += 1
        tf.vertical_line_after_row_label_column &&
            _vline(current_typst_column, borders.center_line)
    end

    # == Data Columns ======================================================================

    nc = _number_of_printed_data_columns(td)

    for i in 1:nc
        current_typst_column += 1

        format = borders.center_line
        vline  = i in vertical_lines_at_data_columns

        if ((i == nc) && tf.vertical_line_after_data_columns)
            # If we have the continuation line, we must use the middle line here.
            format = nc < num_columns ? borders.center_line : borders.right_line
            vline = true
        end

        vline && _vline(current_typst_column, format)
    end

    # == Continuation Column ===============================================================

    if nc < num_columns
        current_typst_column += 1
        tf.vertical_line_after_continuation_column &&
            _vline(current_typst_column, borders.right_line)
    end

    return nothing
end

# == Strings ===============================================================================

# ASCII characters that carry a special meaning in Typst markup and, hence, must be escaped
# with a backslash when they occur inside a cell. Notice that the cell content is emitted
# inside a Typst content block (`[...]`), meaning that an unbalanced `[` or `]` silently
# breaks the entire document.
const _TYPST__ESCAPED_CHARACTERS = ('\\', '#', '[', ']', '*', '_', '$', '<', '>', '@', '`', '~')

"""
    _typst__escape_str(io::IO, s::AbstractString) -> Nothing
    _typst__escape_str(s::AbstractString) -> String

Print the string `s` in `io` escaping the characters for the Typst backend. If `io` is
omitted, the escaped string is returned.
"""
function _typst__escape_str(io::IO, s::AbstractString)
    for c in s
        if Base.isascii(c)
            # Notice that Typst has no `\xNN` escape sequence. Hence, the non-printable
            # characters must be emitted using the `\u{...}` escape sequence.
            c ∈ _TYPST__ESCAPED_CHARACTERS ? print(io, '\\', c) :
            isprint(c) ? print(io, c) : print(io, "\\u{", string(UInt32(c); base = 16), "}")

        elseif !Base.isoverlong(c) && !Base.ismalformed(c)
            isprint(c) ? print(io, c) : print(io, "\\u{", string(UInt32(c); base = 16), "}")

        else # malformed or overlong
            # We cannot represent these bytes in a valid Typst document. Hence, we replace
            # each one with the Unicode replacement character.
            u = bswap(reinterpret(UInt32, c))
            while true
                print(io, '\ufffd')
                (u >>= 8) == 0 && break
            end
        end
    end
end

function _typst__escape_str(s::AbstractString)
    return sprint(_typst__escape_str, s; sizehint = 2 * lastindex(s))
end

"""
    _typst__escape_string_literal(s::AbstractString) -> String

Escape `s` so that it can be embedded inside a Typst **string literal** (`"..."`).

Notice that this is a different context from the one `_typst__escape_str` handles. Inside a
string literal only the backslash and the double quote are special, and an unescaped double
quote terminates the literal, breaking the document.
"""
function _typst__escape_string_literal(s::AbstractString)
    buf = IOBuffer(; sizehint = lastindex(s))

    for c in s
        (c == '\\') || (c == '"') ? print(buf, '\\', c) : print(buf, c)
    end

    return String(take!(buf))
end

"""
    _typst__should_wrap(str_length::Int, il::Int, ns::Int, wrap_column::Int) -> Bool

Determine whether a string with length `str_length` should be wrapped in Typst backend based
on length constraints. The current indentation level of the string is determined by `il`,
the number of spaces per indentation level is `ns`, and the maximum column width for
wrapping is `wrap_column`.
"""
function _typst__should_wrap(str_length::Int, il::Int, ns::Int, wrap_column::Int)
    indentation_length = (il - 1) * ns
    return (wrap_column >= 0) && (indentation_length + str_length) > wrap_column
end

# == Styles ================================================================================

"""
    _typst__get_data_column_widths(table_data::TableData[, data_column_widths]) -> String

Generate a Typst column width specification string for a table specified by `table_data`.
`data_column_widths` must be an iterable of width specifications for data columns. Each
element should be a width value compatible with Typst format (e.g., "auto", "1fr", "2cm").
If `data_column_widths` is `nothing` (default), it defaults to "auto" for all columns.
"""
function _typst__get_data_column_widths(table_data::TableData, ::Nothing)
    return _typst__get_data_column_widths(
        table_data, Base.Iterators.repeated("auto", table_data.num_columns)
    )
end

function _typst__get_data_column_widths(table_data::TableData, data_column_widths)
    buf = IOBuffer(; sizehint = 8 * (table_data.num_columns + 3) + 2)

    print(buf, "(")

    # == Row Number Column =================================================================

    table_data.show_row_number_column && print(buf, "auto, ")

    # == Row Labels ========================================================================

    _has_row_labels(table_data) && print(buf, "auto, ")

    # == Data Columns ======================================================================

    num_printed_data_columns = _number_of_printed_data_columns(table_data)

    i = 1
    for width in data_column_widths
        @_print(buf, width, ",")
        i += 1
        i > num_printed_data_columns && break
        print(buf, " ")
    end

    # == Continuation Column ===============================================================

    num_printed_data_columns < table_data.num_columns && print(buf, " auto,")

    print(buf, ")")

    return String(take!(buf))
end

"""
    _typst__merge_properties!(
        bproperties::Vector{TypstPair},
        nproperties::Vector{TypstPair}
    ) -> Vector{TypstPair}

Merge two Typst properties, `bproperties` and `nproperties`, giving priority to
`nproperties` in case of conflicts.
"""
function _typst__merge_properties!(
    bproperties::Vector{TypstPair}, nproperties::Vector{TypstPair}
)
    # NOTE: `first.(nproperties)` would allocate a `Vector{String}` on every merge. Scanning
    # `nproperties` directly avoids it, and the vectors involved are tiny.
    filter!(bproperties) do l
        k = first(l)

        for np in nproperties
            first(np) == k && return false
        end

        return true
    end

    append!(bproperties, nproperties)

    return bproperties
end
