///|
pub(all) struct PathInfo {
  raw : String
  normalized : String
  segments : Array[String]
  dirname : String
  basename : String
  stem : String
  extension : String?
  depth : Int
  hidden : Bool
} derive(Eq)

///|
pub fn path_info(raw : String) -> Result[PathInfo, ReslockError] {
  let normalized = match normalize_path(raw) {
    Ok(path) => path
    Err(err) => return Err(err)
  }
  let segments = split_to_array(normalized, "/")
  let basename = path_basename_from_segments(segments)
  let dirname = path_dirname_from_segments(segments)
  let stem = path_stem_from_basename(basename)
  let extension = path_extension_from_basename(basename)
  Ok({
    raw,
    normalized,
    segments,
    dirname,
    basename,
    stem,
    extension,
    depth: segments.length(),
    hidden: basename.has_prefix(".") && basename.length() > 1,
  })
}

///|
pub fn path_segments(raw : String) -> Result[Array[String], ReslockError] {
  match normalize_path(raw) {
    Ok(path) => Ok(split_to_array(path, "/"))
    Err(err) => Err(err)
  }
}

///|
pub fn path_basename(raw : String) -> Result[String, ReslockError] {
  match path_info(raw) {
    Ok(info) => Ok(info.basename)
    Err(err) => Err(err)
  }
}

///|
pub fn path_dirname(raw : String) -> Result[String, ReslockError] {
  match path_info(raw) {
    Ok(info) => Ok(info.dirname)
    Err(err) => Err(err)
  }
}

///|
pub fn path_stem(raw : String) -> Result[String, ReslockError] {
  match path_info(raw) {
    Ok(info) => Ok(info.stem)
    Err(err) => Err(err)
  }
}

///|
pub fn path_extension(raw : String) -> Result[String?, ReslockError] {
  match path_info(raw) {
    Ok(info) => Ok(info.extension)
    Err(err) => Err(err)
  }
}

///|
pub fn path_depth(raw : String) -> Result[Int, ReslockError] {
  match path_info(raw) {
    Ok(info) => Ok(info.depth)
    Err(err) => Err(err)
  }
}

///|
pub fn path_is_hidden(raw : String) -> Result[Bool, ReslockError] {
  match path_info(raw) {
    Ok(info) => Ok(info.hidden)
    Err(err) => Err(err)
  }
}

///|
pub fn path_parent_chain(raw : String) -> Result[Array[String], ReslockError] {
  let info = match path_info(raw) {
    Ok(info) => info
    Err(err) => return Err(err)
  }
  let chain : Array[String] = []
  let prefix_parts : Array[String] = []
  let mut seen = 0
  let total = info.segments.length()
  for segment in info.segments {
    seen = seen + 1
    if seen < total {
      prefix_parts.push(segment)
      chain.push(prefix_parts.join("/"))
    }
  }
  Ok(chain)
}

///|
pub fn path_without_extension(raw : String) -> Result[String, ReslockError] {
  match path_info(raw) {
    Ok(info) =>
      if info.extension is Some(_) {
        Ok(path_without_last_extension(info))
      } else {
        Ok(info.normalized)
      }
    Err(err) => Err(err)
  }
}

///|
pub fn path_with_extension(
  raw : String,
  extension : String,
) -> Result[String, ReslockError] {
  let info = match path_info(raw) {
    Ok(info) => info
    Err(err) => return Err(err)
  }
  if extension.trim().is_empty() {
    return Ok(info.normalized)
  }
  let clean = extension.trim().to_owned().to_lower()
  let stem = info.stem
  if stem == "" {
    Ok(info.normalized)
  } else if info.dirname == "." {
    Ok(stem + "." + clean)
  } else {
    Ok(info.dirname + "/" + stem + "." + clean)
  }
}

///|
pub fn path_is_readme_like(raw : String) -> Result[Bool, ReslockError] {
  match path_info(raw) {
    Ok(info) =>
      Ok(
        is_named_document(info.basename, [
          "readme", "readme.md", "readme.txt", "readme.mdx",
        ]),
      )
    Err(err) => Err(err)
  }
}

///|
pub fn path_is_license_like(raw : String) -> Result[Bool, ReslockError] {
  match path_info(raw) {
    Ok(info) =>
      Ok(
        is_named_document(info.basename, [
          "license", "license.md", "license.txt", "copying", "copyright",
        ]),
      )
    Err(err) => Err(err)
  }
}

///|
pub fn path_is_changelog_like(raw : String) -> Result[Bool, ReslockError] {
  match path_info(raw) {
    Ok(info) =>
      Ok(
        is_named_document(info.basename, [
          "changelog", "changes", "news", "history",
        ]),
      )
    Err(err) => Err(err)
  }
}

