///|
pub(all) struct LinkAttribute {
  name : String
  value : String?
} derive(Eq, Debug)

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

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

///|
pub(all) struct CoreLink {
  target : String
  attributes : Array[LinkAttribute]
} derive(Eq, Debug)

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

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

///|
pub fn CoreLink::attribute(self : CoreLink, name : String) -> String? {
  for attribute in self.attributes {
    if attribute.name == name {
      return attribute.value
    }
  }
  None
}

///|
fn link_token(value : Int) -> Bool {
  (value >= 48 && value <= 57) ||
  (value >= 65 && value <= 90) ||
  (value >= 97 && value <= 122) ||
  value == 33 ||
  value == 35 ||
  value == 36 ||
  value == 37 ||
  value == 38 ||
  value == 39 ||
  value == 42 ||
  value == 43 ||
  value == 45 ||
  value == 46 ||
  value == 94 ||
  value == 95 ||
  value == 96 ||
  value == 124 ||
  value == 126
}

///|
fn unsigned_decimal(text : String, maximum : Int64) -> Result[Int64, Failure] {
  if text.is_empty() {
    return Err(Invalid("empty decimal attribute"))
  }
  let mut value = 0L
  for character in text.iter() {
    let digit = character.to_int() - 48
    if digit < 0 || digit > 9 || value > (maximum - digit.to_int64()) / 10L {
      return Err(Invalid("invalid or overflowing decimal attribute"))
    }
    value = value * 10L + digit.to_int64()
  }
  Ok(value)
}

///|
pub fn CoreLink::validate(
  self : CoreLink,
  limits : Limits,
) -> Result[Unit, Failure] {
  if self.target.is_empty() ||
    @utf8.encode(self.target).length() > limits.max_uri_bytes {
    return Err(Invalid("invalid link target length"))
  }
  for character in self.target.iter() {
    let value = character.to_int()
    if value <= 32 ||
      value >= 127 ||
      value == 60 ||
      value == 62 ||
      value == 34 ||
      value == 92 {
      return Err(Invalid("link target must be an escaped ASCII URI reference"))
    }
  }
  match percent_decode(self.target) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  if self.attributes.length() > limits.max_link_attributes {
    return Err(Capacity("too many link attributes"))
  }
  let singleton : Array[String] = []
  for attribute in self.attributes {
    if attribute.name.is_empty() ||
      !attribute.name.iter().all(fn(c) { link_token(c.to_int()) }) {
      return Err(Invalid("invalid link attribute name"))
    }
    if ["rt", "if", "sz", "ct", "title", "anchor", "rel"].contains(
        attribute.name,
      ) {
      if singleton.contains(attribute.name) {
        return Err(Invalid("duplicate singleton link attribute"))
      }
      singleton.push(attribute.name)
    }
    match attribute.value {
      None =>
        if attribute.name == "rt" ||
          attribute.name == "if" ||
          attribute.name == "sz" ||
          attribute.name == "ct" {
          return Err(Invalid("standard link attribute requires a value"))
        }
      Some(value) => {
        if @utf8.encode(value).length() > limits.max_uri_bytes {
          return Err(Capacity("link attribute value too long"))
        }
        if value.iter().any(fn(c) { c.to_int() < 32 || c.to_int() == 127 }) {
          return Err(Invalid("control byte in link value"))
        }
        if attribute.name == "sz" {
          match unsigned_decimal(value, 4294967295L) {
            Err(e) => return Err(e)
            Ok(_) => ()
          }
        }
        if attribute.name == "ct" {
          for format in value.split(" ") {
            match unsigned_decimal(format.to_owned(), 65535L) {
              Err(e) => return Err(e)
              Ok(_) => ()
            }
          }
        }
        if (attribute.name == "rt" || attribute.name == "if") &&
          value.is_empty() {
          return Err(Invalid("empty relation attribute"))
        }
      }
    }
  }
  Ok(())
}

///|
priv struct LinkParser {
  bytes : Bytes
  mut cursor : Int
  limits : Limits
}

///|
fn LinkParser::skip_space(self : LinkParser) -> Unit {
  while self.cursor < self.bytes.length() &&
        (self.bytes[self.cursor] == b' ' || self.bytes[self.cursor] == b'\t') {
    self.cursor = self.cursor + 1
  }
}

///|
fn LinkParser::token(self : LinkParser) -> Result[String, Failure] {
  let start = self.cursor
  while self.cursor < self.bytes.length() &&
        link_token(self.bytes[self.cursor].to_int()) {
    self.cursor = self.cursor + 1
  }
  if self.cursor == start {
    return Err(Syntax(start, "expected link token"))
  }
  decode_utf8(slice_bytes(self.bytes, start, self.cursor))
}

