using YAML


function WeaveDoc(source, informat = nothing)
    path = abspath(source)
    _, fname = splitdir(path)
    basename = splitext(fname)[1]

    isnothing(informat) && (informat = detect_informat(source))
    header, chunks = parse_doc(read(source, String), informat)

    # update default chunk options from header
    chunk_defaults = deepcopy(get_chunk_defaults())
    if (weave_options = get(header, WEAVE_OPTION_NAME, nothing)) !== nothing
        for key in keys(chunk_defaults)
            if (val = get(weave_options, string(key), nothing)) !== nothing
                chunk_defaults[key] = val
            end
        end
    end
    if haskey(header, WEAVE_OPTION_NAME_DEPRECATED)
        @warn "Weave: `options` key is deprecated. Use `weave_options` key instead." _id = WEAVE_OPTION_DEPRECATE_ID maxlog = 1
        for key in keys(chunk_defaults)
            if (val = get(header[WEAVE_OPTION_NAME_DEPRECATED], string(key), nothing)) !== nothing
                chunk_defaults[key] = val
            end
        end
    end

    return WeaveDoc(
        source,
        basename,
        path,
        chunks,
        "",
        nothing,
        "",
        "",
        header,
        chunk_defaults,
    )
end

"""
    detect_informat(path)

Detect Weave input format based on file extension of `path`.
"""
function detect_informat(path)
    ext = lowercase(last(splitext(path)))

    ext == ".jl" && return "script"
    ext == ".jmd" && return "markdown"
    ext == ".ipynb" && return "notebook"
    return "noweb"
end

function parse_doc(document, informat)
    document = replace(document, "\r\n" => "\n") # normalize line ending

    return informat == "markdown" ? parse_markdown(document) :
        informat == "noweb" ? parse_markdown(document; is_pandoc = true) :
        informat == "script" ? parse_script(document) :
        informat == "notebook" ? parse_notebook(document) :
        error("unsupported input format given: $informat")
end

# inline
# ------

function DocChunk(text::AbstractString, number, start_line; notebook = false)
    # don't parse inline code in notebook
    content = notebook ? parse_inline(text) : parse_inlines(text)
    return DocChunk(content, number, start_line)
end

const   INLINE_REGEX = r"`j\s+(.*?)`"
const INLINE_REGEXES = r"`j\s+(.*?)`|^!\s(.*)$"m

# handle code units correctly !
function parse_inlines(str)
    ret = Inline[]
    s = 1
    code_no = text_no = 0
    for m in eachmatch(INLINE_REGEXES, str)
        e = m.offset
        push!(ret, InlineText((str[s:prevind(str,e)]), text_no += 1))
        i = findfirst(!isnothing, m.captures)
        push!(ret, InlineCode(m.captures[i], code_no += 1, isone(i) ? :inline : :line))
        s = e + ncodeunits(m.match)
    end
    push!(ret, InlineText(str[s:end], text_no += 1))
    return ret
end

parse_inline(str) = Inline[InlineText(str, 1)]

# options
# -------

const OptionDict = Dict{Symbol,Any}

function parse_options(str)::OptionDict
    str = string('(', str, ')')
    ex = Meta.parse(str)
    nt = if Meta.isexpr(ex, (
        :block, # "(k1 = v1; k2 = v2, ...)"
        :tuple, # "(k1 = v1, k2 = v2, ...)"
    ))
        eval(Expr(:tuple, filter(is_valid_kv, ex.args)...))
    elseif is_valid_kv(ex) # "(k = v)"
        eval(Expr(:tuple, ex))
    else
        NamedTuple{}()
    end
    return dict(nt)
end

is_valid_kv(x) = Meta.isexpr(x, :(=))
dict(nt) = Dict((k => v for (k,v) in zip(keys(nt), values(nt)))...)
nt(dict) = NamedTuple{(Symbol.(keys(dict))...,)}((collect(values(dict))...,))

# each input format
# -----------------

include("markdown.jl")
include("script.jl")
include("notebook.jl")