///|
pub fn path_is_document_like(raw : String) -> Result[Bool, ReslockError] {
  match path_info(raw) {
    Ok(info) =>
      Ok(
        path_is_readme_name(info.basename) ||
        path_is_license_name(info.basename) ||
        path_is_changelog_name(info.basename) ||
        path_is_document_extension(info.extension),
      )
    Err(err) => Err(err)
  }
}

///|
pub fn path_is_config_like(raw : String) -> Result[Bool, ReslockError] {
  match path_info(raw) {
    Ok(info) =>
      Ok(
        path_is_config_extension(info.extension) ||
        path_is_config_name(info.basename),
      )
    Err(err) => Err(err)
  }
}

///|
pub fn path_is_source_like(raw : String) -> Result[Bool, ReslockError] {
  match path_info(raw) {
    Ok(info) =>
      Ok(
        path_is_source_extension(info.extension) ||
        path_is_script_extension(info.extension) ||
        path_is_shader_extension(info.extension),
      )
    Err(err) => Err(err)
  }
}

///|
pub fn path_is_media_like(raw : String) -> Result[Bool, ReslockError] {
  match path_info(raw) {
    Ok(info) =>
      Ok(
        path_is_image_extension(info.extension) ||
        path_is_audio_extension(info.extension) ||
        path_is_video_extension(info.extension) ||
        path_is_font_extension(info.extension),
      )
    Err(err) => Err(err)
  }
}

///|
pub fn path_is_archive_like(raw : String) -> Result[Bool, ReslockError] {
  match path_info(raw) {
    Ok(info) => Ok(path_is_archive_extension(info.extension))
    Err(err) => Err(err)
  }
}

///|
pub fn path_is_generated_like(raw : String) -> Result[Bool, ReslockError] {
  match path_info(raw) {
    Ok(info) =>
      Ok(
        info.normalized.has_prefix("generated/") ||
        info.normalized.has_prefix("gen/") ||
        info.normalized.has_prefix("build/") ||
        info.normalized.has_prefix("dist/") ||
        info.normalized.has_prefix("cache/") ||
        info.normalized.contains("/generated/") ||
        info.normalized.contains("/gen/") ||
        info.normalized.contains("/build/") ||
        info.normalized.contains("/dist/") ||
        info.normalized.contains("/cache/"),
      )
    Err(err) => Err(err)
  }
}

///|
pub fn render_path_info(info : PathInfo) -> String {
  let out = StringBuilder()
  out.write_string(info.normalized)
  out.write_string(" [")
  out.write_string("depth=")
  out.write_string(info.depth.to_string())
  out.write_string(", basename=")
  out.write_string(info.basename)
  out.write_string(", dirname=")
  out.write_string(info.dirname)
  out.write_string(", stem=")
  out.write_string(info.stem)
  out.write_string(", extension=")
  match info.extension {
    Some(extension) => out.write_string(extension)
    None => out.write_string("-")
  }
  if info.hidden {
    out.write_string(", hidden")
  }
  out.write_string("]")
  out.to_string()
}

///|
fn path_basename_from_segments(segments : Array[String]) -> String {
  let mut basename = ""
  for segment in segments {
    basename = segment
  }
  basename
}

///|
fn path_dirname_from_segments(segments : Array[String]) -> String {
  if segments.length() <= 1 {
    return "."
  }
  let prefix_parts : Array[String] = []
  let mut seen = 0
  let total = segments.length()
  for segment in segments {
    seen = seen + 1
    if seen < total {
      prefix_parts.push(segment)
    }
  }
  prefix_parts.join("/")
}

///|
fn path_stem_from_basename(basename : String) -> String {
  if basename.has_prefix(".") {
    return basename
  }
  let pieces = split_to_array(basename, ".")
  if pieces.length() <= 1 {
    return basename
  }
  let out = StringBuilder()
  let mut seen = 0
  let total = pieces.length()
  for piece in pieces {
    seen = seen + 1
    if seen < total {
      if seen > 1 {
        out.write_string(".")
      }
      out.write_string(piece)
    }
  }
  out.to_string()
}

///|
fn path_extension_from_basename(basename : String) -> String? {
  if basename.has_prefix(".") {
    return None
  }
  let pieces = split_to_array(basename, ".")
  if pieces.length() <= 1 {
    return None
  }
  let mut extension = ""
  for piece in pieces {
    extension = piece
  }
  if extension == "" {
    None
  } else {
    Some(extension.to_lower())
  }
}

///|
fn is_named_document(basename : String, names : Array[String]) -> Bool {
  let lower = basename.to_lower()
  for name in names {
    if lower == name {
      return true
    }
  }
  false
}

///|
fn path_is_readme_name(basename : String) -> Bool {
  is_named_document(basename, [
    "readme", "readme.md", "readme.txt", "readme.mdx",
  ])
}

///|
fn path_is_license_name(basename : String) -> Bool {
  is_named_document(basename, [
    "license", "license.md", "license.txt", "copying", "copyright",
  ])
}