///|
fn LinkParser::quoted(self : LinkParser) -> Result[String, Failure] {
  let output : Array[Byte] = []
  self.cursor = self.cursor + 1
  while self.cursor < self.bytes.length() {
    let byte = self.bytes[self.cursor]
    self.cursor = self.cursor + 1
    if byte == b'"' {
      return decode_utf8(Bytes::from_array(output))
    }
    if byte == b'\\' {
      if self.cursor == self.bytes.length() {
        return Err(Syntax(self.cursor, "truncated quoted escape"))
      }
      let escaped = self.bytes[self.cursor]
      if escaped.to_int() < 32 || escaped.to_int() == 127 {
        return Err(Syntax(self.cursor, "control byte in quoted escape"))
      }
      output.push(escaped)
      self.cursor = self.cursor + 1
    } else {
      if byte.to_int() < 32 || byte.to_int() == 127 {
        return Err(Syntax(self.cursor - 1, "control byte in quoted string"))
      }
      output.push(byte)
    }
    if output.length() > self.limits.max_uri_bytes {
      return Err(Capacity("quoted link value is too long"))
    }
  }
  Err(Syntax(self.cursor, "unterminated quoted string"))
}

///|
fn LinkParser::link(self : LinkParser) -> Result[CoreLink, Failure] {
  self.skip_space()
  if self.cursor >= self.bytes.length() || self.bytes[self.cursor] != b'<' {
    return Err(Syntax(self.cursor, "expected opening angle bracket"))
  }
  self.cursor = self.cursor + 1
  let start = self.cursor
  while self.cursor < self.bytes.length() && self.bytes[self.cursor] != b'>' {
    self.cursor = self.cursor + 1
  }
  if self.cursor == self.bytes.length() {
    return Err(Syntax(self.cursor, "unclosed link target"))
  }
  let target = match decode_utf8(slice_bytes(self.bytes, start, self.cursor)) {
    Err(e) => return Err(e)
    Ok(v) => v
  }
  self.cursor = self.cursor + 1
  let attributes : Array[LinkAttribute] = []
  self.skip_space()
  while self.cursor < self.bytes.length() && self.bytes[self.cursor] == b';' {
    if attributes.length() >= self.limits.max_link_attributes {
      return Err(Capacity("too many link attributes"))
    }
    self.cursor = self.cursor + 1
    self.skip_space()
    let name = match self.token() {
      Err(e) => return Err(e)
      Ok(v) => v
    }
    self.skip_space()
    let mut value : String? = None
    if self.cursor < self.bytes.length() && self.bytes[self.cursor] == b'=' {
      self.cursor = self.cursor + 1
      self.skip_space()
      let parsed = if self.cursor < self.bytes.length() &&
        self.bytes[self.cursor] == b'"' {
        self.quoted()
      } else {
        self.token()
      }
      value = Some(
        match parsed {
          Err(e) => return Err(e)
          Ok(v) => v
        },
      )
    }
    attributes.push({ name, value, })
    self.skip_space()
  }
  let link : CoreLink = { target, attributes, }
  match link.validate(self.limits) {
    Err(e) => Err(e)
    Ok(_) => Ok(link)
  }
}

