# Public.

function Base.show(
        io::IO,
        ::MIME"text/typst",
        ast::Node,
        env = Dict{String, Any}();
        transform = default_transform,
    )
    w = Writer(Typst(io), io, env; transform = transform)
    write_typst(w, ast)
    return nothing
end
"""
    typst(ast::Node) -> String
    typst(filename::String, ast::Node)
    typst(io::IO, ast::Node)

Render a CommonMark AST to Typst markup.

# Examples

```julia
p = Parser()
ast = p("# Hello\\n\\nWorld")
typst(ast)
```
"""
typst(args...; kws...) = writer(MIME"text/typst"(), args...; kws...)

# Internals.

mime_to_str(::MIME"text/typst") = "typst"

mutable struct Typst{I <: IO}
    buffer::I
    indent::Int
    margin::Vector{MarginSegment}
    list_depth::Int
    list_item_number::Vector{Int}
    Typst(io::I) where {I} = new{I}(io, 0, [], 0, [])
end

function write_typst(writer::Writer, ast::Node)
    mime = MIME"text/typst"()
    for (node, entering) in ast
        node, entering = _transform(writer.transform, mime, node, entering, writer)
        write_typst(node.t, writer, node, entering)
    end
    return
end

# Writers.

write_typst(::Document, w, node, ent) = nothing

write_typst(::Text, w, node, ent) = typst_escape(w, node.literal)

write_typst(::Backslash, w, node, ent) = literal(w, "\\")

function write_typst(::Union{SoftBreak, LineBreak}, w, node, ent)
    cr(w)
    return print_margin(w)
end

function write_typst(::Code, w, node, ent)
    content = node.literal
    maxrun = foldl(eachmatch(r"`+", content); init = 0) do a, b
        max(a, length(b.match))
    end
    return if maxrun == 0
        literal(w, "`", content, "`")
    else
        # Typst closes a raw span at a run of N backticks and parses a language
        # tag after 3+ opening backticks. Use N greater than the longest run in
        # the content (and at least 3, since `` is the empty-raw special case),
        # pad with a leading space to suppress the language tag, and a trailing
        # space when the content ends in a backtick so it does not merge with
        # the closing delimiter. Typst trims both pad spaces.
        delim = "`"^max(3, maxrun + 1)
        literal(w, delim, " ", content)
        endswith(content, '`') && literal(w, " ")
        literal(w, delim)
    end
end

write_typst(::HtmlInline, w, node, ent) = nothing

function write_typst(link::Link, w, node, ent)
    return if ent
        literal(w, "#link(", repr(link.destination), ")[")
    else
        literal(w, "]")
    end
end

function write_typst(image::Image, w, node, ent)
    return if ent
        literal(w, "#figure(image(", repr(image.destination), "), caption: [")
    else
        literal(w, "])")
    end
end

write_typst(::Emph, w, node, ent) = literal(w, ent ? "#emph[" : "]")

write_typst(::Strong, w, node, ent) = literal(w, ent ? "#strong[" : "]")

function write_typst(::Paragraph, w, node, ent)
    return if ent
        print_margin(w)
    else
        cr(w)
        linebreak(w, node)
    end
end

function write_typst(heading::Heading, w, node, ent)
    return if ent
        print_margin(w)
        literal(w, "="^heading.level, " ")
    else
        cr(w)
        linebreak(w, node)
    end
end

function write_typst(::BlockQuote, w, node, ent)
    return if ent
        literal(w, "#quote(block: true)[")
        cr(w)
    else
        literal(w, "]")
        cr(w)
    end
end

function write_typst(list::List, w, node, ent)
    return if ent
        w.format.list_depth += 1
        push!(w.format.list_item_number, list.list_data.start)
    else
        w.format.list_depth -= 1
        pop!(w.format.list_item_number)
        cr(w)
        linebreak(w, node)
    end
end

function write_typst(item::Item, w, node, enter)
    return if enter
        if item.list_data.type === :ordered
            number = lpad(string(w.format.list_item_number[end], ". "), 4, " ")
            w.format.list_item_number[end] += 1
            push_margin!(w, 1, number)
        else
            push_margin!(w, 1, lpad("- ", 4, " "))
        end
    else
        if isnull(node.first_child)
            print_margin(w)
            linebreak(w, node)
        end
        pop_margin!(w)
        if !item.list_data.tight
            cr(w)
            linebreak(w, node)
        end
    end
end

function write_typst(::ThematicBreak, w, node, ent)
    literal(w, "#line(start: (25%, 0%), end: (75%, 0%))")
    cr(w)
    return linebreak(w, node)
end

function write_typst(code::CodeBlock, w, node, ent)
    fence = code.is_fenced ? code.fence_char^code.fence_length : "```"
    print_margin(w)
    literal(w, fence, unescape_string(code.info))
    cr(w)
    for line in eachline(IOBuffer(node.literal); keep = true)
        print_margin(w)
        literal(w, line)
    end
    print_margin(w)
    literal(w, fence)
    cr(w)
    return linebreak(w, node)
end

function write_typst(::HtmlBlock, w, node, ent)
    for line in eachline(IOBuffer(node.literal); keep = true)
        print_margin(w)
        literal(w, line)
    end
    return linebreak(w, node)
end

let chars = Dict{Char, String}()
    for c in "~\$#_"
        chars[c] = "\\$c"
    end
    global function typst_escape(w::Writer, s::AbstractString)
        for ch in s
            literal(w, get(chars, ch, ch))
        end
        return
    end

    global function typst_escape(s::AbstractString)
        buffer = IOBuffer()
        for ch in s
            write(buffer, get(chars, ch, ch))
        end
        return String(take!(buffer))
    end
end
