///|
/// Run a raising function that raises `LdapError`, converting the outcome to
/// a `Result`.
pub fn[T] result_of_ldap(f : () -> T raise LdapError) -> Result[T, LdapError] {
  Ok(f()) catch {
    e => Err(e)
  }
}

///|
fn decode_error(msg : String) -> LdapError {
  Decode(msg)
}

///|
fn expect_universal(
  value : BerValue,
  number : Int,
  constructed : Bool,
) -> Unit raise LdapError {
  if value.tag.class != Universal ||
    value.tag.number != number ||
    value.tag.constructed != constructed {
    raise decode_error("expected universal tag \{number}")
  }
}

///|
fn as_integer(value : BerValue) -> Int raise LdapError {
  expect_universal(value, tag_integer, false)
  match parse_integer(value.content, 8) {
    Ok(v) => v
    Err(e) => raise LdapError::Ber(e)
  }
}

///|
fn as_boolean(value : BerValue) -> Bool raise LdapError {
  expect_universal(value, tag_boolean, false)
  match parse_boolean(value.content) {
    Ok(v) => v
    Err(e) => raise LdapError::Ber(e)
  }
}

///|
fn as_enumerated(value : BerValue) -> Int raise LdapError {
  expect_universal(value, tag_enumerated, false)
  match parse_integer(value.content, 8) {
    Ok(v) => v
    Err(e) => raise LdapError::Ber(e)
  }
}

///|
fn as_octets(value : BerValue) -> Bytes raise LdapError {
  expect_universal(value, tag_octet_string, false)
  value.content
}

///|
fn as_ldap_string(value : BerValue) -> String raise LdapError {
  @utf8.decode_lossy(as_octets(value)[:])
}

///|
fn encode_ldap_string(s : String) -> Bytes {
  encode_octet_string(@utf8.encode(s))
}

///|
/// Encode an LDAPResult's SEQUENCE content (RFC 4511 4.1.9).
fn encode_result_content(result : LdapResult) -> Bytes {
  let mut content = encode_enumerated(result.result_code.to_int())
  content = content + encode_ldap_string(result.matched_dn)
  content = content + encode_ldap_string(result.diagnostic_message)
  match result.referral {
    Some(uris) =>
      if !uris.is_empty() {
        let mut ref_content = Bytes::new(0)
        for uri in uris {
          ref_content = ref_content + encode_ldap_string(uri)
        }
        content = content + encode_tlv(context_tag(3, true), ref_content)
      }
    None => ()
  }
  content
}

///|
fn decode_result_content(
  children : Array[BerValue],
) -> LdapResult raise LdapError {
  if children.length() < 3 {
    raise decode_error("LDAPResult has fewer than 3 elements")
  }
  let code = ResultCode::from_int(as_enumerated(children[0]))
  let matched_dn = as_ldap_string(children[1])
  let diagnostic = as_ldap_string(children[2])
  let mut referral : Array[String]? = None
  if children.length() > 3 {
    let extra = children[3]
    if extra.tag.class == ContextSpecific &&
      extra.tag.number == 3 &&
      extra.tag.constructed {
      let uris : Array[String] = []
      for child in extra.children_or_empty() {
        uris.push(as_ldap_string(child))
      }
      referral = Some(uris)
    }
  }
  { result_code: code, matched_dn, diagnostic_message: diagnostic, referral }
}

///|
/// Encode a BindRequest protocol op content.
fn encode_bind_request_content(req : BindRequest) -> Bytes {
  let mut content = encode_integer(req.version)
  content = content + encode_ldap_string(req.name)
  match req.authentication {
    Simple(password) =>
      content = content +
        encode_tlv(context_tag(0, false), @utf8.encode(password))
    Sasl(creds) => {
      let mut sasl = encode_ldap_string(creds.mechanism)
      sasl = sasl + encode_octet_string(creds.credentials)
      content = content + encode_tlv(context_tag(3, true), sasl)
    }
  }
  content
}

