///|
pub(all) enum OptionKind {
  Opaque
  Unsigned
  Text
  Flag
} derive(Eq, Debug)

///|
pub(all) struct OptionSpec {
  number : Int
  name : String
  kind : OptionKind
  min_length : Int
  max_length : Int
  repeatable : Bool
  request_only : Bool
  response_only : Bool
} derive(Eq, Debug)

///|
pub extend OptionKind with Eq::{equal, not_equal}

///|
pub extend OptionKind with @debug.Debug::{to_repr}

///|
pub extend OptionSpec with Eq::{equal, not_equal}

///|
pub extend OptionSpec with @debug.Debug::{to_repr}

///|
fn option_spec(
  number : Int,
  name : String,
  kind : OptionKind,
  min_length : Int,
  max_length : Int,
  repeatable : Bool,
  request_only : Bool,
  response_only : Bool,
) -> OptionSpec {
  {
    number,
    name,
    kind,
    min_length,
    max_length,
    repeatable,
    request_only,
    response_only,
  }
}

///|
pub fn standard_option(number : Int) -> OptionSpec? {
  match number {
    1 => Some(option_spec(1, "If-Match", Opaque, 0, 8, true, true, false))
    3 => Some(option_spec(3, "Uri-Host", Text, 1, 255, false, true, false))
    4 => Some(option_spec(4, "ETag", Opaque, 1, 8, true, false, false))
    5 => Some(option_spec(5, "If-None-Match", Flag, 0, 0, false, true, false))
    7 => Some(option_spec(7, "Uri-Port", Unsigned, 0, 2, false, true, false))
    8 => Some(option_spec(8, "Location-Path", Text, 0, 255, true, false, true))
    11 => Some(option_spec(11, "Uri-Path", Text, 0, 255, true, true, false))
    12 =>
      Some(
        option_spec(12, "Content-Format", Unsigned, 0, 2, false, false, false),
      )
    14 => Some(option_spec(14, "Max-Age", Unsigned, 0, 4, false, false, true))
    15 => Some(option_spec(15, "Uri-Query", Text, 0, 255, true, true, false))
    17 => Some(option_spec(17, "Accept", Unsigned, 0, 2, false, true, false))
    20 =>
      Some(option_spec(20, "Location-Query", Text, 0, 255, true, false, true))
    35 => Some(option_spec(35, "Proxy-Uri", Text, 1, 1034, false, true, false))
    39 =>
      Some(option_spec(39, "Proxy-Scheme", Text, 1, 255, false, true, false))
    60 => Some(option_spec(60, "Size1", Unsigned, 0, 4, false, false, false))
    _ => None
  }
}

///|
pub fn option_is_critical(number : Int) -> Bool {
  (number & 1) != 0
}

///|
pub fn option_is_unsafe(number : Int) -> Bool {
  (number & 2) != 0
}

///|
pub fn option_is_cache_key(number : Int) -> Bool {
  (number & 30) != 28
}

///|
pub fn option_uint(number : Int, value : Int64) -> Result[CoapOption, Failure] {
  if value < 0L || value > 4294967295L {
    return Err(Invalid("option integer must fit unsigned 32 bits"))
  }
  let mut remaining = value
  let reverse : Array[Byte] = []
  while remaining > 0L {
    reverse.push((remaining & 255L).to_int().to_byte())
    remaining = remaining >> 8
  }
  let bytes = Bytes::makei(reverse.length(), fn(i) {
    reverse[reverse.length() - 1 - i]
  })
  Ok({ number, value: bytes, })
}

///|
pub fn option_to_uint(option : CoapOption) -> Result[Int64, Failure] {
  if option.value.length() > 4 {
    return Err(Invalid("option integer is longer than four bytes"))
  }
  let mut value = 0L
  for byte in option.value {
    value = value * 256L + byte.to_int().to_int64()
  }
  Ok(value)
}

///|
pub fn option_text(number : Int, text : String) -> CoapOption {
  { number, value: @utf8.encode(text), }
}

///|
pub fn option_to_text(option : CoapOption) -> Result[String, Failure] {
  decode_utf8(option.value)
}

///|
pub fn validate_options(
  message : Message,
  supported_critical : Array[Int],
) -> Result[Unit, Failure] {
  let seen : Array[Int] = []
  for option in message.options {
    if option.number < 0 || option.number > 65535 {
      return Err(Invalid("option number out of range"))
    }
    match standard_option(option.number) {
      None =>
        if option_is_critical(option.number) &&
          !supported_critical.contains(option.number) {
          return Err(Unsupported("unknown critical option"))
        }
      Some(spec) => {
        if option.value.length() < spec.min_length ||
          option.value.length() > spec.max_length {
          return Err(Invalid("invalid length for " + spec.name))
        }
        if (
            !spec.repeatable ||
            (option.number == 4 && message.code.is_response())
          ) &&
          seen.contains(option.number) {
          return Err(Invalid("duplicate " + spec.name))
        }
        if spec.request_only && !message.code.is_request() {
          return Err(Invalid(spec.name + " is request-only"))
        }
        if spec.response_only && !message.code.is_response() {
          return Err(Invalid(spec.name + " is response-only"))
        }
        if (option.number == 11 || option.number == 8) &&
          (option.value == b"." || option.value == b"..") {
          return Err(Invalid("dot segments are not permitted in path options"))
        }
        match spec.kind {
          Text =>
            match option_to_text(option) {
              Err(error) => return Err(error)
              Ok(text) =>
                for character in text.iter() {
                  if character.to_int() < 32 || character.to_int() == 127 {
                    return Err(Invalid("control character in " + spec.name))
                  }
                }
            }
          Unsigned =>
            match option_to_uint(option) {
              Err(error) => return Err(error)
              Ok(_) => ()
            }
          Opaque | Flag => ()
        }
        seen.push(option.number)
      }
    }
  }
  if seen.contains(1) && seen.contains(5) {
    return Err(Invalid("If-Match and If-None-Match are mutually exclusive"))
  }
  if seen.contains(35) {
    if seen.contains(3) ||
      seen.contains(7) ||
      seen.contains(11) ||
      seen.contains(15) ||
      seen.contains(39) {
      return Err(Invalid("Proxy-Uri conflicts with URI options"))
    }
    return Err(Unsupported("proxying is outside this profile"))
  }
  if seen.contains(39) {
    return Err(Unsupported("proxying is outside this profile"))
  }
  Ok(())
}

///|
pub fn message_uint(
  message : Message,
  number : Int,
  default_value : Int64,
) -> Result[Int64, Failure] {
  match message.first_option(number) {
    None => Ok(default_value)
    Some(value) => option_to_uint({ number, value, })
  }
}

///|
pub fn request_path(message : Message) -> Result[String, Failure] {
  let parts = message.option_values(11)
  let mut path = ""
  for part in parts {
    match decode_utf8(part) {
      Ok(value) => path = path + "/" + value
      Err(error) => return Err(error)
    }
  }
  if path == "" {
    Ok("/")
  } else {
    Ok(path)
  }
}

///|
pub fn request_queries(message : Message) -> Result[Array[String], Failure] {
  let result : Array[String] = []
  for part in message.option_values(15) {
    match decode_utf8(part) {
      Ok(value) => result.push(value)
      Err(error) => return Err(error)
    }
  }
  Ok(result)
}

///|
pub fn path_segments(message : Message) -> Array[Bytes] {
  let parts = message.option_values(11)
  if parts == [b""] {
    []
  } else {
    parts
  }
}