///|
pub(all) enum PathSourceKind {
  Empty
  BarePath
  AbsolutePath
  AbsoluteUrl
} derive(Eq, Debug)

///|
pub(all) struct PathSnapshot {
  original : String
  normalized : String
  source : PathSourceKind
  query : String?
  fragment : String?
  had_query : Bool
  had_fragment : Bool
  collapsed_slashes : Bool
  removed_dot_segments : Bool
  root_fallback : Bool
} derive(Eq, Debug)

///|
pub(all) struct PathReferenceParts {
  source : PathSourceKind
  path : String
  query : String?
  fragment : String?
  had_query : Bool
  had_fragment : Bool
} derive(Eq, Debug)

///|
struct QueryFragmentParts {
  path : String
  query : String?
  fragment : String?
  had_query : Bool
  had_fragment : Bool
} derive(Eq, Debug)

///|
pub(all) struct NormalizedPathBody {
  normalized : String
  segments : Array[String]
  collapsed_slashes : Bool
  removed_dot_segments : Bool
  root_fallback : Bool
} derive(Eq, Debug)

///|
pub fn PathSourceKind::label(self : PathSourceKind) -> String {
  match self {
    Empty => "empty"
    BarePath => "bare-path"
    AbsolutePath => "absolute-path"
    AbsoluteUrl => "absolute-url"
  }
}

///|
pub fn normalize_target(input : StringView) -> PathSnapshot {
  let original = input.to_owned()
  let trimmed = input.trim()
  if trimmed.is_empty() {
    {
      original,
      normalized: "/",
      source: Empty,
      query: None,
      fragment: None,
      had_query: false,
      had_fragment: false,
      collapsed_slashes: false,
      removed_dot_segments: false,
      root_fallback: true,
    }
  } else {
    let parsed = split_reference(trimmed)
    let normalized_path = normalize_path_body(parsed.path)
    {
      original,
      normalized: normalized_path.normalized,
      source: parsed.source,
      query: parsed.query,
      fragment: parsed.fragment,
      had_query: parsed.had_query,
      had_fragment: parsed.had_fragment,
      collapsed_slashes: normalized_path.collapsed_slashes,
      removed_dot_segments: normalized_path.removed_dot_segments,
      root_fallback: normalized_path.root_fallback,
    }
  }
}

///|
pub fn extract_path(input : StringView) -> String {
  normalize_target(input).normalized
}

///|
pub fn path_segments(path : StringView) -> Array[String] {
  let normalized = normalize_path_body(path)
  normalized.segments
}

///|
pub fn path_depth(path : StringView) -> Int {
  path_segments(path).length()
}

///|
pub fn path_leaf(path : StringView) -> String {
  let segments = path_segments(path)
  if segments.is_empty() {
    "/"
  } else {
    segments[segments.length() - 1]
  }
}

///|
pub fn path_parent(path : StringView) -> String {
  let segments = path_segments(path)
  if segments.length() <= 1 {
    "/"
  } else {
    let parent : Array[String] = []
    for index, segment in segments.iter2() {
      if index < segments.length() - 1 {
        parent.push(segment)
      }
    }
    join_segments(parent, false)
  }
}

///|
pub fn path_has_trailing_slash(path : StringView) -> Bool {
  let text = path.trim()
  text.has_suffix("/")
}

///|
pub fn path_without_trailing_slash(path : StringView) -> String {
  let text = normalize_target(path).normalized
  if text == "/" {
    text
  } else {
    trim_trailing_slash(text)
  }
}

///|
pub fn path_extension(path : StringView) -> String? {
  let leaf = path_leaf(path)
  match leaf.split_once(".") {
    Some((_, ext)) => if ext.is_empty() { None } else { Some(ext.to_owned()) }
    None => None
  }
}

///|
pub fn path_starts_with_segment(
  path : StringView,
  segment : StringView,
) -> Bool {
  let needle = segment.trim()
  if needle.is_empty() {
    false
  } else {
    let segments = path_segments(path)
    !segments.is_empty() && segments[0] == needle.to_owned()
  }
}

///|
pub fn path_contains_segment(path : StringView, segment : StringView) -> Bool {
  let needle = segment.trim()
  if needle.is_empty() {
    false
  } else {
    let segments = path_segments(path)
    segments.any(part => part == needle.to_owned())
  }
}

///|
pub fn path_join(segments : Array[String]) -> String {
  join_segments(segments, false)
}

///|
pub fn PathSnapshot::as_path(self : PathSnapshot) -> String {
  self.normalized
}

///|
pub fn PathSnapshot::to_line(self : PathSnapshot) -> String {
  let parts : Array[String] = []
  parts.push(self.source.label())
  parts.push(self.normalized)
  if self.had_query {
    match self.query {
      Some(query) => parts.push("?" + query)
      None => ()
    }
  }
  if self.had_fragment {
    match self.fragment {
      Some(fragment) => parts.push("#" + fragment)
      None => ()
    }
  }
  parts.join(" ")
}

///|
pub fn PathSnapshot::to_markdown(self : PathSnapshot) -> String {
  let lines : Array[String] = []
  lines.push("- Source: " + self.source.label())
  lines.push("- Normalized: " + self.normalized)
  lines.push("- Original: " + self.original)
  lines.push("- Query present: " + bool_label(self.had_query))
  lines.push("- Fragment present: " + bool_label(self.had_fragment))
  lines.push("- Collapsed slashes: " + bool_label(self.collapsed_slashes))
  lines.push("- Removed dot segments: " + bool_label(self.removed_dot_segments))
  lines.push("- Root fallback: " + bool_label(self.root_fallback))
  lines.join("\n")
}

