"""
    Revise.WatchList

A struct for holding files that live inside a directory.
Some platforms (OSX) have trouble watching too many files. So we
watch parent directories, and keep track of which files in them
should be tracked.

Fields:
- `trackedfiles`: map from basename to `PkgId` for each watched file
- `file_ctimes`: last-seen `ctime` for each tracked file, used to detect changes
- `file_hashes`: content hash for each tracked file, recorded when the file is
  queued for revision. Disambiguates a filesystem event whose ctime matches the
  stored one: kernel timestamps have tick resolution, so an unchanged ctime can
  mean either a duplicate notification or a same-tick rewrite.
"""
mutable struct WatchList
    trackedfiles::Dict{String,PkgId}
    file_ctimes::Dict{String,Float64}
    file_hashes::Dict{String,UInt64}
end

"""
    ExtendedData

Linked list structure for storing extension data from multiple tools.
Each node contains:
- `owner::Symbol`: The extension that owns this data (e.g., `:jet`, `:coverage`)
- `data`: The extension-specific data (untyped for flexibility)
- `next::Union{ExtendedData,Nothing}`: Link to the next extension's data

The linked list allows multiple extensions to attach data to the same signature
without interfering with each other.
"""
struct ExtendedData
    owner::Symbol
    data::Any
    next::ExtendedData
    ExtendedData(owner::Symbol, @nospecialize(data), next::ExtendedData) = new(owner, data, next)
    ExtendedData(owner::Symbol, @nospecialize(data)) = new(owner, data)
    ExtendedData() = new(:Revise, nothing)
end

# Sentinel value representing the absence of extension data.
const no_extended_data = ExtendedData()

"""
    SigInfo

Data structure used as a leaf node of the `pkgdatas` data structure,
which can hold additional extension data.

Fields:
- `mt::Union{Nothing,MethodTable}`: Method table (or `nothing`)
- `sig::Type`: Signature type
- `ext::ExtendedData`: Extension data (for external tools like JET),
  stored as a linked list of [`ExtendedData`](@ref)

This is a Revise-internal data structure; when interacting with CodeTracking,
use `MethodInfoKey(::SigInfo)` to convert to `MethodInfoKey`.
"""
struct SigInfo
    mt::Union{Nothing,MethodTable}
    sig::Type
    ext::ExtendedData
    SigInfo(mt::Union{Nothing,MethodTable}, @nospecialize(sig::Type), ext::ExtendedData) = new(mt, sig, ext)
end

SigInfo(mt::Union{Nothing,MethodTable}, @nospecialize(sig::Type)) = SigInfo(mt, sig, no_extended_data)
SigInfo((mt, sig)::MethodInfoKey) = SigInfo(mt, sig, no_extended_data)

function Base.iterate(e::SigInfo, st::Int=0)
    if st == 0
        return e.mt, 1
    elseif st == 1
        return e.sig, 2
    elseif st == 2
        return e.ext, 3
    else
        return nothing
    end
end

CodeTracking.MethodInfoKey(si::SigInfo) = MethodInfoKey(si.mt, si.sig)

struct TypeInfo
    typname::Core.TypeName
    ext::ExtendedData
    TypeInfo(typname::Core.TypeName, ext::ExtendedData = no_extended_data) = new(typname, ext)
end

"""
    ExInfo(ex::Expr)

Create a cache for storing information about method and type definitions.

Adding signatures to such an object inserts them into `CodeTracking.method_info`,
which maps signature Tuple-types to `(lnn::LineNumberNode, ex::Expr)` pairs.
Because method signatures are unique within a module, this is the foundation for
identifying methods in a manner independent of source-code location.

Type definitions are also tracked to support struct revision, but unlike method
signatures, they are not currently cached in CodeTracking.

It also has the following fields:

- `exprstack`: used when descending into `@eval` statements: `ex` (used in creating the `ExInfo` object) is the first entry in the stack
- `allsigs`: a list of all method signatures defined by a given expression (cached in `CodeTracking.method_info`)
- `typeinfos`: a list of all type definitions defined by a given expression (not cached in CodeTracking)
- `includes`: a list of `(module, mapexpr, filename)` for any `include` statements encountered
  while the expression was parsed. `mapexpr` is the transform passed to
  `include(mapexpr, filename)`, or `identity` for plain `include(filename)`.
"""
struct ExInfo
    exprstack::Vector{Expr}
    allsigs::Vector{SigInfo}
    typeinfos::Vector{TypeInfo}
    includes::Vector{Tuple{Module,Function,String}}
