"""
    FootnoteRule()

Parse footnote definitions and references.

Not enabled by default. Define footnotes with `[^id]:` and reference with `[^id]`.

```markdown
Here is a footnote reference[^1].

[^1]: This is the footnote content.
```
"""
struct FootnoteRule
    cache::Dict{String, Node}
    FootnoteRule() = new(Dict())
end
block_rule(fr::FootnoteRule) = Rule(0.5, "[") do parser, container
    if !parser.indented
        ln = rest_from_nonspace(parser)
        m = match(r"^\[\^([\w\d]+)\]:[ ]?", ln)
        if m !== nothing
            close_unmatched_blocks(parser)
            fr.cache[m[1]] =
                add_child(parser, FootnoteDefinition(m[1]), parser.next_nonspace)
            advance_offset(parser, length(m.match), false)
            return 1
        end
    end
    return 0
end
inline_rule(fr::FootnoteRule) = Rule(0.5, "[") do p, node
    m = consume(p, match(r"^\[\^([\w\d]+)]", p))
    m === nothing && return false
    append_child(node, Node(FootnoteLink(m[1], fr)))
    return true
end

"""Footnote definition block. Build with `Node(FootnoteDefinition, "id", children...)`."""
struct FootnoteDefinition <: AbstractBlock
    id::String
end

function Node(::Type{FootnoteDefinition}, id::AbstractString, children...)
    fd = FootnoteDefinition(id)
    return _build(fd, children)
end

"""Footnote reference link. Build with `Node(FootnoteLink, "id")`."""
struct FootnoteLink <: AbstractInline
    id::String
    rule::FootnoteRule
end

# Temporary rule for programmatic construction - replaced by Document builder
const _UNLINKED_FOOTNOTE_RULE = FootnoteRule()

Node(::Type{FootnoteLink}, id::AbstractString) =
    Node(FootnoteLink(id, _UNLINKED_FOOTNOTE_RULE))

is_container(::FootnoteDefinition) = true
accepts_lines(::FootnoteDefinition) = false
can_contain(::FootnoteDefinition, t) = !(t isa Item)
finalize(::FootnoteDefinition, ::Parser, ::Node) = nothing
function continue_(::FootnoteDefinition, parser::Parser, ::Any)
    if parser.indent ≥ 4
        advance_offset(parser, 4, true)
    elseif parser.blank
        advance_next_nonspace(parser)
    else
        return 1
    end
    return 0
end

#
# Writers
#

# Definitions

function write_html(f::FootnoteDefinition, rend, node, enter)
    return if enter
        tag(
            rend,
            "div",
            attributes(rend, node, ["class" => "footnote", "id" => "footnote-$(f.id)"]),
        )
        tag(rend, "p", ["class" => "footnote-title"])
        print(rend.buffer, f.id)
        tag(rend, "/p")
    else
        tag(rend, "/div")
    end
end

function write_latex(f::FootnoteDefinition, w, node, enter)
    get(w.buffer, :footnote, false) || (w.enabled = !enter)
    return nothing
end

function write_typst(f::FootnoteDefinition, w, node, enter)
    get(w.buffer, :footnote, false) || (w.enabled = !enter)
    return nothing
end

function write_term(f::FootnoteDefinition, rend, node, enter)
    style = crayon"red"
    return if enter
        header = rpad("┌ [^$(f.id)] ", available_columns(rend), "─")
        print_margin(rend)
        print_literal(rend, style, header, inv(style), "\n")
        push_margin!(rend, "│", style)
        push_margin!(rend, " ", crayon"")
    else
        pop_margin!(rend)
        pop_margin!(rend)
        print_margin(rend)
        print_literal(
            rend,
            style,
            rpad("└", available_columns(rend), "─"),
            inv(style),
            "\n",
        )
        if !isnull(node.nxt)
            print_margin(rend)
            print_literal(rend, "\n")
        end
    end
end

function write_markdown(f::FootnoteDefinition, w, node, ent)
    return if ent
        push_margin!(w, 1, "[^$(f.id)]: ", " "^4)
    else
        pop_margin!(w)
        cr(w)
        linebreak(w, node)
    end
end

# Links

function write_html(f::FootnoteLink, rend, node, enter)
    tag(
        rend,
        "a",
        attributes(rend, node, ["href" => "#footnote-$(f.id)", "class" => "footnote"]),
    )
    print(rend.buffer, f.id)
    return tag(rend, "/a")
end

function write_latex(f::FootnoteLink, w, node, enter)
    if haskey(f.rule.cache, f.id)
        seen = get!(() -> Set{String}(), w, :footnotes)
        if f.id in seen
            literal(w, "\\footref{fn:$(f.id)}")
        else
            push!(seen, f.id)
            literal(w, "\\footnote{")
            latex(IOContext(w.buffer, :footnote => true), f.rule.cache[f.id])
            literal(w, "\\label{fn:$(f.id)}}")
        end
    end
    return nothing
end

function write_typst(f::FootnoteLink, w, node, enter)
    if haskey(f.rule.cache, f.id)
        seen = get!(() -> Set{String}(), w, :footnotes)
        if f.id in seen
            literal(w, "#footnote(<fn-$(f.id)>)")
        else
            push!(seen, f.id)
            literal(w, "#footnote[")
            typst(IOContext(w.buffer, :footnote => true), f.rule.cache[f.id])
            literal(w, "] <fn-$(f.id)>")
        end
    end
    return nothing
end

function write_term(f::FootnoteLink, rend, node, enter)
    style = crayon"red"
    print_literal(rend, style)
    push_inline!(rend, style)
    print_literal(rend, "[^", f.id, "]")
    pop_inline!(rend)
    return print_literal(rend, inv(style))
end

write_markdown(f::FootnoteLink, w, node, ent) = literal(w, "[^", f.id, "]")

# Footnote definitions are skipped; their content is inlined at the link.
write_json(::FootnoteDefinition, ctx, node, enter) = nothing

function write_json(f::FootnoteLink, ctx, node, enter)
    enter || return
    haskey(f.rule.cache, f.id) || return
    def = f.rule.cache[f.id]
    # Collect blocks from the footnote definition.
    blocks = Any[]
    push_container!(ctx, blocks)
    for (child, child_enter) in def
        isnull(child) && continue
        child === def && continue
        write_json(child.t, ctx, child, child_enter)
    end
    blocks = pop_container!(ctx)
    return push_element!(ctx, json_el(ctx, "Note", blocks))
end
