(** Module hierarchy management

    This module handles hierarchical module names (like A.B.C) and their
    mapping to filesystem paths. It manages the lookup and caching of module
    files across directory structures. *)

(** The type of a module hierarchy - a list of module names *)
type t = Modname.t list

(** File entry types representing different kinds of source files *)
type file_entry =
  | FileEntry of (Filepath.filepath * Filepath.filepath)
    (** Normal source file: (root_path, full_path) *)
  | GeneratedFileEntry of (Filepath.filepath * Filepath.filepath * Filepath.filename)
    (** Generated source file: (root_path, full_path, generated_filename) *)
  | DirectoryEntry of (Filepath.filepath * Filepath.filepath)
    (** Directory representing a module: (root_path, full_path) *)

(** {1 Exceptions} *)

exception EmptyModuleHierarchy
(** Raised when attempting to create an empty module hierarchy *)

(** {1 Construction and Conversion} *)

val make : Modname.t list -> t
(** [make mods] creates a module hierarchy from a list of module names.
    @raise EmptyModuleHierarchy if the list is empty *)

val of_string : string -> t
(** [of_string "A.B.C"] creates a module hierarchy by splitting on dots *)

val of_modname : Modname.t -> t
(** [of_modname m] creates a single-level hierarchy from module name [m] *)

val of_filename : Filepath.filename -> t
(** [of_filename fn] creates a hierarchy from a filename,
    removing extension and capitalizing *)

val to_string : t -> string
(** [to_string hier] converts to dot-separated string (e.g., "A.B.C") *)

val to_node : t -> Modname.t list
(** [to_node hier] returns the underlying module name list *)

(** {1 Hierarchy Navigation} *)

val root : t -> Modname.t
(** [root hier] returns the first module in the hierarchy *)

val leaf : t -> Modname.t
(** [leaf hier] returns the last module in the hierarchy *)

val parent : t -> t option
(** [parent hier] returns the parent hierarchy, or [None] if single-level.
    Example: parent [A; B; C] = Some [A; B] *)

val lvl : t -> int
(** [lvl hier] returns the depth level (0-indexed from root).
    Example: lvl [A; B; C] = 2 *)

val append : t -> Modname.t -> t
(** [append hier m] appends module [m] to hierarchy *)

(** {1 Path Conversion} *)

val to_dirpath : t -> Filepath.filepath
(** [to_dirpath hier] converts hierarchy to directory path.
    Example: [A; B; C] -> "a/b" (excludes leaf) *)

val add_prefix : Filepath.filepath -> t -> Filepath.filepath
(** [add_prefix prefix hier] combines prefix path with hierarchy path,
    intelligently handling overlapping components *)

val ml_to_ext : Filepath.filepath -> Filetype.t -> Filepath.filepath
(** [ml_to_ext path ext] changes file extension of path to [ext] *)

(** {1 File Lookup} *)

val get_filepath : Filepath.filepath -> t -> Filetype.t -> file_entry option
(** [get_filepath root hier ext] searches for a file matching the hierarchy
    with the given extension. Returns cached result if available. *)

val to_filename : t -> Filepath.filepath -> file_entry option
(** [to_filename hier root] finds the .ml file for hierarchy *)

val to_interface : t -> Filepath.filepath -> file_entry option
(** [to_interface hier root] finds the .mli file for hierarchy *)

val to_directory : t -> Filepath.filepath -> file_entry option
(** [to_directory hier root] finds the directory for hierarchy *)

val to_generators : t -> Filepath.filepath -> file_entry option
(** [to_generators hier root] finds source files matching custom generators defined in .obuild *)

val get_file_entry : t -> Filepath.filepath list -> file_entry
(** [get_file_entry hier paths] searches for hierarchy across multiple root paths,
    trying all lookup methods (filename, directory, generators, interface).
    @raise Not_found if hierarchy not found in any path *)

val get_file_entry_maybe : t -> file_entry option
(** [get_file_entry_maybe hier] returns cached file entry if available *)

(** {1 File Entry Operations} *)

val file_entry_to_string : file_entry -> string
(** [file_entry_to_string entry] converts file entry to debug string *)

val get_src_file : Filepath.filepath -> file_entry -> Filepath.filepath
(** [get_src_file dst_dir entry] returns the source file path from an entry *)

val get_dest_file : Filepath.filepath -> Filetype.t -> t -> Filepath.filepath
(** [get_dest_file dst_dir ext hier] computes destination file path
    for hierarchy with given extension in destination directory.
    @raise Not_found if hierarchy not cached *)

val get_dest_file_ext : Filepath.filepath -> t -> (Filetype.t -> Filetype.t) -> Filepath.filepath
(** [get_dest_file_ext dst_dir hier ext_fn] computes destination file path
    using [ext_fn] to transform the source file type.
    @raise Not_found if hierarchy not cached *)

val register_synthetic_entry : t -> Filepath.filepath -> Filepath.filepath -> unit
(** [register_synthetic_entry hier root_path full_path] registers a synthetic file entry
    for modules that will be generated during build (e.g., cstubs-generated modules,
    generate-block modules). This allows get_dest_file to work for these modules even
    before the source file exists. Replaces any existing entry (which may have been
    cached during dependency analysis before the module was identified as synthetic). *)

val register_generated_entry : t -> Filepath.filepath -> Filepath.filepath -> Filepath.filename -> unit
(** [register_generated_entry hier root_path src_path output_file] registers a generated
    file entry for modules produced by generators (e.g., atdgen). This allows modules
    like Ollama_t (from ollama.atd) to be discovered before generation.
    - [hier]: the module hierarchy (e.g., Ollama_t)
    - [root_path]: the source directory containing the generator input
    - [src_path]: full path to the source file (e.g., lib/ollama.atd)
    - [output_file]: the generated output filename (e.g., ollama_t.ml) *)

(** {1 Global Generated Module Registry} *)

val register_generated_module : string -> unit
(** [register_generated_module name] registers a module name as globally generated
    (from a generate block in any target). This allows dependent targets to
    recognize generated modules before their generating target is prepared. *)

val is_generated_module : string -> bool
(** [is_generated_module name] returns true if the module was registered as
    generated via [register_generated_module]. *)

val clear : unit -> unit
(** [clear ()] resets all caches (file entry cache and generated module registry). *)