///|
pub fn PathSnapshot::is_root(self : PathSnapshot) -> Bool {
  self.normalized == "/"
}

///|
pub fn PathSnapshot::has_query(self : PathSnapshot) -> Bool {
  self.had_query
}

///|
pub fn PathSnapshot::has_fragment(self : PathSnapshot) -> Bool {
  self.had_fragment
}

///|
pub fn PathSnapshot::debug_flags(self : PathSnapshot) -> String {
  let flags : Array[String] = []
  if self.had_query {
    flags.push("query")
  }
  if self.had_fragment {
    flags.push("fragment")
  }
  if self.collapsed_slashes {
    flags.push("collapsed-slashes")
  }
  if self.removed_dot_segments {
    flags.push("dot-segments")
  }
  if self.root_fallback {
    flags.push("root-fallback")
  }
  if flags.is_empty() {
    "stable"
  } else {
    flags.join(",")
  }
}

///|
fn split_reference(input : StringView) -> PathReferenceParts {
  let trimmed = input.trim()
  if trimmed.is_empty() {
    {
      source: Empty,
      path: "/",
      query: None,
      fragment: None,
      had_query: false,
      had_fragment: false,
    }
  } else if trimmed.has_prefix("http://") || trimmed.has_prefix("https://") {
    let after_scheme = match trimmed.split_once("://") {
      Some((_, rest)) => rest
      None => trimmed
    }
    let after_host = match after_scheme.split_once("/") {
      Some((_, rest)) => rest
      None => ""
    }
    let parts = split_query_fragment(after_host)
    {
      source: AbsoluteUrl,
      path: parts.path,
      query: parts.query,
      fragment: parts.fragment,
      had_query: parts.had_query,
      had_fragment: parts.had_fragment,
    }
  } else if trimmed.has_prefix("/") {
    let parts = split_query_fragment(trimmed)
    {
      source: AbsolutePath,
      path: parts.path,
      query: parts.query,
      fragment: parts.fragment,
      had_query: parts.had_query,
      had_fragment: parts.had_fragment,
    }
  } else {
    let parts = split_query_fragment(trimmed)
    {
      source: BarePath,
      path: "/" + parts.path,
      query: parts.query,
      fragment: parts.fragment,
      had_query: parts.had_query,
      had_fragment: parts.had_fragment,
    }
  }
}

///|
fn split_query_fragment(input : StringView) -> QueryFragmentParts {
  let mut path = input
  let mut query : String? = None
  let mut fragment : String? = None
  let mut had_query = false
  let mut had_fragment = false
  match path.split_once("#") {
    Some((left, right)) => {
      had_fragment = true
      fragment = Some(right.to_owned())
      path = left
    }
    None => ()
  }
  match path.split_once("?") {
    Some((left, right)) => {
      had_query = true
      query = Some(right.to_owned())
      path = left
    }
    None => ()
  }
  { path: path.to_owned(), query, fragment, had_query, had_fragment }
}

///|
fn normalize_path_body(input : StringView) -> NormalizedPathBody {
  let text = input.trim()
  if text.is_empty() {
    {
      normalized: "/",
      segments: [],
      collapsed_slashes: false,
      removed_dot_segments: false,
      root_fallback: true,
    }
  } else {
    let raw_segments = text.split("/")
    let mut collapsed_slashes = false
    let mut removed_dot_segments = false
    let mut stack : Array[String] = []
    let mut seen_segment = false
    let mut previous_empty = false
    let mut leading_empty_count = 0
    for segment in raw_segments {
      let part = segment.trim()
      if part.is_empty() {
        if seen_segment && previous_empty {
          collapsed_slashes = true
        } else if !seen_segment {
          leading_empty_count = leading_empty_count + 1
          if leading_empty_count > 1 {
            collapsed_slashes = true
          }
        }
        if seen_segment {
          previous_empty = true
        }
      } else if part == "." {
        removed_dot_segments = true
        seen_segment = true
        previous_empty = false
      } else if part == ".." {
        removed_dot_segments = true
        stack = drop_last_segment(stack)
        seen_segment = true
        previous_empty = false
      } else {
        if seen_segment && previous_empty {
          collapsed_slashes = true
        }
        stack.push(part.to_owned())
        seen_segment = true
        previous_empty = false
      }
    }
    let trailing_slash = text.has_suffix("/") && !stack.is_empty()
    let normalized = join_segments(stack, trailing_slash)
    {
      normalized,
      segments: stack,
      collapsed_slashes,
      removed_dot_segments,
      root_fallback: normalized == "/",
    }
  }
}

///|
fn drop_last_segment(segments : Array[String]) -> Array[String] {
  if segments.is_empty() {
    segments
  } else {
    let result : Array[String] = []
    let limit = segments.length() - 1
    for index, segment in segments.iter2() {
      if index < limit {
        result.push(segment)
      }
    }
    result
  }
}

///|
fn join_segments(segments : Array[String], trailing_slash : Bool) -> String {
  if segments.is_empty() {
    "/"
  } else {
    let body = segments.join("/")
    if trailing_slash {
      "/" + body + "/"
    } else {
      "/" + body
    }
  }
}

///|
fn trim_trailing_slash(path : String) -> String {
  if path == "/" {
    path
  } else if path.has_suffix("/") {
    match path.strip_suffix("/") {
      Some(body) => trim_trailing_slash(body.to_owned())
      None => path
    }
  } else {
    path
  }
}

///|
fn bool_label(value : Bool) -> String {
  if value {
    "yes"
  } else {
    "no"
  }
}