///|
/// ETag parsing errors
pub(all) enum ETagError {
  Empty
  InvalidFormat
  MissingQuote
  InvalidCharacter
} derive(Show, Eq)

///|
/// Entity Tag (ETag) (RFC 9110 Section 8.8.3)
pub(all) struct EntityTag {
  weak : Bool
  tag : String
} derive(Show, Eq)

///|
pub fn EntityTag::parse(input : String) -> Result[EntityTag, ETagError] {
  let s = etag_trim_string(input)
  if s.length() == 0 {
    return Err(Empty)
  }
  let (weak, start_idx) = if s.length() >= 2 &&
    (etag_string_starts_with(s, "W/") || etag_string_starts_with(s, "w/")) {
    (true, 2)
  } else {
    (false, 0)
  }

  // Check for opening quote (ASCII 34 = ")
  if start_idx >= s.length() || etag_char_at(s, start_idx) != 34 {
    return Err(MissingQuote)
  }

  // Find closing quote
  let end_quote = etag_find_char_from(s, 34, start_idx + 1)
  match end_quote {
    None => Err(MissingQuote)
    Some(idx) => {
      let tag = etag_string_slice(s, start_idx + 1, idx)
      // Validate tag characters (etagc: 0x21 (!) or 0x23-0x7E or >= 0x80)
      if !is_valid_etag(tag) {
        return Err(InvalidCharacter)
      }
      Ok({ weak, tag })
    }
  }
}

///|
pub fn EntityTag::strong(tag : String) -> Result[EntityTag, ETagError] {
  if !is_valid_etag(tag) {
    return Err(InvalidCharacter)
  }
  Ok({ weak: false, tag })
}

///|
pub fn EntityTag::weak(tag : String) -> Result[EntityTag, ETagError] {
  if !is_valid_etag(tag) {
    return Err(InvalidCharacter)
  }
  Ok({ weak: true, tag })
}

///|
pub fn EntityTag::is_weak(self : EntityTag) -> Bool {
  self.weak
}

///|
pub fn EntityTag::is_strong(self : EntityTag) -> Bool {
  !self.weak
}

///|
pub fn EntityTag::tag(self : EntityTag) -> String {
  self.tag
}

///|
pub fn EntityTag::strong_compare(self : EntityTag, other : EntityTag) -> Bool {
  !self.weak && !other.weak && self.tag == other.tag
}

///|
pub fn EntityTag::weak_compare(self : EntityTag, other : EntityTag) -> Bool {
  self.tag == other.tag
}

///|
pub fn EntityTag::to_string(self : EntityTag) -> String {
  if self.weak {
    "W/\"" + self.tag + "\""
  } else {
    "\"" + self.tag + "\""
  }
}

///|
/// ETag list for If-Match, If-None-Match headers
pub(all) enum ETagList {
  Any
  Tags(Array[EntityTag])
} derive(Show, Eq)

///|
pub fn parse_etag_list(input : String) -> Result[ETagList, ETagError] {
  let s = etag_trim_string(input)
  if s.length() == 0 {
    return Err(Empty)
  }
  if s == "*" {
    return Ok(Any)
  }
  let parts = etag_split_string(s, ",")
  let etags : Array[EntityTag] = []
  let mut part_idx = 0
  while part_idx < parts.length() {
    let part = parts[part_idx]
    let trimmed = etag_trim_string(part)
    if trimmed.length() > 0 {
      let parse_result = EntityTag::parse(trimmed)
      match parse_result {
        Ok(etag) => etags.push(etag)
        Err(e) => return Err(e)
      }
    }
    part_idx = part_idx + 1
  }
  if etags.length() == 0 {
    return Err(Empty)
  }
  Ok(Tags(etags))
}

///|
pub fn ETagList::is_any(self : ETagList) -> Bool {
  match self {
    Any => true
    Tags(_) => false
  }
}

///|
pub fn ETagList::contains_weak(self : ETagList, etag : EntityTag) -> Bool {
  match self {
    Any => true
    Tags(tags) => {
      let mut idx = 0
      while idx < tags.length() {
        if tags[idx].weak_compare(etag) {
          return true
        }
        idx = idx + 1
      }
      false
    }
  }
}

///|
pub fn ETagList::contains_strong(self : ETagList, etag : EntityTag) -> Bool {
  match self {
    Any => true
    Tags(tags) => {
      let mut idx = 0
      while idx < tags.length() {
        if tags[idx].strong_compare(etag) {
          return true
        }
        idx = idx + 1
      }
      false
    }
  }
}

///|
pub fn ETagList::to_string(self : ETagList) -> String {
  match self {
    Any => "*"
    Tags(tags) => {
      let strs = array_map(tags, fn(t) { t.to_string() })
      etag_join_strings(strs, ", ")
    }
  }
}

///|

///|
/// Helper functions

///|

///|
fn is_valid_etag(tag : String) -> Bool {
  let mut idx = 0
  while idx < tag.length() {
    let ci = etag_char_at(tag, idx)
    // Check if character is valid etagc: 0x21 (!), 0x23-0x7E, or >= 0x80
    if ci == 33 || (ci >= 35 && ci <= 126) || ci >= 128 {
      idx = idx + 1
    } else {
      return false
    }
  }
  true
}

///|
fn etag_trim_string(s : String) -> String {
  str_trim_string(s)
}

///|
fn etag_string_starts_with(s : String, prefix : String) -> Bool {
  if prefix.length() > s.length() {
    return false
  }
  let mut idx = 0
  while idx < prefix.length() {
    if s[idx] != prefix[idx] {
      return false
    }
    idx = idx + 1
  }
  true
}

///|
fn etag_char_at(s : String, idx : Int) -> Int {
  str_char_at(s, idx)
}

///|
fn etag_find_char_from(s : String, target : Int, start : Int) -> Int? {
  let mut idx = start
  while idx < s.length() {
    if etag_char_at(s, idx) == target {
      return Some(idx)
    }
    idx = idx + 1
  }
  None
}

///|
fn etag_string_slice(s : String, start : Int, end : Int) -> String {
  str_string_slice(s, start, end)
}

///|
fn etag_split_string(s : String, sep : String) -> Array[String] {
  str_split_string(s, sep)
}

///|
fn[T, U] array_map(arr : Array[T], f : (T) -> U) -> Array[U] {
  let result : Array[U] = []
  let mut idx = 0
  while idx < arr.length() {
    let mapped = f(arr[idx])
    result.push(mapped)
    idx = idx + 1
  }
  result
}

///|
fn etag_join_strings(strs : Array[String], sep : String) -> String {
  let mut result = ""
  let mut idx = 0
  while idx < strs.length() {
    if idx > 0 {
      result = result + sep
    }
    result = result + strs[idx]
    idx = idx + 1
  }
  result
}