///|
fn decode_bind_request(
  children : Array[BerValue],
) -> BindRequest raise LdapError {
  if children.length() < 3 {
    raise decode_error("bindRequest has fewer than 3 elements")
  }
  let version = as_integer(children[0])
  let name = as_ldap_string(children[1])
  let auth = children[2]
  let authentication = if auth.tag.class == ContextSpecific &&
    auth.tag.number == 0 &&
    !auth.tag.constructed {
    Simple(@utf8.decode_lossy(auth.content[:]))
  } else if auth.tag.class == ContextSpecific &&
    auth.tag.number == 3 &&
    auth.tag.constructed {
    let kids = auth.children_or_empty()
    if kids.is_empty() {
      raise decode_error("saslCredentials has no mechanism")
    }
    let mechanism = as_ldap_string(kids[0])
    let credentials = if kids.length() > 1 {
      as_octets(kids[1])
    } else {
      Bytes::new(0)
    }
    Sasl(SaslCredentials::new(mechanism, credentials))
  } else {
    raise decode_error("invalid authentication choice")
  }
  { version, name, authentication }
}

///|
fn encode_bind_response_content(resp : BindResponse) -> Bytes {
  let mut content = encode_result_content(resp.result)
  match resp.server_sasl_creds {
    Some(creds) => content = content + encode_tlv(context_tag(7, false), creds)
    None => ()
  }
  content
}

///|
fn decode_bind_response(
  children : Array[BerValue],
) -> BindResponse raise LdapError {
  let result = decode_result_content(children)
  let mut sasl : Bytes? = None
  for child in children {
    if child.tag.class == ContextSpecific &&
      child.tag.number == 7 &&
      !child.tag.constructed {
      sasl = Some(child.content)
    }
  }
  { result, server_sasl_creds: sasl }
}

///|
/// Encode a full LDAPMessage to its BER wire form.
pub fn encode_message(msg : LdapMessage) -> Result[Bytes, LdapError] {
  result_of_ldap(fn() raise LdapError {
    if msg.message_id < 0 {
      raise decode_error("messageID must not be negative")
    }
    let mut content = encode_integer(msg.message_id)
    match msg.op {
      BindRequest(req) =>
        content = content +
          encode_tlv(application_tag(0, true), encode_bind_request_content(req))
      BindResponse(resp) =>
        content = content +
          encode_tlv(
            application_tag(1, true),
            encode_bind_response_content(resp),
          )
      UnbindRequest =>
        content = content + encode_tlv(application_tag(2, false), Bytes::new(0))
      SearchRequest(req) =>
        content = content +
          encode_tlv(
            application_tag(3, true),
            encode_search_request_content(req),
          )
      SearchResultEntry(entry) =>
        content = content +
          encode_tlv(
            application_tag(4, true),
            encode_search_entry_content(entry),
          )
      SearchResultDone(result) =>
        content = content +
          encode_tlv(application_tag(5, true), encode_result_content(result))
      SearchResultReference(refs) =>
        content = content +
          encode_tlv(application_tag(19, true), encode_ref_content(refs))
      ExtendedRequest(req) =>
        content = content +
          encode_tlv(
            application_tag(23, true),
            encode_extended_request_content(req),
          )
      ExtendedResponse(resp) =>
        content = content +
          encode_tlv(
            application_tag(24, true),
            encode_extended_response_content(resp),
          )
      AbandonRequest(id) =>
        content = content +
          encode_tlv(application_tag(16, false), integer_content(id))
      ModifyRequest(req) =>
        content = content +
          encode_tlv(
            application_tag(6, true),
            encode_modify_request_content(req),
          )
      ModifyResponse(resp) =>
        content = content +
          encode_tlv(
            application_tag(7, true),
            encode_result_content(resp.result),
          )
      AddRequest(req) =>
        content = content +
          encode_tlv(application_tag(8, true), encode_add_request_content(req))
      AddResponse(resp) =>
        content = content +
          encode_tlv(
            application_tag(9, true),
            encode_result_content(resp.result),
          )
      DelRequest(req) =>
        content = content +
          encode_tlv(application_tag(10, false), @utf8.encode(req.entry))
      DelResponse(resp) =>
        content = content +
          encode_tlv(
            application_tag(11, true),
            encode_result_content(resp.result),
          )
      ModifyDnRequest(req) =>
        content = content +
          encode_tlv(
            application_tag(12, true),
            encode_modify_dn_request_content(req),
          )
      ModifyDnResponse(resp) =>
        content = content +
          encode_tlv(
            application_tag(13, true),
            encode_result_content(resp.result),
          )
      CompareRequest(req) =>
        content = content +
          encode_tlv(
            application_tag(14, true),
            encode_compare_request_content(req),
          )
      CompareResponse(resp) =>
        content = content +
          encode_tlv(
            application_tag(15, true),
            encode_result_content(resp.result),
          )
    }
    match msg.controls {
      Some(controls) =>
        if !controls.is_empty() {
          let mut ctrl_seq = Bytes::new(0)
          for ctrl in controls {
            let mut c = encode_ldap_string(ctrl.control_type)
            if ctrl.criticality {
              c = c + encode_boolean(true)
            }
            if ctrl.control_value.length() > 0 {
              c = c + encode_octet_string(ctrl.control_value)
            }
            ctrl_seq = ctrl_seq + encode_sequence([c])
          }
          content = content + encode_tlv(context_tag(0, true), ctrl_seq)
        }
      None => ()
    }
    encode_sequence([content])
  })
}

