///|
/// A parsed RFC 4514 distinguished name. `rdns` is the ordered list of
/// relative distinguished names; each RDN is an ordered list of attribute
/// type/value pairs (a multi-valued RDN has more than one pair).
pub struct Dn {
  rdns : Array[Array[(String, String)]]
} derive(Eq, @debug.Debug)

///|
pub fn Dn::empty() -> Dn {
  { rdns: [] }
}

///|
/// Parse an RFC 4514 DN string.
pub fn parse_dn(s : String) -> Result[Dn, LdapError] {
  result_of_ldap(fn() raise LdapError {
    let p = DnParser::new(s)
    let dn = p.parse_dn()
    if p.pos != p.data.length() {
      raise invalid_dn("unexpected trailing characters at offset \{p.pos}")
    }
    dn
  })
}

///|
pub fn Dn::parse(s : String) -> Result[Dn, LdapError] {
  parse_dn(s)
}

///|
pub fn Dn::to_string(self : Dn) -> String {
  let sb = StringBuilder()
  for i in 0.. 0 {
      sb.write_char(',')
    }
    let rdn = self.rdns[i]
    for j in 0.. 0 {
        sb.write_char('+')
      }
      let (attr_type, attr_value) = rdn[j]
      sb.write_string(attr_type)
      sb.write_char('=')
      sb.write_string(escape_dn_value(attr_value))
    }
  }
  sb.to_string()
}

///|
pub fn Dn::is_empty(self : Dn) -> Bool {
  self.rdns.is_empty()
}

///|
/// The number of RDNs.
pub fn Dn::length(self : Dn) -> Int {
  self.rdns.length()
}

///|
/// Escape a DN attribute value for serialization. Escapes the special
/// characters `, + " \ < > ; =`, NUL and leading `#`, plus leading and
/// trailing spaces, using the `\XX` hex form. Runs of non-special bytes are
/// decoded as UTF-8 so multi-byte characters survive the round trip.
pub fn escape_dn_value(value : String) -> String {
  let bytes = @utf8.encode(value)
  let n = bytes.length()
  let sb = StringBuilder()
  let run : Array[Byte] = []
  for i in 0.. Result[String, LdapError] {
  result_of_ldap(fn() raise LdapError {
    let p = DnParser::new(s)
    let bytes = p.read_value_bytes()
    @utf8.decode_lossy(bytes[:])
  })
}

///|
priv struct DnParser {
  data : Bytes
  mut pos : Int
}

///|
fn DnParser::new(s : String) -> DnParser {
  { data: @utf8.encode(s), pos: 0 }
}

///|
fn invalid_dn(msg : String) -> LdapError {
  LdapError::InvalidDn(msg)
}

///|
fn DnParser::peek(self : DnParser) -> Int? {
  if self.pos >= self.data.length() {
    return None
  }
  Some(self.data.get(self.pos).unwrap().to_int())
}

///|
fn DnParser::advance(self : DnParser) -> Unit {
  self.pos = self.pos + 1
}

///|
fn DnParser::parse_dn(self : DnParser) -> Dn raise LdapError {
  let rdns : Array[Array[(String, String)]] = []
  if self.pos >= self.data.length() {
    return { rdns, }
  }
  while true {
    let rdn = self.parse_rdn()
    rdns.push(rdn)
    match self.peek() {
      Some(c) if c == ','.to_int() => {
        self.advance()
        if self.pos >= self.data.length() {
          raise invalid_dn("trailing comma")
        }
      }
      _ => break
    }
  }
  { rdns, }
}

///|
fn DnParser::parse_rdn(
  self : DnParser,
) -> Array[(String, String)] raise LdapError {
  let pairs : Array[(String, String)] = []
  while true {
    let pair = self.parse_attr()
    pairs.push(pair)
    match self.peek() {
      Some(c) if c == '+'.to_int() => self.advance()
      _ => break
    }
  }
  pairs
}

///|
fn DnParser::parse_attr(self : DnParser) -> (String, String) raise LdapError {
  let attr_type = self.read_attr_type()
  if attr_type.is_empty() {
    raise invalid_dn("missing attribute type at offset \{self.pos}")
  }
  match self.peek() {
    Some(c) if c == '='.to_int() => self.advance()
    _ => raise invalid_dn("expected '=' at offset \{self.pos}")
  }
  let value_bytes = self.read_value_bytes()
  let value = @utf8.decode_lossy(value_bytes[:])
  (attr_type, value)
}

///|
fn DnParser::read_attr_type(self : DnParser) -> String {
  let sb = StringBuilder()
  while true {
    match self.peek() {
      Some(c) if is_dn_attr_char(c) => {
        sb.write_char(c.unsafe_to_char())
        self.advance()
      }
      _ => break
    }
  }
  sb.to_string()
}

///|
fn is_dn_attr_char(c : Int) -> Bool {
  if c >= 'a'.to_int() && c <= 'z'.to_int() {
    return true
  }
  if c >= 'A'.to_int() && c <= 'Z'.to_int() {
    return true
  }
  if c >= '0'.to_int() && c <= '9'.to_int() {
    return true
  }
  c == '-'.to_int() || c == '.'.to_int() || c == '_'.to_int()
}

///|
fn is_dn_special(c : Int) -> Bool {
  c == ','.to_int() ||
  c == '='.to_int() ||
  c == '+'.to_int() ||
  c == '<'.to_int() ||
  c == '>'.to_int() ||
  c == ';'.to_int() ||
  c == '"'.to_int() ||
  c == '\\'.to_int() ||
  c == '#'.to_int() ||
  c == '['.to_int() ||
  c == ']'.to_int() ||
  c == ' '.to_int()
}

///|
fn DnParser::read_value_bytes(self : DnParser) -> Bytes raise LdapError {
  let out : Array[Byte] = []
  while true {
    match self.peek() {
      Some(c) if c == ','.to_int() || c == '+'.to_int() => break
      Some(c) if c == '\\'.to_int() => {
        self.advance()
        let h1 = self.peek()
        match h1 {
          None => raise invalid_dn("truncated escape at end of input")
          Some(a) => {
            self.advance()
            let h2 = self.peek()
            let hi = hex_nibble(a)
            let lo = match h2 {
              Some(b) => hex_nibble(b)
              None => -1
            }
            if hi >= 0 && lo >= 0 {
              self.advance()
              out.push(((hi << 4) | lo).to_byte())
            } else if is_dn_special(a) {
              // RFC 4514 single-character escape, e.g. `\,` or `\\`.
              out.push(a.to_byte())
            } else {
              raise invalid_dn("invalid escape sequence at offset \{self.pos}")
            }
          }
        }
      }
      Some(c) => {
        self.advance()
        out.push(c.to_byte())
      }
      None => break
    }
  }
  Bytes::from_array(out)
}