///|
/// Classification used while resolving source entries.
pub(all) enum SourcePathKind {
  Anonymous
  DataUri
  AbsoluteUri
  ProtocolRelative
  WindowsAbsolute
  PosixAbsolute
  Relative
} derive(Eq, Debug)

///|
/// Options for resolving `sourceRoot` and source entries.
pub(all) struct SourceResolverOptions {
  map_url : String?
  source_root : String?
  normalize_windows : Bool
}

///|
/// Construct source resolver options.
pub fn SourceResolverOptions::new(
  map_url? : String,
  source_root? : String,
  normalize_windows? : Bool = true,
) -> SourceResolverOptions {
  { map_url, source_root, normalize_windows }
}

///|
fn source_has_scheme(value : String) -> Bool {
  match value.split_once(":") {
    None => false
    Some((scheme, _)) => {
      if scheme.is_empty() {
        return false
      }
      for index, ch in scheme {
        let valid = (ch >= 'a' && ch <= 'z') ||
          (ch >= 'A' && ch <= 'Z') ||
          (
            index > 0 &&
            ((ch >= '0' && ch <= '9') || ch == '+' || ch == '-' || ch == '.')
          )
        if !valid {
          return false
        }
      }
      true
    }
  }
}

///|
fn source_windows_absolute(value : String) -> Bool {
  value.length() >= 3 &&
  value.get_char(1) == Some(':') &&
  (value.get_char(2) == Some('/') || value.get_char(2) == Some('\\'))
}

///|
fn source_uri_is_hierarchical(value : String) -> Bool {
  match value.split_once(":") {
    Some((_, rest)) => rest.has_prefix("/")
    None => false
  }
}

///|
/// Classify a raw source entry before resolution.
pub fn classify_source_path(source : String?) -> SourcePathKind {
  match source {
    None => Anonymous
    Some(value) =>
      if value.has_prefix("data:") {
        DataUri
      } else if value.has_prefix("//") {
        ProtocolRelative
      } else if source_windows_absolute(value) {
        WindowsAbsolute
      } else if source_has_scheme(value) {
        AbsoluteUri
      } else if value.has_prefix("/") {
        PosixAbsolute
      } else {
        Relative
      }
  }
}

///|
fn split_source_suffix(value : String) -> (String, String) {
  let mut boundary = value.length()
  for index, ch in value {
    if (ch == '?' || ch == '#') && index < boundary {
      boundary = index
    }
  }
  (value[:boundary].to_owned(), value[boundary:].to_owned())
}

///|
fn source_prefix(value : String) -> (String, String, Bool) {
  if value.has_prefix("//") {
    match value[2:].split_once("/") {
      Some((authority, path)) =>
        ("//" + authority.to_owned(), "/" + path.to_owned(), true)
      None => (value, "", true)
    }
  } else if source_windows_absolute(value) {
    (value[:2].to_owned(), value[2:].to_owned(), true)
  } else if value.has_prefix("/") {
    ("/", value[1:].to_owned(), true)
  } else {
    match value.split_once("://") {
      Some((scheme, rest)) =>
        match rest.split_once("/") {
          Some((authority, path)) =>
            (
              scheme.to_owned() + "://" + authority.to_owned(),
              "/" + path.to_owned(),
              true,
            )
          None => (value, "", true)
        }
      None =>
        match value.split_once(":") {
          Some((scheme, rest)) if rest.has_prefix("/") =>
            (scheme.to_owned() + ":", rest.to_owned(), true)
          _ => ("", value, false)
        }
    }
  }
}

///|
fn normalize_source_path(value : String, normalize_windows : Bool) -> String {
  let (path, suffix) = split_source_suffix(value)
  let input = if normalize_windows {
    path.replace_all(old="\\", new="/")
  } else {
    path
  }
  let (prefix, body, absolute) = source_prefix(input)
  let segments : Array[String] = []
  for segment in body.split("/") {
    if segment.is_empty() || segment == "." {
      continue
    }
    if segment == ".." {
      if !segments.is_empty() && segments[segments.length() - 1] != ".." {
        segments.pop() |> ignore
      } else if !absolute {
        segments.push("..")
      }
    } else {
      segments.push(segment.to_owned())
    }
  }
  let joined = segments.join("/")
  let normalized = if prefix == "/" {
    "/" + joined
  } else if prefix != "" {
    if joined == "" {
      prefix
    } else {
      prefix + "/" + joined
    }
  } else {
    joined
  }
  normalized + suffix
}

///|
fn source_directory(value : String) -> String {
  let (path, _) = split_source_suffix(value)
  match path.rev_split_once("/") {
    Some((directory, _)) => directory.to_owned()
    None => ""
  }
}

///|
fn source_join(base : String, child : String) -> String {
  if base == "" {
    child
  } else if base.has_suffix("/") {
    base + child
  } else {
    base + "/" + child
  }
}

///|
fn resolve_relative_source(
  source : String,
  base : String?,
  normalize_windows : Bool,
) -> String {
  match base {
    Some(base) =>
      normalize_source_path(source_join(base, source), normalize_windows)
    None => normalize_source_path(source, normalize_windows)
  }
}

///|
/// Resolve one source entry using ECMA-426 `sourceRoot` and a map URL.
///
/// Absolute URIs, data URIs, protocol-relative URLs and absolute filesystem
/// paths do not inherit either base. A relative `sourceRoot` is first resolved
/// against the directory containing the map.
pub fn resolve_source_url(
  source : String?,
  options? : SourceResolverOptions = SourceResolverOptions::new(),
) -> String? {
  match source {
    None => None
    Some(source) =>
      match classify_source_path(Some(source)) {
        DataUri => Some(source)
        AbsoluteUri =>
          if source_uri_is_hierarchical(source) {
            Some(normalize_source_path(source, options.normalize_windows))
          } else {
            Some(source)
          }
        ProtocolRelative | WindowsAbsolute | PosixAbsolute =>
          Some(normalize_source_path(source, options.normalize_windows))
        Anonymous => None
        Relative => {
          let map_base = options.map_url.map(source_directory)
          let root_base = match options.source_root {
            None => map_base
            Some(root) =>
              match classify_source_path(Some(root)) {
                Relative =>
                  Some(
                    resolve_relative_source(
                      root,
                      map_base,
                      options.normalize_windows,
                    ),
                  )
                _ =>
                  Some(normalize_source_path(root, options.normalize_windows))
              }
          }
          Some(
            resolve_relative_source(
              source,
              root_base,
              options.normalize_windows,
            ),
          )
        }
      }
  }
}

///|
/// Normalize a generated-file key for registry matching.
pub fn normalize_generated_file(value : String) -> String {
  match classify_source_path(Some(value)) {
    DataUri => value
    AbsoluteUri if !source_uri_is_hierarchical(value) => value
    _ => normalize_source_path(value, true)
  }
}