///|
/// Decode a full LDAPMessage from its BER wire form.
pub fn decode_message(
  bytes : Bytes,
  limits : BerLimits?,
) -> Result[LdapMessage, LdapError] {
  result_of_ldap(fn() raise LdapError {
    let value = match decode_ber(bytes, limits) {
      Ok(v) => v
      Err(e) => raise LdapError::Ber(e)
    }
    expect_universal(value, tag_sequence, true)
    let children = value.children_or_empty()
    if children.length() < 2 {
      raise decode_error("LDAPMessage has fewer than 2 elements")
    }
    let message_id = as_integer(children[0])
    if message_id < 0 {
      raise LdapError::InvalidMessageId(message_id)
    }
    let op_value = children[1]
    let op = decode_protocol_op(op_value)
    let mut controls : Array[Control]? = None
    if children.length() > 2 {
      let ctrl_value = children[2]
      if ctrl_value.tag.class == ContextSpecific &&
        ctrl_value.tag.number == 0 &&
        ctrl_value.tag.constructed {
        let seq = ctrl_value.children_or_empty()
        let ctrls : Array[Control] = []
        for ctrl in seq {
          let parts = ctrl.children_or_empty()
          if parts.length() < 1 {
            raise decode_error("control has no type")
          }
          let ctype = as_ldap_string(parts[0])
          let mut critical = false
          let mut cvalue = Bytes::new(0)
          // `criticality` is optional (DEFAULT FALSE) so position varies:
          // either `type, value` or `type, criticality, value`.
          for i in 1.. ProtocolOp raise LdapError {
  if value.tag.class != Application {
    raise decode_error("protocolOp must be application class")
  }
  let kids = value.children_or_empty()
  match value.tag.number {
    0 => BindRequest(decode_bind_request(kids))
    1 => BindResponse(decode_bind_response(kids))
    2 => UnbindRequest
    3 => SearchRequest(decode_search_request(kids))
    4 => SearchResultEntry(decode_search_entry(kids))
    5 => SearchResultDone(decode_result_content(kids))
    19 => SearchResultReference(decode_ref(kids))
    6 => ModifyRequest(decode_modify_request(kids))
    7 => ModifyResponse(ModifyResponse::new(decode_result_content(kids)))
    8 => AddRequest(decode_add_request(kids))
    9 => AddResponse(AddResponse::new(decode_result_content(kids)))
    10 => DelRequest(DelRequest::new(@utf8.decode_lossy(value.content[:])))
    11 => DelResponse(DelResponse::new(decode_result_content(kids)))
    12 => ModifyDnRequest(decode_modify_dn_request(kids))
    13 => ModifyDnResponse(ModifyDnResponse::new(decode_result_content(kids)))
    14 => CompareRequest(decode_compare_request(kids))
    15 => CompareResponse(CompareResponse::new(decode_result_content(kids)))
    23 => ExtendedRequest(decode_extended_request(kids))
    24 => ExtendedResponse(decode_extended_response(kids))
    16 =>
      // Implicitly-tagged [APPLICATION 17] MessageID: primitive INTEGER content.
      match parse_integer(value.content, 8) {
        Ok(id) => AbandonRequest(id)
        Err(e) => raise LdapError::Ber(e)
      }
    n => raise LdapError::UnexpectedOp(n)
  }
}

///|
fn encode_search_request_content(req : SearchRequest) -> Bytes {
  let mut content = encode_ldap_string(req.base_object)
  content = content + encode_enumerated(req.scope.to_int())
  content = content + encode_enumerated(req.deref_aliases.to_int())
  content = content + encode_integer(req.size_limit)
  content = content + encode_integer(req.time_limit)
  content = content + encode_boolean(req.types_only)
  content = content + encode_filter(req.filter)
  let mut attrs = Bytes::new(0)
  for attr in req.attributes {
    attrs = attrs + encode_ldap_string(attr)
  }
  content = content + encode_sequence([attrs])
  content
}

///|
fn decode_search_request(
  children : Array[BerValue],
) -> SearchRequest raise LdapError {
  if children.length() < 8 {
    raise decode_error("searchRequest has fewer than 8 elements")
  }
  let base_object = as_ldap_string(children[0])
  let scope = match SearchScope::from_int(as_enumerated(children[1])) {
    Some(s) => s
    None => raise decode_error("invalid search scope")
  }
  let deref = match DerefAliases::from_int(as_enumerated(children[2])) {
    Some(d) => d
    None => raise decode_error("invalid derefAliases")
  }
  let size_limit = as_integer(children[3])
  let time_limit = as_integer(children[4])
  let types_only = as_boolean(children[5])
  let filter = decode_filter_value(children[6])
  let attributes : Array[String] = []
  let attr_seq = children[7]
  for attr in attr_seq.children_or_empty() {
    attributes.push(as_ldap_string(attr))
  }
  {
    base_object,
    scope,
    deref_aliases: deref,
    size_limit,
    time_limit,
    types_only,
    attributes,
    filter,
  }
}

///|
fn encode_search_entry_content(entry : SearchResultEntry) -> Bytes {
  let mut attrs = Bytes::new(0)
  for pa in entry.attributes {
    let mut pa_content = encode_ldap_string(pa.attr_type)
    let mut values = Bytes::new(0)
    for v in pa.values {
      values = values + encode_octet_string(v)
    }
    pa_content = pa_content + encode_set([values])
    attrs = attrs + encode_sequence([pa_content])
  }
  encode_ldap_string(entry.object_name) + encode_sequence([attrs])
}

///|
fn decode_search_entry(
  children : Array[BerValue],
) -> SearchResultEntry raise LdapError {
  if children.length() != 2 {
    raise decode_error("searchResultEntry must have two elements")
  }
  let object_name = as_ldap_string(children[0])
  let attributes : Array[PartialAttribute] = []
  let attr_list = children[1]
  for pa_value in attr_list.children_or_empty() {
    let pa_kids = pa_value.children_or_empty()
    if pa_kids.length() != 2 {
      raise decode_error("partialAttribute must have two elements")
    }
    let attr_type = as_ldap_string(pa_kids[0])
    let values : Array[Bytes] = []
    for v in pa_kids[1].children_or_empty() {
      values.push(as_octets(v))
    }
    attributes.push(PartialAttribute::new(attr_type, values))
  }
  { object_name, attributes }
}

///|
fn encode_ref_content(refs : SearchResultReference) -> Bytes {
  let mut content = Bytes::new(0)
  for uri in refs.uris {
    content = content + encode_ldap_string(uri)
  }
  content
}

///|
fn decode_ref(
  children : Array[BerValue],
) -> SearchResultReference raise LdapError {
  let out : Array[String] = []
  for uri in children {
    out.push(as_ldap_string(uri))
  }
  SearchResultReference::new(out)
}

///|
fn encode_partial_attribute_content(pa : PartialAttribute) -> Bytes {
  let mut values = Bytes::new(0)
  for v in pa.values {
    values = values + encode_octet_string(v)
  }
  encode_ldap_string(pa.attr_type) + encode_set([values])
}

///|
fn encode_modify_request_content(req : ModifyRequest) -> Bytes {
  let mut changes = Bytes::new(0)
  for change in req.changes {
    let mut c = encode_enumerated(change.operation.to_int())
    c = c +
      encode_sequence([encode_partial_attribute_content(change.modification)])
    changes = changes + encode_sequence([c])
  }
  encode_ldap_string(req.object) + encode_sequence([changes])
}

///|
fn decode_modify_request(
  children : Array[BerValue],
) -> ModifyRequest raise LdapError {
  if children.length() != 2 {
    raise decode_error("modifyRequest must have two elements")
  }
  let object = as_ldap_string(children[0])
  let changes : Array[ModifyRequestChange] = []
  for change_value in children[1].children_or_empty() {
    let parts = change_value.children_or_empty()
    if parts.length() != 2 {
      raise decode_error("change must have two elements")
    }
    let operation = match ModifyOperation::from_int(as_enumerated(parts[0])) {
      Some(op) => op
      None => raise decode_error("invalid modify operation")
    }
    let pa_parts = parts[1].children_or_empty()
    if pa_parts.length() != 2 {
      raise decode_error("partialAttribute must have two elements")
    }
    let attr_type = as_ldap_string(pa_parts[0])
    let values : Array[Bytes] = []
    for v in pa_parts[1].children_or_empty() {
      values.push(as_octets(v))
    }
    changes.push(
      ModifyRequestChange::new(
        operation,
        PartialAttribute::new(attr_type, values),
      ),
    )
  }
  ModifyRequest::new(object, changes)
}

///|
fn encode_add_request_content(req : AddRequest) -> Bytes {
  let mut attrs = Bytes::new(0)
  for attr in req.attributes {
    attrs = attrs +
      encode_sequence([
        encode_ldap_string(attr.attr_type) + encode_set_partial(attr.values),
      ])
  }
  encode_ldap_string(req.entry) + encode_sequence([attrs])
}

///|
fn encode_set_partial(values : Array[Bytes]) -> Bytes {
  let mut out = Bytes::new(0)
  for v in values {
    out = out + encode_octet_string(v)
  }
  encode_set([out])
}

///|
fn decode_add_request(children : Array[BerValue]) -> AddRequest raise LdapError {
  if children.length() != 2 {
    raise decode_error("addRequest must have two elements")
  }
  let entry = as_ldap_string(children[0])
  let attributes : Array[Attribute] = []
  for attr_value in children[1].children_or_empty() {
    let parts = attr_value.children_or_empty()
    if parts.length() != 2 {
      raise decode_error("attribute must have two elements")
    }
    let attr_type = as_ldap_string(parts[0])
    let values : Array[Bytes] = []
    for v in parts[1].children_or_empty() {
      values.push(as_octets(v))
    }
    attributes.push(Attribute::new(attr_type, values))
  }
  AddRequest::new(entry, attributes)
}

///|
fn encode_modify_dn_request_content(req : ModifyDnRequest) -> Bytes {
  let mut content = encode_ldap_string(req.entry)
  content = content + encode_ldap_string(req.newrdn)
  content = content + encode_boolean(req.delete_old_rdn)
  match req.new_superior {
    Some(superior) =>
      content = content +
        encode_tlv(context_tag(0, false), @utf8.encode(superior))
    None => ()
  }
  content
}

///|
fn decode_modify_dn_request(
  children : Array[BerValue],
) -> ModifyDnRequest raise LdapError {
  if children.length() < 3 {
    raise decode_error("modifyDNRequest has fewer than 3 elements")
  }
  let entry = as_ldap_string(children[0])
  let newrdn = as_ldap_string(children[1])
  let delete_old_rdn = as_boolean(children[2])
  let mut new_superior : String? = None
  if children.length() > 3 {
    let extra = children[3]
    if extra.tag.class == ContextSpecific && extra.tag.number == 0 {
      new_superior = Some(@utf8.decode_lossy(extra.content[:]))
    }
  }
  ModifyDnRequest::new(entry, newrdn, delete_old_rdn, new_superior)
}

///|
fn encode_compare_request_content(req : CompareRequest) -> Bytes {
  let ava = encode_sequence([
    encode_ldap_string(req.ava.attribute_desc) +
    encode_octet_string(req.ava.assertion_value),
  ])
  encode_ldap_string(req.entry) + ava
}

///|
fn decode_compare_request(
  children : Array[BerValue],
) -> CompareRequest raise LdapError {
  if children.length() != 2 {
    raise decode_error("compareRequest must have two elements")
  }
  let entry = as_ldap_string(children[0])
  let ava_parts = children[1].children_or_empty()
  if ava_parts.length() != 2 {
    raise decode_error("attributeValueAssertion must have two elements")
  }
  CompareRequest::new(
    entry,
    AttributeValueAssertion::new(
      as_ldap_string(ava_parts[0]),
      as_octets(ava_parts[1]),
    ),
  )
}

///|
fn encode_extended_request_content(req : ExtendedRequest) -> Bytes {
  let mut content = encode_tlv(
    context_tag(0, false),
    @utf8.encode(req.request_name),
  )
  match req.request_value {
    Some(value) => content = content + encode_tlv(context_tag(1, false), value)
    None => ()
  }
  content
}

///|
fn decode_extended_request(
  children : Array[BerValue],
) -> ExtendedRequest raise LdapError {
  if children.is_empty() {
    raise decode_error("extendedRequest has no requestName")
  }
  let name_value = children[0]
  if !(name_value.tag.class == ContextSpecific &&
    name_value.tag.number == 0 &&
    !name_value.tag.constructed) {
    raise decode_error("extendedRequest requestName must be [0]")
  }
  let mut request_value : Bytes? = None
  if children.length() > 1 {
    let v = children[1]
    if v.tag.class == ContextSpecific && v.tag.number == 1 && !v.tag.constructed {
      request_value = Some(v.content)
    }
  }
  ExtendedRequest::new(@utf8.decode_lossy(name_value.content[:]), request_value)
}

///|
fn encode_extended_response_content(resp : ExtendedResponse) -> Bytes {
  let mut content = encode_result_content(resp.result)
  match resp.response_name {
    Some(name) =>
      content = content + encode_tlv(context_tag(10, false), @utf8.encode(name))
    None => ()
  }
  match resp.response_value {
    Some(value) => content = content + encode_tlv(context_tag(11, false), value)
    None => ()
  }
  content
}

///|
fn decode_extended_response(
  children : Array[BerValue],
) -> ExtendedResponse raise LdapError {
  let result = decode_result_content(children)
  let resp = ExtendedResponse::new(result)
  for child in children {
    if child.tag.class == ContextSpecific && !child.tag.constructed {
      if child.tag.number == 10 {
        resp.response_name = Some(@utf8.decode_lossy(child.content[:]))
      } else if child.tag.number == 11 {
        resp.response_value = Some(child.content)
      }
    }
  }
  resp
}