end
ExInfo(ex::Expr) = ExInfo(Expr[ex], SigInfo[], TypeInfo[], Tuple{Module,Function,String}[])

"""
    get_extended_data(ext::ExtendedData, owner::Symbol) -> ext::Union{ExtendedData,Nothing}

Retrieve extension data for a specific owner from the linked list.
Returns `nothing` if no data is found for the given owner.
"""
function get_extended_data(ext::ExtendedData, owner::Symbol)
    while true
        ext.owner === owner && return ext
        isdefined(ext, :next) || break
        ext = ext.next
    end
    return nothing
end

"""
    get_extended_data(siginfo::SigInfo, owner::Symbol) -> ext::Union{ExtendedData,Nothing}

Retrieve extension data for a specific owner from the `SigInfo`'s extension data.
Returns `nothing` if no data is found for the given owner.
"""
get_extended_data(siginfo::SigInfo, owner::Symbol) = get_extended_data(siginfo.ext, owner)

"""
    replace_extended_data(ext::ExtendedData, owner::Symbol, @nospecialize(data)) -> new_ext::ExtendedData

Replace extension data for a specific owner, or add it if not present.
Returns a new `ExtendedData` linked list with the updated data.
"""
function replace_extended_data(ext::ExtendedData, owner::Symbol, @nospecialize(data))
    if isdefined(ext, :next)
        if ext.owner === owner
            # Base case: this is the node to replace
            return ExtendedData(owner, data, ext.next)
        else
            # Recursive case: rebuild this node with updated next
            return ExtendedData(ext.owner, ext.data, replace_extended_data(ext.next, owner, data))
        end
    else
        if ext.owner === owner
            # If this is the last node and we found the owner, just return new node
            return ExtendedData(owner, data)
        else
            # If this is the last node and we haven't found the owner, add new node at end
            return ExtendedData(ext.owner, ext.data, ExtendedData(owner, data))
        end
    end
end

"""
    replace_extended_data(siginfo::SigInfo, owner::Symbol, @nospecialize(data)) -> new_siginfo::SigInfo

Replace extension data for a specific owner in a `SigInfo`, or add it if not present.
Returns a new `SigInfo` with the updated extension data.
"""
function replace_extended_data(siginfo::SigInfo, owner::Symbol, @nospecialize(data))
    new_ext = replace_extended_data(siginfo.ext, owner, data)
    return SigInfo(siginfo.mt, siginfo.sig, new_ext)
end

const ExprsInfos = OrderedDict{RelocatableExpr,Union{Nothing,Vector{Union{SigInfo,TypeInfo}}}}

# `TypePredictions`
#
# Per-revision record of whether the incoming source preserves each type it
# (re)defines, built by `predict_changes!` before any deletions are applied.
#
# - `preserved[(mod, name)]` is `true` when evaluating the new code is predicted to
#   keep the existing binding `mod.name`: the new definition is equivalent per the
#   runtime's own redefinition checks (`Core.resolve_typegroup`'s internal
#   equivalence test, or `Core._equiv_typedef`/`Core._typebody!` on older
#   versions), so evaluation reuses the existing type and the `const` re-assert
#   leaves the binding partition untouched. `delete_missing!` consults this to skip the expensive subtype-tree
#   walk in `handle_type_deletion!` (issue #1022).
# - `skipped` records every `(typeinfo, oldtype)` whose deletion walk was skipped on
#   the strength of a prediction. Predictions are made before any queued change is
#   applied, so a same-revision change to a binding the type's structure depends on
#   (a field-type alias, a supertype, a parameter bound) can falsify them; `revise`
#   re-checks each entry after evaluation and runs the walk late for stale ones.
struct TypePredictions
    preserved::Dict{Tuple{Module,Symbol},Bool}
    skipped::Vector{Tuple{TypeInfo,Type}}
end
TypePredictions() = TypePredictions(Dict{Tuple{Module,Symbol},Bool}(), Tuple{TypeInfo,Type}[])

const DepDictVals = Tuple{Module,RelocatableExpr}
const DepDict = Dict{Symbol,Set{DepDictVals}}

function Base.show(io::IO, exs_infos::ExprsInfos)
    compact = get(io, :compact, false)
    if compact
        n_sigs = n_types = 0
        for (_, exinfos) in exs_infos
            exinfos === nothing && continue
            for exinfo in exinfos
                if exinfo isa SigInfo
                    n_sigs += 1
                else
                    n_types += 1
                end
            end
        end
        print(io, "ExprsInfos(<$(length(exs_infos)) expressions>, <$n_sigs signatures>, <$n_types types>)")
    else
        print(io, "ExprsInfos with the following expressions: ")
        for def_rex in keys(exs_infos)
            print(io, "\n  ")
            Base.show_unquoted(io, RelocatableExpr(unwrap(def_rex)), 2)
        end
    end
