///|
fn is_control_tag(tag : String) -> Bool {
  tag >= "001" && tag <= "009"
}

///|
fn ascii_char(raw : Bytes, offset : Int) -> Char? {
  let value = raw[offset].to_int()
  if value > 0x7F {
    None
  } else {
    Some(value.unsafe_to_char())
  }
}

///|
fn decode_field(
  raw : Bytes,
  offset : Int,
  entry : @directory.DirectoryEntry,
) -> Result[@record.MarcField, @record.RecordError] {
  if entry.length < 1 || offset < 0 || offset + entry.length > raw.length() {
    return Err(@record.FieldOutsideRecord(entry.tag))
  }
  let end = offset + entry.length
  if raw[end - 1].to_int() != 0x1E {
    return Err(@record.FieldTerminatorMissing(entry.tag))
  }
  let content_length = entry.length - 1
  if is_control_tag(entry.tag) {
    if content_length > 0 && raw[offset].to_int() == 0x1F {
      return Err(@record.InvalidControlField(entry.tag))
    }
    let value = match @binary.decode_utf8(raw, offset~, length=content_length) {
      Err(_) => return Err(@record.InvalidUtf8(entry.tag))
      Ok(value) => value
    }
    return Ok(@record.MarcField::ControlField(tag=entry.tag, value~))
  }
  if content_length < 2 {
    return Err(@record.InvalidDataField(entry.tag))
  }
  let indicator1 = match ascii_char(raw, offset) {
    None => return Err(@record.InvalidDataField(entry.tag))
    Some(value) => value
  }
  let indicator2 = match ascii_char(raw, offset + 1) {
    None => return Err(@record.InvalidDataField(entry.tag))
    Some(value) => value
  }
  let subfields : Array[@record.Subfield] = []
  let mut cursor = offset + 2
  let content_end = end - 1
  while cursor < content_end {
    if raw[cursor].to_int() != 0x1F || cursor + 1 >= content_end {
      return Err(@record.InvalidSubfield(entry.tag))
    }
    let code = match ascii_char(raw, cursor + 1) {
      None => return Err(@record.InvalidSubfield(entry.tag))
      Some(value) => value
    }
    let value_start = cursor + 2
    let mut value_end = value_start
    while value_end < content_end && raw[value_end].to_int() != 0x1F {
      value_end = value_end + 1
    }
    let value = match
      @binary.decode_utf8(
        raw,
        offset=value_start,
        length=value_end - value_start,
      ) {
      Err(_) => return Err(@record.InvalidUtf8(entry.tag))
      Ok(value) => value
    }
    subfields.push({ code, value })
    cursor = value_end
  }
  Ok(
    @record.MarcField::DataField(
      tag=entry.tag,
      indicator1~,
      indicator2~,
      subfields~,
    ),
  )
}

///|
/// Parse one complete ISO 2709 / MARC 21 record.
pub fn parse_record(
  raw : Bytes,
) -> Result[@record.MarcRecord, @record.RecordError] {
  if raw.length() < 25 {
    return Err(@record.InvalidLength(25, raw.length()))
  }
  if raw[raw.length() - 1].to_int() != 0x1D {
    return Err(@record.FieldTerminatorMissing("record"))
  }
  let leader = match @leader.parse(@binary.slice(raw, 0, 24)) {
    Err(error) => return Err(@record.Leader(error))
    Ok(value) => value
  }
  let expected_length = match leader.record_length() {
    Err(error) => return Err(@record.Leader(error))
    Ok(value) => value
  }
  if expected_length != raw.length() {
    return Err(@record.InvalidLength(expected_length, raw.length()))
  }
  let base_address = match leader.base_address() {
    Err(error) => return Err(@record.Leader(error))
    Ok(value) => value
  }
  if base_address <= 24 || base_address >= raw.length() {
    return Err(@record.FieldOutsideRecord("directory"))
  }
  let entries = match @directory.parse(raw, leader) {
    Err(error) => return Err(@record.Directory(error))
    Ok(value) => value
  }
  let fields : Array[@record.MarcField] = []
  for entry in entries {
    let offset = base_address + entry.start
    fields.push(
      match decode_field(raw, offset, entry) {
        Err(error) => return Err(error)
        Ok(field) => field
      },
    )
  }
  Ok({ leader, fields })
}

///|
/// Parse a concatenated stream of complete records.
pub fn parse_records(
  raw : Bytes,
) -> Array[Result[@record.MarcRecord, @record.RecordError]] {
  let records : Array[Result[@record.MarcRecord, @record.RecordError]] = []
  let mut offset = 0
  while offset < raw.length() {
    if raw.length() - offset < 24 {
      records.push(Err(@record.InvalidLength(24, raw.length() - offset)))
      break
    }
    let leader_bytes = @binary.slice(raw, offset, offset + 24)
    let leader = match @leader.parse(leader_bytes) {
      Err(error) => {
        records.push(Err(@record.Leader(error)))
        break
      }
      Ok(value) => value
    }
    let length = match leader.record_length() {
      Err(error) => {
        records.push(Err(@record.Leader(error)))
        break
      }
      Ok(value) => value
    }
    if length <= 0 || offset + length > raw.length() {
      records.push(Err(@record.InvalidLength(length, raw.length() - offset)))
      break
    }
    records.push(parse_record(@binary.slice(raw, offset, offset + length)))
    offset = offset + length
  }
  records
}