///|
fn path_is_changelog_name(basename : String) -> Bool {
  is_named_document(basename, ["changelog", "changes", "news", "history"])
}

///|
fn path_is_document_extension(extension : String?) -> Bool {
  match extension {
    Some(value) =>
      value == "md" ||
      value == "markdown" ||
      value == "mdx" ||
      value == "rst" ||
      value == "adoc" ||
      value == "txt" ||
      value == "pdf" ||
      value == "html" ||
      value == "htm" ||
      value == "epub"
    None => false
  }
}

///|
fn path_is_config_extension(extension : String?) -> Bool {
  match extension {
    Some(value) =>
      value == "toml" ||
      value == "yaml" ||
      value == "yml" ||
      value == "json" ||
      value == "ini" ||
      value == "cfg" ||
      value == "conf" ||
      value == "env" ||
      value == "xml"
    None => false
  }
}

///|
fn path_is_config_name(basename : String) -> Bool {
  is_named_document(basename, [
    "moon.mod", "moon.pkg", "package.json", "tsconfig.json", "cargo.toml", "go.mod",
  ])
}

///|
fn path_is_source_extension(extension : String?) -> Bool {
  match extension {
    Some(value) =>
      value == "moon" ||
      value == "mbt" ||
      value == "rs" ||
      value == "go" ||
      value == "c" ||
      value == "cc" ||
      value == "cpp" ||
      value == "h" ||
      value == "hpp" ||
      value == "java" ||
      value == "kt" ||
      value == "swift" ||
      value == "py" ||
      value == "rb" ||
      value == "js" ||
      value == "ts" ||
      value == "tsx" ||
      value == "jsx" ||
      value == "lua" ||
      value == "sh" ||
      value == "bash" ||
      value == "zsh"
    None => false
  }
}

///|
fn path_is_script_extension(extension : String?) -> Bool {
  match extension {
    Some(value) =>
      value == "sh" ||
      value == "bash" ||
      value == "zsh" ||
      value == "ps1" ||
      value == "bat" ||
      value == "cmd" ||
      value == "py" ||
      value == "pl" ||
      value == "rb" ||
      value == "js" ||
      value == "ts"
    None => false
  }
}

///|
fn path_is_shader_extension(extension : String?) -> Bool {
  match extension {
    Some(value) =>
      value == "glsl" ||
      value == "wgsl" ||
      value == "vert" ||
      value == "frag" ||
      value == "comp" ||
      value == "spv"
    None => false
  }
}

///|
fn path_is_image_extension(extension : String?) -> Bool {
  match extension {
    Some(value) =>
      value == "png" ||
      value == "jpg" ||
      value == "jpeg" ||
      value == "gif" ||
      value == "webp" ||
      value == "bmp" ||
      value == "ico" ||
      value == "avif" ||
      value == "heic" ||
      value == "svg" ||
      value == "svgz" ||
      value == "tif" ||
      value == "tiff" ||
      value == "psd" ||
      value == "xcf"
    None => false
  }
}

///|
fn path_is_audio_extension(extension : String?) -> Bool {
  match extension {
    Some(value) =>
      value == "mp3" ||
      value == "wav" ||
      value == "ogg" ||
      value == "flac" ||
      value == "aac" ||
      value == "m4a" ||
      value == "opus" ||
      value == "mid" ||
      value == "midi"
    None => false
  }
}

///|
fn path_is_video_extension(extension : String?) -> Bool {
  match extension {
    Some(value) =>
      value == "mp4" ||
      value == "mov" ||
      value == "mkv" ||
      value == "webm" ||
      value == "avi" ||
      value == "m4v" ||
      value == "mpg" ||
      value == "mpeg" ||
      value == "ogv" ||
      value == "flv"
    None => false
  }
}

///|
fn path_is_font_extension(extension : String?) -> Bool {
  match extension {
    Some(value) =>
      value == "ttf" ||
      value == "otf" ||
      value == "woff" ||
      value == "woff2" ||
      value == "eot" ||
      value == "fon"
    None => false
  }
}

///|
fn path_is_archive_extension(extension : String?) -> Bool {
  match extension {
    Some(value) =>
      value == "zip" ||
      value == "tar" ||
      value == "gz" ||
      value == "tgz" ||
      value == "bz2" ||
      value == "xz" ||
      value == "7z" ||
      value == "rar" ||
      value == "zst" ||
      value == "cab"
    None => false
  }
}

///|
fn split_to_array(value : String, delimiter : String) -> Array[String] {
  let parts : Array[String] = []
  for part in value.split(delimiter) {
    parts.push(part.to_owned())
  }
  parts
}

///|
fn path_without_last_extension(info : PathInfo) -> String {
  let out_parts : Array[String] = []
  let mut seen = 0
  let total = info.segments.length()
  for segment in info.segments {
    seen = seen + 1
    if seen == total {
      out_parts.push(info.stem)
    } else {
      out_parts.push(segment)
    }
  }
  out_parts.join("/")
}