end

"""
    ModuleExprsInfos

For a particular source file, the corresponding `ModuleExprsInfos` is a mapping
`mod=>exprs=>mt_sigs` of the expressions `exprs` found in `mod` and the method table/signature pairs `mt_sigs`
that arise from them. Specifically, if `mes` is a `ModuleExprsInfos`, then `mes[mod][ex]`
is a list of method table/signature pairs that result from evaluating `ex` in `mod`. It is possible that
this returns `nothing`, which can mean either that `ex` does not define any methods
or that the method table/signature pairs have not yet been cached.

The first `mod` key is guaranteed to be the module into which this file was `include`d.

To create a `ModuleExprsInfos` from a source file, see [`Revise.parse_and_maybe_eval_source`](@ref).
"""
const ModuleExprsInfos = OrderedDict{Module,ExprsInfos}

function Base.typeinfo_prefix(::IO, mod_exs_infos::ModuleExprsInfos)
    tn = typeof(mod_exs_infos).name
    return string(tn.module, '.', tn.name), true
end

"""
    fm = ModuleExprsInfos(mod::Module)

Initialize an empty `ModuleExprsInfos` for a file that is `include`d into `mod`.
"""
ModuleExprsInfos(mod::Module) = ModuleExprsInfos(mod=>ExprsInfos())

Base.isempty(fm::ModuleExprsInfos) = length(fm) == 1 && isempty(first(values(fm)))

"""
    FileInfo(mod_exs_infos::ModuleExprsInfos, cachefile=""; mapexpr=identity)

Structure to hold the per-module expressions found when parsing a
single file.
`mod_exs_infos` holds the [`Revise.ModuleExprsInfos`](@ref) for the file.

`mapexpr` is the transform passed to `include(mapexpr, filename)` when the file was
included (`identity` for plain `include`). Parsing the file for revision applies the
same transform to each top-level expression, matching what `include` evaluated.
A file included more than once with different `mapexpr`s has one `FileInfo` per
inclusion (see `Revise.fileindices`).

Optionally, a `FileInfo` can also record the path to a cache file holding the original source code.
This is applicable only for precompiled modules and `Base`.
(This cache file is distinct from the original source file that might be edited by the
developer, and it will always hold the state
of the code when the package was precompiled or Julia's `Base` was built.)
When a cache is available, `mod_exs_infos` will be empty until the file gets edited:
the original source code gets parsed only when a revision needs to be made.

Source cache files greatly reduce the overhead of using Revise.
"""
struct FileInfo
    mod_exs_infos::ModuleExprsInfos
    mapexpr::Function                                  # transform applied to each top-level expression (identity if none)
    cachefile::String
    cachefilename::String                              # the source path as recorded inside `cachefile`; the lookup key for reading from the cache
    cachesrcid::Union{Nothing,UInt64}                  # identifies the source text `cachefile` holds for this file (`nothing` if unknown)
    cacheexprs::Vector{Tuple{Module,Expr}}             # "unprocessed" exprs, used to support @require
    extracted::Base.RefValue{Bool}                     # true if signatures have been processed from mod_exs_infos
    parsed::Base.RefValue{Bool}                        # true if mod_exs_infos have been parsed from cachefile
end
FileInfo(mod_exs_infos::ModuleExprsInfos, cachefile="", cachefilename=""; mapexpr::Function=identity, cachesrcid=nothing) =
    FileInfo(mod_exs_infos, mapexpr, cachefile, cachefilename, cachesrcid, Tuple{Module,Expr}[], Ref(false), Ref(false))

"""
    FileInfo(mod::Module, cachefile="", cachefilename=""; mapexpr=identity, cachesrcid=nothing)

Initialize an empty FileInfo for a file that is `include`d into `mod`.
"""
FileInfo(mod::Module, cachefile::AbstractString="", cachefilename::AbstractString=""; mapexpr::Function=identity, cachesrcid=nothing) =
    FileInfo(ModuleExprsInfos(mod), cachefile, cachefilename; mapexpr, cachesrcid)

FileInfo(fm::ModuleExprsInfos, fi::FileInfo) = FileInfo(fm, fi.mapexpr, fi.cachefile, fi.cachefilename, fi.cachesrcid, copy(fi.cacheexprs), Ref(fi.extracted[]), Ref(fi.parsed[]))

