# Public.

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

Render a CommonMark AST back to Markdown text.

Useful for normalizing Markdown formatting or for roundtrip testing.
Output uses opinionated formatting. Hard breaks use two trailing spaces.

# Examples

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

# Internals.

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

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

escape_markdown_title(s::AbstractString) = replace(s, "\"" => "\\\"")

# Print margin with trailing whitespace stripped (for blank lines)
function print_margin_rstrip(w)
    margin = sprint() do io
        for seg in w.format.margin
            if seg.count == 0
                print(io, ' '^seg.width)
            else
                print(io, seg.text)
            end
        end
    end
    return literal(w, rstrip(margin))
end

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

function linebreak(w, node)
    if !isnull(node.nxt)
        # Skip in tight lists - Item writer handles loose list spacing
        if node.parent.t isa Item && node.parent.parent.t.list_data.tight
            return nothing
        end
        print_margin_rstrip(w)
        literal(w, "\n")
    end
    return nothing
end

# Writers.

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

write_markdown(::Text, w, node, ent) = literal(w, node.literal)

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

function write_markdown(::SoftBreak, w, node, ent)
    return if node.parent.t isa Heading
        literal(w, " ")
    else
        cr(w)
        print_margin(w)
    end
end

function write_markdown(::LineBreak, w, node, ent)
    # Backslash hard breaks already have the `\` from the Backslash node
    if isnull(node.prv) || !(node.prv.t isa Backslash)
        literal(w, "  ")
    end
    cr(w)
    return print_margin(w)
end

# Emit `content` wrapped in a backtick span, with a trailing `suffix` after the
# closing delimiter. The delimiter is the next odd count longer than the longest
# backtick run in `content` (even counts collide with math syntax), and content
# that starts or ends with a backtick is space-padded so it can't merge with the
# delimiter.
function backtick_span(w, content, suffix = "")
    num = foldl(eachmatch(r"`+", content); init = 0) do a, b
        max(a, length(b.match))
    end
    backticks = num + (isodd(num) ? 2 : 1)
    pad = !isempty(content) && (startswith(content, '`') || endswith(content, '`'))
    literal(w, "`"^backticks)
    pad && literal(w, " ")
    literal(w, content)
    pad && literal(w, " ")
    return literal(w, "`"^backticks, suffix)
end

write_markdown(::Code, w, node, ent) = backtick_span(w, node.literal)

function write_markdown(t::HtmlInline, w, node, ent)
    return t.raw ? backtick_span(w, node.literal, "{=html}") : literal(w, node.literal)
end

function write_markdown(link::Link, w, node, ent)
    return if ent
        literal(w, "[")
    else
        literal(w, "](", link.destination)
        isempty(link.title) || literal(w, " \"", escape_markdown_title(link.title), "\"")
        literal(w, ")")
    end
end

function write_markdown(image::Image, w, node, ent)
    return if ent
        literal(w, "![")
    else
        literal(w, "](", image.destination)
        isempty(image.title) || literal(w, " \"", escape_markdown_title(image.title), "\"")
        literal(w, ")")
    end
end

write_markdown(::Emph, w, node, ent) = literal(w, node.literal)

write_markdown(::Strong, w, node, ent) = literal(w, node.literal)

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

function write_markdown(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_markdown(::BlockQuote, w, node, ent)
    return if ent
        push_margin!(w, ">")
        push_margin!(w, " ")
    else
        pop_margin!(w)
        maybe_print_margin(w, node)
        pop_margin!(w)
        cr(w)
        linebreak(w, node)
    end
end

function write_markdown(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_markdown(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
            bullets = ['-', '+', '*', '-', '+', '*']
            bullet = bullets[min(w.format.list_depth, length(bullets))]
            push_margin!(w, 1, lpad("$bullet ", 4, " "))
        end
    else
        if isnull(node.first_child)
            print_margin_rstrip(w)
            cr(w)
        end
        pop_margin!(w)
        if !node.parent.t.list_data.tight
            cr(w)
            linebreak(w, node)
        end
    end
end

function write_markdown(::ThematicBreak, w, node, ent)
    print_margin(w)
    literal(w, "* * *")
    cr(w)
    return linebreak(w, node)
end

function write_markdown(code::CodeBlock, w, node, ent)
    if code.is_fenced
        fence = code.fence_char^code.fence_length
        print_margin(w)
        literal(w, fence, 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)
    else
        for line in eachline(IOBuffer(node.literal); keep = true)
            print_margin(w)
            indent = all(isspace, line) ? 0 : CODE_INDENT
            literal(w, ' '^indent, line)
        end
    end
    return linebreak(w, node)
end

function write_markdown(t::HtmlBlock, w, node, ent)
    if t.raw
        print_margin(w)
        literal(w, "```{=html}\n")
        for line in eachline(IOBuffer(node.literal))
            print_margin(w)
            literal(w, line, "\n")
        end
        print_margin(w)
        literal(w, "```\n")
    else
        for line in eachline(IOBuffer(node.literal); keep = true)
            print_margin(w)
            literal(w, line)
        end
        cr(w)
    end
    return linebreak(w, node)
end