///|
pub fn parse_links(
  payload : Bytes,
  limits : Limits,
) -> Result[Array[CoreLink], Failure] {
  match limits.validate() {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  if payload.length() > limits.max_payload {
    return Err(Capacity("discovery payload exceeds limit"))
  }
  let parser : LinkParser = { bytes: payload, cursor: 0, limits, }
  let links : Array[CoreLink] = []
  parser.skip_space()
  while parser.cursor < payload.length() {
    if links.length() >= limits.max_links {
      return Err(Capacity("too many discovery links"))
    }
    match parser.link() {
      Err(e) => return Err(e)
      Ok(link) => links.push(link)
    }
    if parser.cursor == payload.length() {
      break
    }
    if payload[parser.cursor] != b',' {
      return Err(Syntax(parser.cursor, "expected comma between links"))
    }
    parser.cursor = parser.cursor + 1
    parser.skip_space()
    if parser.cursor == payload.length() {
      return Err(Syntax(parser.cursor, "trailing link separator"))
    }
  }
  Ok(links)
}

///|
fn quote_link_value(text : String) -> String {
  let characters : Array[Char] = ['"']
  for character in text.iter() {
    if character == '"' || character == '\\' {
      characters.push('\\')
    }
    characters.push(character)
  }
  characters.push('"')
  String::from_array(characters)
}

///|
pub fn serialize_links(
  links : Array[CoreLink],
  limits : Limits,
) -> Result[Bytes, Failure] {
  match limits.validate() {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  if links.length() > limits.max_links {
    return Err(Capacity("too many links"))
  }
  let mut text = ""
  for i = 0; i < links.length(); i = i + 1 {
    let link = links[i]
    match link.validate(limits) {
      Err(e) => return Err(e)
      Ok(_) => ()
    }
    if i > 0 {
      text = text + ","
    }
    text = text + "<" + link.target + ">"
    for attribute in link.attributes {
      text = text + ";" + attribute.name
      match attribute.value {
        None => ()
        Some(value) => text = text + "=" + quote_link_value(value)
      }
    }
    if @utf8.encode(text).length() > limits.max_payload {
      return Err(Capacity("discovery serialization exceeds payload limit"))
    }
  }
  Ok(@utf8.encode(text))
}

///|
pub(all) struct LinkFilter {
  name : String
  value : String
  prefix : Bool
} derive(Eq, Debug)

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

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

///|
pub fn LinkFilter::parse(query : String) -> Result[LinkFilter, Failure] {
  let (name, value) = match query.split_once("=") {
    None => return Err(Invalid("discovery filter requires name=value"))
    Some((name, value)) => (name.to_owned(), value.to_owned())
  }
  if name.is_empty() || !name.iter().all(fn(c) { link_token(c.to_int()) }) {
    return Err(Invalid("invalid discovery filter name"))
  }
  let prefix = value.has_suffix("*")
  let value = if prefix { value[:value.length() - 1].to_owned() } else { value }
  if value.contains("*") {
    return Err(Invalid("only a final wildcard is supported"))
  }
  if value.iter().any(fn(c) { c.to_int() < 32 || c.to_int() == 127 }) {
    return Err(Invalid("invalid filter value"))
  }
  Ok({ name, value, prefix, })
}

///|
fn LinkFilter::matches_value(self : LinkFilter, value : String) -> Bool {
  if self.prefix {
    value.has_prefix(self.value)
  } else {
    value == self.value
  }
}

///|
pub fn LinkFilter::matches(self : LinkFilter, link : CoreLink) -> Bool {
  if self.name == "href" {
    return self.matches_value(link.target)
  }
  for attribute in link.attributes {
    if attribute.name != self.name {
      continue
    }
    match attribute.value {
      None => if self.prefix && self.value.is_empty() { return true }
      Some(value) =>
        if ["rt", "if", "rel", "ct"].contains(self.name) {
          for token in value.split(" ") {
            if self.matches_value(token.to_owned()) {
              return true
            }
          }
        } else if self.matches_value(value) {
          return true
        }
    }
  }
  false
}

///|
pub struct Discovery {
  priv limits : Limits
  priv mut links : Array[CoreLink]
}

///|
pub fn Discovery::new(limits : Limits) -> Result[Discovery, Failure] {
  match limits.validate() {
    Err(e) => Err(e)
    Ok(_) => Ok({ limits, links: [], })
  }
}

///|
pub fn Discovery::add(
  self : Discovery,
  link : CoreLink,
) -> Result[Unit, Failure] {
  match link.validate(self.limits) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  if !link.target.has_prefix("/") ||
    link.target.has_prefix("//") ||
    link.attribute("anchor") is Some(_) {
    return Err(
      Unsupported(
        "local discovery catalog requires origin paths without anchors",
      ),
    )
  }
  let candidate = self.links.filter(fn(existing) {
    existing.target != link.target
  })
  candidate.push({ target: link.target, attributes: link.attributes.copy(), })
  match serialize_links(candidate, self.limits) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  self.links = candidate
  Ok(())
}

///|
pub fn Discovery::remove(self : Discovery, target : String) -> Bool {
  let before = self.links.length()
  self.links = self.links.filter(fn(link) { link.target != target })
  before != self.links.length()
}

///|
pub fn Discovery::links(self : Discovery) -> Array[CoreLink] {
  self.links.map(fn(link) {
    { target: link.target, attributes: link.attributes.copy(), }
  })
}

///|
pub fn Discovery::handle(self : Discovery, message : Message) -> Response {
  if message.option_values(11) != [b".well-known", b"core"] {
    return Response::error(132)
  }
  if message.code.verb() != Some(Get) {
    return Response::error(133)
  }
  match message_uint(message, 17, 40L) {
    Err(_) => return Response::error(130)
    Ok(v) => if v != 40L { return Response::error(134) }
  }
  let queries = match request_queries(message) {
    Err(_) => return Response::error(128)
    Ok(v) => v
  }
  if queries.length() > 1 {
    return Response::error(128)
  }
  let filter = if queries.is_empty() {
    None
  } else {
    match LinkFilter::parse(queries[0]) {
      Err(_) => return Response::error(128)
      Ok(v) => Some(v)
    }
  }
  let selected = match filter {
    None => self.links
    Some(filter) => self.links.filter(fn(link) { filter.matches(link) })
  }
  match serialize_links(selected, self.limits) {
    Err(_) => Response::error(163)
    Ok(payload) =>
      {
        code: { value: 69, },
        options: [option_uint(12, 40L).unwrap()],
        payload,
      }
  }
}

///|
pub fn ResourceStore::discovery(
  self : ResourceStore,
  limits : Limits,
) -> Result[Discovery, Failure] {
  let discovery = match Discovery::new(limits) {
    Err(e) => return Err(e)
    Ok(v) => v
  }
  for info in self.inventory() {
    let link : CoreLink = {
      target: info.path,
      attributes: [
        { name: "ct", value: Some(info.content_format.to_string()), },
        { name: "sz", value: Some(info.size.to_string()), },
      ],
    }
    match discovery.add(link) {
      Err(e) => return Err(e)
      Ok(_) => ()
    }
  }
  Ok(discovery)
}