function Base.show(io::IO, fi::FileInfo)
    print(io, "FileInfo(")
    for (mod, exs_infos) in fi.mod_exs_infos
        show(io, mod)
        print(io, "=>")
        show(io, exs_infos)
        print(io, ", ")
    end
    if fi.mapexpr !== identity
        print(io, "with mapexpr ", fi.mapexpr, ", ")
    end
    if !isempty(fi.cachefile)
        print(io, "with cachefile ", fi.cachefile)
    end
    print(io, ')')
end

"""
    PkgData(id, path, fileinfos::Dict{String,FileInfo})

A structure holding the data required to handle a particular package.
`path` is the top-level directory defining the package,
and `fileinfos` holds the [`Revise.FileInfo`](@ref) for each file defining the package.
`cachebuildid` is the build id of the precompile cache this session loaded the package
from, and identifies the source snapshot the `FileInfo`s read from that cache (see
[`Revise.rewritten_caches`](@ref)); it is zero for packages not loaded from a cache.
`cacheio` is a handle held open on that cache file, which on platforms where a rename
does not disturb open handles keeps the snapshot readable even after another process
replaces the file; it is `nothing` when no handle is held.

For the `PkgData` associated with `Main` (e.g., for files loaded with [`includet`](@ref)),
the corresponding `path` entry will be empty.
"""
mutable struct PkgData
    info::PkgFiles
    fileinfos::Vector{FileInfo}
    requirements::Vector{PkgId}
    cachebuildid::UInt128        # build id of the precompile cache this session loaded (0 if none)
    cacheio::Union{Nothing,IOStream}   # handle held open on that cache file, where the platform allows it
end

PkgData(id::PkgId, path) = PkgData(PkgFiles(id, path), FileInfo[], PkgId[], zero(UInt128), nothing)
PkgData(id::PkgId, ::Nothing) = PkgData(id, "")
function PkgData(id::PkgId)
    bp = basepath(id)
    if !isempty(bp) && !isabspath(bp)
        bp = abspath(bp)
    end
    PkgData(id, bp)
end

# Abstraction interface for PkgData
Base.PkgId(pkgdata::PkgData) = PkgId(pkgdata.info)
CodeTracking.basedir(pkgdata::PkgData) = basedir(pkgdata.info)
CodeTracking.srcfiles(pkgdata::PkgData) = srcfiles(pkgdata.info)

function fileindex(info::PkgData, file::AbstractString)
    for (i, f) in enumerate(srcfiles(info))
        String(f) == String(file) && return i
    end
    return nothing
end

# A single file may be `include`d into several modules, in which case it appears
# once in `srcfiles` per inclusion, each with its own `FileInfo` (issue #730).
# `fileindices` returns every such index so a revision updates all of them.
function fileindices(info::PkgData, file::AbstractString)
    sf = String(file)
    return Int[i for (i, f) in enumerate(srcfiles(info)) if String(f) == sf]
end

function hasfile(info::PkgData, file::AbstractString)
    if isabspath(file)
        file = relpath(file, info)
    end
    fileindex(info, file) !== nothing
end

function fileinfo(pkgdata::PkgData, file::String)
    i = fileindex(pkgdata, file)
    i === nothing && error("file ", file, " not found")
    return pkgdata.fileinfos[i]
end
fileinfo(pkgdata::PkgData, i::Int) = pkgdata.fileinfos[i]

function Base.push!(pkgdata::PkgData, pr::Pair{<:AbstractString,FileInfo})
    push!(srcfiles(pkgdata), pr.first)
    push!(pkgdata.fileinfos, pr.second)
    return pkgdata
end

function Base.show(io::IO, pkgdata::PkgData)
    compact = get(io, :compact, false)
    print(io, "PkgData(")
    if compact
        print(io, '"', pkgdata.info.basedir, "\", ")
        nexs, nsigs, nparsed = 0, 0, 0
        for fi in pkgdata.fileinfos
            thisnexs, thisnsigs = 0, 0
            for (_, exs_infos) in fi.mod_exs_infos
                for (_, exinfos) in exs_infos
                    thisnexs += 1
                    exinfos === nothing && continue
                    thisnsigs += length(exinfos)
                end
            end
            nexs += thisnexs
            nsigs += thisnsigs
            if thisnexs > 0
                nparsed += 1
            end
        end
        print(io, nparsed, '/', length(pkgdata.fileinfos), " parsed files, ", nexs, " expressions, ", nsigs, " signatures)")
    else
        show(io, pkgdata.info.id)
        println(io, ", basedir \"", pkgdata.info.basedir, "\":")
        for (f, fi) in zip(pkgdata.info.files, pkgdata.fileinfos)
            print(io, "  \"", f, "\": ")
            show(IOContext(io, :compact=>true), fi)
            print(io, '\n')
        end
    end
