///|
/// Builds a `Bytes` from an array of octets.
fn bytes_of(items : Array[Byte]) -> Bytes {
  Bytes::from_array(items[:])
}

///|
/// Concatenates several byte slices into one buffer.
fn concat(parts : Array[BytesView]) -> Bytes {
  let mut size = 0
  for part in parts {
    size = size + part.length()
  }
  let buffer = @buffer.Buffer(size_hint=size)
  for part in parts {
    buffer.write_bytes(part)
  }
  buffer.to_bytes()
}

///|
/// Encodes the identifier octets of `tag`, using the high-tag-number form for
/// tag numbers of 31 or more (X.690, 8.1.2).
pub fn encode_tag(tag : Tag) -> Bytes raise DerError {
  let class_bits = match tag.cls {
    Universal => 0x00
    Application => 0x40
    ContextSpecific => 0x80
    Private => 0xC0
  }
  let constructed_bit = if tag.constructed { 0x20 } else { 0x00 }
  if tag.number < 0 {
    raise InvalidValue("tag number must not be negative")
  }
  if tag.number < 0x1F {
    bytes_of([((class_bits | constructed_bit | tag.number) & 0xFF).to_byte()])
  } else {
    let buffer = @buffer.Buffer(size_hint=4)
    buffer.write_byte(((class_bits | constructed_bit | 0x1F) & 0xFF).to_byte())
    let groups : Array[Int] = []
    let mut remaining = tag.number
    while remaining > 0 {
      groups.push(remaining & 0x7F)
      remaining = remaining >> 7
    }
    for i in 0.. Bytes raise DerError {
  if length < 0 {
    raise InvalidValue("length must not be negative")
  }
  if length < 0x80 {
    return bytes_of([length.to_byte()])
  }
  let buffer = @buffer.Buffer(size_hint=5)
  let octets : Array[Int] = []
  let mut remaining = length
  while remaining > 0 {
    octets.push(remaining & 0xFF)
    remaining = remaining >> 8
  }
  buffer.write_byte((0x80 | octets.length()).to_byte())
  for i in 0.. Bytes raise DerError {
  concat([encode_tag(tag)[:], encode_length(content.length())[:], content])
}

///|
/// Encodes a primitive value with the given universal tag number.
fn encode_universal(number : Int, content : BytesView) -> Bytes raise DerError {
  encode_tlv(Tag::new(Universal, number), content)
}

///|
/// Encodes a BOOLEAN. DER requires `0xFF` for true and `0x00` for false.
pub fn encode_boolean(value : Bool) -> Bytes raise DerError {
  encode_universal(TAG_BOOLEAN, if value { b"\xff"[:] } else { b"\x00"[:] })
}

///|
/// Encodes a NULL.
pub fn encode_null() -> Bytes raise DerError {
  encode_universal(TAG_NULL, b""[:])
}

///|
/// Encodes the content octets of an INTEGER: minimal two's complement,
/// big-endian, always at least one octet (X.690, 8.3 and 11.3).
pub fn encode_integer_content(value : @bigint.BigInt) -> Bytes {
  if value.is_zero() {
    return b"\x00"
  }
  if value.compare_int(0) > 0 {
    let magnitude = value.to_octets()
    if (magnitude.unsafe_get(0).to_int() & 0x80) != 0 {
      concat([b"\x00"[:], magnitude[:]])
    } else {
      magnitude
    }
  } else {
    let magnitude = value.neg().to_octets()
    // Smallest octet count n with -2^(8n-1) <= value, that is
    // magnitude <= 2^(8n-1). Start from a tight lower bound and adjust.
    let bits = value.neg().bit_length()
    let mut octet_count = if bits <= 1 { 1 } else { (bits - 1) / 8 }
    if octet_count < 1 {
      octet_count = 1
    }
    let one = @bigint.BigInt::from_int(1)
    while magnitude_view(magnitude) > one << (8 * octet_count - 1) {
      octet_count = octet_count + 1
    }
    let modulus = @bigint.BigInt::from_int(1) << (8 * octet_count)
    // Two's complement of a negative value is 2^(8n) + value.
    (modulus + value).to_octets(length=octet_count)
  }
}

///|
/// Interprets an unsigned big-endian octet string as a non-negative integer.
fn magnitude_view(octets : Bytes) -> @bigint.BigInt {
  @bigint.BigInt::from_octets(octets[:])
}

///|
/// Encodes an INTEGER from an arbitrary-precision value.
pub fn encode_integer(value : @bigint.BigInt) -> Bytes raise DerError {
  encode_universal(TAG_INTEGER, encode_integer_content(value)[:])
}

///|
/// Encodes an INTEGER from a machine integer.
pub fn encode_int(value : Int) -> Bytes raise DerError {
  encode_integer(@bigint.BigInt::from_int(value))
}

///|
/// Encodes an INTEGER from a 64-bit integer.
pub fn encode_int64(value : Int64) -> Bytes raise DerError {
  encode_integer(@bigint.BigInt::from_int64(value))
}

///|
/// Encodes an ENUMERATED from a machine integer.
pub fn encode_enumerated(value : Int) -> Bytes raise DerError {
  encode_universal(
    TAG_ENUMERATED,
    encode_integer_content(@bigint.BigInt::from_int(value))[:],
  )
}

///|
/// Encodes the content octets of an OBJECT IDENTIFIER, including the combined
/// first two arcs (X.690, 8.19).
pub fn encode_object_identifier_content(
  oid : ObjectIdentifier,
) -> Bytes raise DerError {
  let arcs = oid.arcs()
  if arcs.length() < 2 {
    raise InvalidValue("OBJECT IDENTIFIER needs at least two arcs")
  }
  let first = arcs[0]
  let second = arcs[1]
  if first < 0L || first > 2L {
    raise InvalidValue("first OBJECT IDENTIFIER arc must be 0, 1, or 2")
  }
  if first < 2L && (second < 0L || second > 39L) {
    raise InvalidValue(
      "second arc must be at most 39 when the first arc is 0 or 1",
    )
  }
  if second < 0L {
    raise InvalidValue("OBJECT IDENTIFIER arcs must not be negative")
  }
  let buffer = @buffer.Buffer(size_hint=arcs.length() + 2)
  write_subidentifier(buffer, first * 40L + second)
  for i in 2.. Unit {
  let groups : Array[Int] = []
  let mut remaining = value
  while remaining > 0L {
    groups.push((remaining & 0x7FL).to_int())
    remaining = remaining >> 7
  }
  if groups.is_empty() {
    buffer.write_byte(0)
    return
  }
  for i in 0.. Bytes raise DerError {
  encode_universal(
    TAG_OBJECT_IDENTIFIER,
    encode_object_identifier_content(oid)[:],
  )
}

///|
/// Encodes an OCTET STRING.
pub fn encode_octet_string(value : BytesView) -> Bytes raise DerError {
  encode_universal(TAG_OCTET_STRING, value)
}

///|
/// Encodes the content octets of a BIT STRING: the unused-bit count followed by
/// the value octets (X.690, 8.6 and 11.2).
pub fn encode_bit_string_content(
  value : BytesView,
  unused_bits? : Int = 0,
) -> Bytes raise DerError {
  if unused_bits < 0 || unused_bits > 7 {
    raise InvalidValue("BIT STRING unused bit count must be 0-7")
  }
  if value.length() == 0 && unused_bits != 0 {
    raise InvalidValue("BIT STRING unused bit count must be 0 when empty")
  }
  if value.length() > 0 && unused_bits > 0 {
    let last = value.unsafe_get(value.length() - 1).to_int()
    if (last & ((1 << unused_bits) - 1)) != 0 {
      raise InvalidValue("BIT STRING has non-zero unused bits")
    }
  }
  concat([bytes_of([unused_bits.to_byte()])[:], value])
}

///|
/// Encodes a BIT STRING.
pub fn encode_bit_string(
  value : BytesView,
  unused_bits? : Int = 0,
) -> Bytes raise DerError {
  encode_universal(
    TAG_BIT_STRING,
    encode_bit_string_content(value, unused_bits~)[:],
  )
}

///|
/// Encodes a UTF8String.
pub fn encode_utf8_string(value : StringView) -> Bytes raise DerError {
  encode_universal(TAG_UTF8_STRING, @utf8.encode(value)[:])
}

///|
/// Encodes an ASCII-only string type, rejecting any character above `0x7F`.
fn encode_ascii(
  number : Int,
  value : StringView,
  allowed : (Char) -> Bool,
  kind : String,
) -> Bytes raise DerError {
  let buffer = @buffer.Buffer(size_hint=value.length())
  for ch in value {
    if !allowed(ch) {
      raise InvalidValue(kind + " contains a character outside its alphabet")
    }
    buffer.write_byte(Char::to_int(ch).to_byte())
  }
  encode_universal(number, buffer.to_bytes()[:])
}

///|
/// Encodes a PrintableString, whose alphabet is defined by X.680 41.
pub fn encode_printable_string(value : StringView) -> Bytes raise DerError {
  encode_ascii(
    TAG_PRINTABLE_STRING,
    value,
    fn(ch) {
      match ch {
        'A'..='Z' | 'a'..='z' | '0'..='9' => true
        ' '
        | '\''
        | '('
        | ')'
        | '+'
        | ','
        | '-'
        | '.'
        | '/'
        | ':'
        | '='
        | '?' => true
        _ => false
      }
    },
    "PrintableString",
  )
}

///|
/// Encodes an IA5String, whose alphabet is the 128 ASCII characters.
pub fn encode_ia5_string(value : StringView) -> Bytes raise DerError {
  encode_ascii(
    TAG_IA5_STRING,
    value,
    fn(ch) { Char::to_int(ch) <= 0x7F },
    "IA5String",
  )
}

///|
/// Encodes a NumericString, whose alphabet is the digits and the space.
pub fn encode_numeric_string(value : StringView) -> Bytes raise DerError {
  encode_ascii(
    TAG_NUMERIC_STRING,
    value,
    fn(ch) {
      match ch {
        '0'..='9' | ' ' => true
        _ => false
      }
    },
    "NumericString",
  )
}

///|
/// Encodes a VisibleString, whose alphabet is the printable ASCII range.
pub fn encode_visible_string(value : StringView) -> Bytes raise DerError {
  encode_ascii(
    TAG_VISIBLE_STRING,
    value,
    fn(ch) {
      let code = Char::to_int(ch)
      code >= 0x20 && code <= 0x7E
    },
    "VisibleString",
  )
}

///|
/// Encodes a BMPString as UTF-16BE without a byte order mark (X.680, 8.4).
pub fn encode_bmp_string(value : StringView) -> Bytes raise DerError {
  for ch in value {
    if Char::to_int(ch) > 0xFFFF {
      raise InvalidValue("BMPString contains a character outside the BMP")
    }
  }
  encode_universal(
    TAG_BMP_STRING,
    @utf16.encode(value, endianness=@utf16.Big)[:],
  )
}

///|
/// Encodes a UniversalString as UTF-32BE (X.680, 8.7).
pub fn encode_universal_string(value : StringView) -> Bytes raise DerError {
  let buffer = @buffer.Buffer(size_hint=value.length() * 4)
  for ch in value {
    let code = Char::to_int(ch)
    buffer.write_byte(((code >> 24) & 0xFF).to_byte())
    buffer.write_byte(((code >> 16) & 0xFF).to_byte())
    buffer.write_byte(((code >> 8) & 0xFF).to_byte())
    buffer.write_byte((code & 0xFF).to_byte())
  }
  encode_universal(TAG_UNIVERSAL_STRING, buffer.to_bytes()[:])
}

///|
/// Builds a constructed DER value from already-encoded elements.
///
/// This is the ergonomic way to assemble nested structures such as sequences,
/// sets and explicitly tagged fields.
pub struct DerWriter {
  items : Array[Bytes]
}

///|
/// Creates an empty writer.
pub fn DerWriter::new() -> DerWriter {
  { items: [], }
}

///|
/// Appends one complete tag-length-value element.
pub fn DerWriter::add(self : DerWriter, element : BytesView) -> Unit {
  self.items.push(@buffer.from_bytes(element).to_bytes())
}

///|
/// Number of elements added so far.
pub fn DerWriter::length(self : DerWriter) -> Int {
  self.items.length()
}

///|
/// Whether the writer is empty.
pub fn DerWriter::is_empty(self : DerWriter) -> Bool {
  self.items.is_empty()
}

///|
/// Concatenates everything added so far, without adding a wrapper.
pub fn DerWriter::to_content(self : DerWriter) -> Bytes {
  concat(self.items.map(fn(item) { item[:] }))
}

///|
/// Wraps everything added so far in a SEQUENCE.
pub fn DerWriter::to_sequence(self : DerWriter) -> Bytes raise DerError {
  encode_tlv(
    Tag::new(Universal, TAG_SEQUENCE, constructed=true),
    self.to_content()[:],
  )
}

///|
/// Wraps everything added so far in a SET OF, sorting the elements by their
/// encoding as required for DER (X.690, 11.6).
pub fn DerWriter::to_set_of(self : DerWriter) -> Bytes raise DerError {
  let sorted = self.items.copy()
  sorted.sort_by(fn(a, b) { compare_octets(a[:], b[:]) })
  encode_tlv(
    Tag::new(Universal, TAG_SET, constructed=true),
    concat(sorted.map(fn(item) { item[:] }))[:],
  )
}

///|
/// Wraps everything added so far in an explicitly tagged constructed value
/// (X.690, 8.14.3).
pub fn DerWriter::to_explicit(
  self : DerWriter,
  number : Int,
) -> Bytes raise DerError {
  encode_tlv(
    Tag::new(ContextSpecific, number, constructed=true),
    self.to_content()[:],
  )
}

///|
/// Wraps a single encoded value in an explicitly tagged constructed value.
pub fn encode_explicit(number : Int, inner : BytesView) -> Bytes raise DerError {
  let writer = DerWriter::new()
  writer.add(inner)
  writer.to_explicit(number)
}

///|
/// Encodes a primitive implicitly tagged value, for example a context-specific
/// string inside a certificate.
pub fn encode_context_primitive(
  number : Int,
  content : BytesView,
) -> Bytes raise DerError {
  encode_tlv(Tag::new(ContextSpecific, number), content)
}

///|
/// Encodes a constructed value from already-encoded elements.
pub fn encode_sequence(elements : Array[BytesView]) -> Bytes raise DerError {
  encode_tlv(
    Tag::new(Universal, TAG_SEQUENCE, constructed=true),
    concat(elements)[:],
  )
}

///|
/// Encodes a DER SET OF, sorting the elements by their encoding.
pub fn encode_set_of(elements : Array[BytesView]) -> Bytes raise DerError {
  let copies : Array[Bytes] = elements.map(fn(element) {
    @buffer.from_bytes(element).to_bytes()
  })
  copies.sort_by(fn(a, b) { compare_octets(a[:], b[:]) })
  encode_tlv(
    Tag::new(Universal, TAG_SET, constructed=true),
    concat(copies.map(fn(item) { item[:] }))[:],
  )
}

///|
/// Compares two octet strings the way DER orders the components of a SET OF:
/// byte by byte as unsigned values, with a prefix ordering before its
/// extension.
pub fn compare_octets(a : BytesView, b : BytesView) -> Int {
  let shared = if a.length() < b.length() { a.length() } else { b.length() }
  for i in 0.. Bytes raise DerError {
  match self {
    Primitive(tag, content) => encode_tlv(tag, content)
    Constructed(tag, children) => {
      let encoded = children.map(child => child.encode())
      if tag.cls == Universal && tag.number == TAG_SET {
        encoded.sort_by(fn(a, b) { compare_octets(a[:], b[:]) })
      }
      encode_tlv(tag, concat(encoded.map(fn(item) { item[:] }))[:])
    }
  }
}