///|
/// Transport-independent IRCv3 CAP 302 state. Call observe for server CAP replies.
/// The caller owns authentication, registration timing and socket I/O.
pub struct Capabilities {
  mut offered : Map[String, String?]
  mut active : Map[String, Bool]
  staged : Map[String, Array[String]]
  mut ready : Bool
}

///|
pub fn Capabilities::new() -> Capabilities {
  { offered: Map([]), active: Map([]), staged: Map([]), ready: false, }
}

///|
pub fn Capabilities::available(self : Capabilities, name : String) -> Bool {
  self.offered.contains(name)
}

///|
pub fn Capabilities::value(self : Capabilities, name : String) -> String? {
  match self.offered.get(name) {
    Some(value) => value
    None => None
  }
}

///|
pub fn Capabilities::enabled(self : Capabilities, name : String) -> Bool {
  self.active.contains(name)
}

///|
pub fn Capabilities::listing_complete(self : Capabilities) -> Bool {
  self.ready
}

///|
fn capability_token(
  token : String,
  values : Bool,
  disabling : Bool,
) -> (String, String?, Bool) raise IrcError {
  let negative = token.has_prefix("-")
  let token = if negative && disabling { token[1:].to_owned() } else { token }
  let parts = token.split("=").map(x => x.to_owned()).collect()
  let name = parts[0]
  if name.is_empty() ||
    name.has_prefix("-") ||
    name
    .to_array()
    .iter()
    .any(c => c.to_int() <= 32 || c.to_int() >= 127 || c == ':' || c == '*') {
    raise Invalid("invalid capability name")
  }
  if parts.length() > 1 && !values {
    raise Invalid("capability values not allowed here")
  }
  (
    name,
    if parts.length() > 1 {
      Some(parts[1:].to_owned().join("="))
    } else {
      None
    },
    negative,
  )
}

///|
/// Build a bounded REQ. Does not enable capabilities before server ACK.
pub fn Capabilities::request(
  self : Capabilities,
  names : Array[String],
) -> Message raise IrcError {
  if names.is_empty() {
    raise Invalid("empty capability request")
  }
  for token in names {
    let (name, _, negative) = capability_token(token, false, true)
    if !negative &&
      !self.available(name) &&
      !(name == "cap-notify" && self.ready) {
      raise Invalid("capability not advertised")
    }
    if negative && name == "cap-notify" {
      raise Invalid("CAP 302 requires cap-notify")
    }
  }
  let msg = Message::{
    tags: [],
    prefix: None,
    command: "CAP",
    params: ["REQ", names.join(" ")],
  }
  ignore(msg.encode())
  msg
}

///|
/// Multiline LS/LIST/ACK commits atomically. Malformed replies leave state unchanged.
/// LS values are opaque and case-sensitive; duplicate names use the last value.
pub fn Capabilities::observe(
  self : Capabilities,
  message : Message,
) -> Unit raise IrcError {
  if message.command != "CAP" {
    return
  }
  let p = message.params
  if p.length() < 3 || p.length() > 4 {
    raise Invalid("invalid CAP reply")
  }
  let kind = p[1]
  if !["LS", "LIST", "ACK", "NAK", "NEW", "DEL"].contains(kind) {
    raise Invalid("unsupported CAP reply")
  }
  let continued = p.length() == 4
  if continued && (p[2] != "*" || !["LS", "LIST", "ACK"].contains(kind)) {
    raise Invalid("invalid CAP continuation")
  }
  let tokens = p[p.length() - 1]
    .split(" ")
    .filter(x => !x.is_empty())
    .map(x => x.to_owned())
    .collect()
  let combined = match self.staged.get(kind) {
    Some(tokens) => tokens.copy()
    None => []
  }
  for token in tokens {
    ignore(
      capability_token(
        token,
        kind == "LS" || kind == "NEW",
        kind == "ACK" || kind == "NAK",
      ),
    )
    combined.push(token)
  }
  if combined.length() > 512 || combined.join(" ").length() > 32768 {
    raise Invalid("CAP list limit")
  }
  if continued {
    self.staged[kind] = combined
    return
  }
  let offered = self.offered.copy()
  let active = self.active.copy()
  if kind == "LS" {
    offered.clear()
  }
  if kind == "LIST" {
    active.clear()
  }
  for token in combined {
    let (name, value, negative) = capability_token(
      token,
      kind == "LS" || kind == "NEW",
      kind == "ACK" || kind == "NAK",
    )
    match kind {
      "LS" | "NEW" => offered[name] = value
      "LIST" => active[name] = true
      "ACK" => if negative { active.remove(name) } else { active[name] = true }
      "DEL" => {
        offered.remove(name)
        active.remove(name)
      }
      _ => ()
    }
  }
  if offered.length() > 512 || active.length() > 512 {
    raise Invalid("CAP state limit")
  }
  // CAP 302 implicitly enables cap-notify after the server accepts LS.
  if kind == "LS" || kind == "LIST" {
    active["cap-notify"] = true
  }
  self.offered = offered
  self.active = active
  self.staged.remove(kind)
  if kind == "LS" {
    self.ready = true
  }
}