end

function pkgfileless((pkgdata1,file1)::Tuple{PkgData,String}, (pkgdata2,file2)::Tuple{PkgData,String})
    # implements a partial order
    PkgId(pkgdata1) ∈ pkgdata2.requirements && return true
    PkgId(pkgdata1) == PkgId(pkgdata2) && return fileindex(pkgdata1, file1) < fileindex(pkgdata2, file2)
    return false
end

"""
    ReviseEvalException(loc::String, exc::Exception, stacktrace=nothing)

Provide additional location information about `exc`.

When running via the interpreter, the backtraces point to interpreter code rather than the original
culprit. This makes it possible to use `loc` to provide information about the frame backtrace,
and even to supply a fake backtrace.

If `stacktrace` is supplied it must be a `Vector{Any}` containing `(::StackFrame, n)` pairs where `n`
is the recursion count (typically 1).
"""
struct ReviseEvalException <: Exception
    loc::String
    exc::Exception
    stacktrace::Union{Nothing,Vector{Any}}
end
ReviseEvalException(loc::AbstractString, exc::Exception) = ReviseEvalException(loc, exc, nothing)

function Base.showerror(io::IO, ex::ReviseEvalException; blame_revise::Bool=true)
    showerror(io, ex.exc)
    st = ex.stacktrace
    if st !== nothing
        Base.show_backtrace(io, st)
    end
    if blame_revise
        println(io, "\nRevise evaluation error at ", ex.loc)
    end
end

"""
    StaleCacheError(id::PkgId, file::String)

Reports that `file` cannot be revised because the precompile cache of `id` no longer
holds the source this session loaded it from, leaving nothing to compare the current
source against. See [`Revise.rewritten_caches`](@ref).
"""
struct StaleCacheError <: Exception
    id::PkgId
    file::String
end

function Base.showerror(io::IO, err::StaleCacheError)
    print(io, """
        $(err.file) cannot be revised: the precompile cache of $(err.id.name) was rebuilt by another process, and the source this session loaded for this file is no longer recorded anywhere, so there is nothing to compare the current source against.
        The definitions in this file remain as they were when the session loaded them. Restart Julia to pick up their current state.""")
end

"""
    SignatureExtractionError(mod::Module, loc::String, exc::Exception)

Reports a failure to reconstruct method signatures using partial evaluation.
"""
struct SignatureExtractionError <: Exception
    mod::Module
    loc::String
    exc
    SignatureExtractionError(mod::Module, loc::String, @nospecialize exc) = new(mod, loc, exc)
end

function captured_stacktrace(ex::SignatureExtractionError)
    cause = ex.exc
    while cause isa ReviseEvalException
        cause.stacktrace === nothing || return cause.stacktrace
        cause = cause.exc
    end
    return nothing
end

function Base.showerror(io::IO, ex::SignatureExtractionError)
    print(io, "failed to extract method signatures for ", ex.mod, " at ", ex.loc)
    print(io, "\nRevise could not determine which methods this top-level expression defines")
    print(io, "\nwithout re-running module initialization. Generated definitions may depend")
    print(io, "\non temporary state created by earlier top-level operations, such as mutation")
    print(io, "\nor `@eval`. To make this file revisable, derive each generated definition")
    print(io, "\ndirectly from stable inputs rather than from state built by preceding")
    print(io, "\noperations. (`mode = :sigs` does not replay all assignments and side effects.)")
    print(io, "\n\ncaused by: ")
    cause = ex.exc
    while cause isa ReviseEvalException
        cause = cause.exc
    end
    showerror(io, cause)
    stacktrace = captured_stacktrace(ex)
    stacktrace === nothing || Base.show_backtrace(io, stacktrace)
end

struct GitRepoException <: Exception
    filename::String
end

function Base.showerror(io::IO, ex::GitRepoException)
    print(io, "no repository at ", ex.filename, " to track stdlibs you must build Julia from source")
end

struct LoweringException <: Exception
    ex::Expr
end

function Base.showerror(io::IO, ex::LoweringException)
    print(io, "lowering returned an exception:\n", ex.ex)
end

"""
    thunk = TaskThunk(f, args)

To facilitate precompilation and reduce latency, we avoid creation of anonymous thunks.
`thunk` can be used as an argument in `schedule(Task(thunk))`.
"""
struct TaskThunk
    f          # deliberately untyped
    args       # deliberately untyped
end

@noinline (thunk::TaskThunk)() = thunk.f(thunk.args...)
