///|
/// URI classification used by packaging and dependency auditing. It never
/// touches the filesystem, which keeps analysis deterministic and safe.

///|
pub(all) enum UriKind {
  Data
  Relative
  Absolute
  Fragment
  Empty
} derive(Debug, ToJson, Eq)

///|
pub(all) struct UriInfo {
  value : String
  kind : UriKind
  scheme : String?
  media_type : String?
  encoded : Bool
} derive(Debug, ToJson, Eq)

///|
fn find_colon(value : String) -> Int? {
  let mut index = 0
  for character in value {
    if character == ':' {
      return Some(index)
    }
    index += 1
  }
  None
}

///|
fn find_character(value : String, target : Char) -> Int? {
  let mut index = 0
  for character in value {
    if character == target {
      return Some(index)
    }
    index += 1
  }
  None
}

///|
pub fn classify_uri(value : String) -> UriKind {
  if value == "" {
    Empty
  } else if value.has_prefix("#") {
    Fragment
  } else if value.has_prefix("data:") {
    Data
  } else if find_colon(value) is Some(_) {
    Absolute
  } else {
    Relative
  }
}

///|
pub fn uri_scheme(value : String) -> String? {
  match find_colon(value) {
    Some(index) => if index > 0 { Some(value[:index].to_owned()) } else { None }
    None => None
  }
}

///|
pub fn uri_data_media_type(value : String) -> String? {
  if !value.has_prefix("data:") {
    return None
  }
  let comma = find_character(value, ',')
  match comma {
    Some(index) => {
      let header = value[5:index].to_owned()
      let semicolon = find_character(header, ';')
      match semicolon {
        Some(position) => Some(header[:position].to_owned())
        None => if header == "" { None } else { Some(header) }
      }
    }
    None => None
  }
}

///|
pub fn uri_is_base64(value : String) -> Bool {
  if !value.has_prefix("data:") {
    false
  } else {
    value.contains(";base64,")
  }
}

///|
pub fn uri_is_safe_external(value : String) -> Bool {
  match classify_uri(value) {
    Relative => !value.contains("..")
    Data | Fragment | Empty => true
    Absolute => false
  }
}

///|
pub fn inspect_uri(value : String) -> UriInfo {
  {
    value,
    kind: classify_uri(value),
    scheme: uri_scheme(value),
    media_type: uri_data_media_type(value),
    encoded: uri_is_base64(value),
  }
}

///|
pub fn normalize_relative_uri(value : String) -> String {
  let replaced = value.replace(old="\\", new="/")
  let parts = replaced.split("/")
  let output : Array[String] = []
  for part in parts {
    if part == "" || part == "." {
      continue
    }
    if part == ".." {
      if output.length() > 0 {
        ignore(output.pop())
      }
    } else {
      output.push(part.to_owned())
    }
  }
  output.join("/")
}

///|
pub fn uri_extension(value : String) -> String? {
  let normalized = normalize_relative_uri(value)
  let parts : Array[StringView] = normalized.split(".").collect()
  if parts.length() > 1 {
    Some(parts[parts.length() - 1].to_owned())
  } else {
    None
  }
}

///|
pub fn uri_is_image(value : String) -> Bool {
  match uri_data_media_type(value) {
    Some(media) => media.has_prefix("image/")
    None =>
      match uri_extension(value) {
        Some(ext) =>
          ext == "png" ||
          ext == "jpg" ||
          ext == "jpeg" ||
          ext == "webp" ||
          ext == "ktx2"
        None => false
      }
  }
}

///|
pub fn uri_is_buffer(value : String) -> Bool {
  match uri_data_media_type(value) {
    Some(media) => media == "application/octet-stream"
    None =>
      match uri_extension(value) {
        Some(ext) => ext == "bin" || ext == "buffer"
        None => false
      }
  }
}

///|
pub fn document_external_uris(doc : GltfDocument) -> Array[UriInfo] {
  let output : Array[UriInfo] = []
  let deps = collect_resource_deps(doc)
  for uri in deps.external_uris {
    output.push(inspect_uri(uri))
  }
  output
}

///|
pub fn document_unsafe_uri_count(doc : GltfDocument) -> Int {
  document_external_uris(doc).fold(init=0, (count, info) => {
    if uri_is_safe_external(info.value) {
      count
    } else {
      count + 1
    }
  })
}

///|
pub fn document_image_uri_count(doc : GltfDocument) -> Int {
  document_external_uris(doc).fold(init=0, (count, info) => {
    if uri_is_image(info.value) {
      count + 1
    } else {
      count
    }
  })
}

///|
pub fn document_buffer_uri_count(doc : GltfDocument) -> Int {
  document_external_uris(doc).fold(init=0, (count, info) => {
    if uri_is_buffer(info.value) {
      count + 1
    } else {
      count
    }
  })
}