///|
/// One ISO 2709 directory entry.
pub(all) struct DirectoryEntry {
  tag : String
  length : Int
  start : Int
} derive(Eq, Debug)

///|
pub(all) suberror DirectoryError {
  Truncated(Int)
  InvalidDirectoryLength(Int)
  InvalidEntry(String)
  InvalidTag(String)
  InvalidFieldLength(String)
  InvalidFieldStart(String)
  MissingTerminator
} derive(Eq, Debug)

///|
pub impl Show for DirectoryError with fn output(self, logger) {
  logger.write_string(self.to_string())
}

///|
pub fn DirectoryError::to_string(self : DirectoryError) -> String {
  match self {
    Truncated(need) => "truncated directory: need \{need} more bytes"
    InvalidDirectoryLength(length) =>
      "directory length is not divisible by entry width: \{length}"
    InvalidEntry(value) => "invalid directory entry: \{value}"
    InvalidTag(value) => "invalid directory tag: \{value}"
    InvalidFieldLength(value) => "invalid directory field length: \{value}"
    InvalidFieldStart(value) => "invalid directory field start: \{value}"
    MissingTerminator => "directory is missing the field terminator"
  }
}

///|
fn digits(value : String) -> Int? {
  let mut result = 0
  if value.is_empty() {
    return None
  }
  for char in value {
    let digit = char.to_int()
    if digit < 48 || digit > 57 {
      return None
    }
    result = result * 10 + digit - 48
  }
  Some(result)
}

///|
fn ascii_field(raw : Bytes, offset : Int, length : Int) -> String? {
  let chars : Array[Char] = []
  let mut index = 0
  while index < length {
    let value = raw[offset + index].to_int()
    if value > 0x7F {
      return None
    }
    chars.push(value.unsafe_to_char())
    index = index + 1
  }
  Some(String::from_array(chars))
}

///|
/// Parse the directory occupying bytes 24 through `leader.base_address() - 1`.
pub fn parse(
  raw : Bytes,
  leader : @leader.Leader,
) -> Result[Array[DirectoryEntry], DirectoryError] {
  let base_address = match leader.base_address() {
    Err(_) => return Err(InvalidEntry("invalid leader base address"))
    Ok(value) => value
  }
  let map = match leader.entry_map() {
    Err(_) => return Err(InvalidEntry("invalid leader entry map"))
    Ok(value) => value
  }
  let tag_width = 3
  let length_width = map.0
  let start_width = map.1 + map.2
  let entry_width = tag_width + length_width + start_width
  if entry_width <= 0 || base_address < 25 || base_address > raw.length() {
    return Err(InvalidDirectoryLength(base_address))
  }
  let directory_bytes = base_address - 24 - 1
  if directory_bytes < 0 || directory_bytes % entry_width != 0 {
    return Err(InvalidDirectoryLength(directory_bytes))
  }
  if raw[base_address - 1].to_int() != 0x1E {
    return Err(MissingTerminator)
  }
  let entries : Array[DirectoryEntry] = []
  let mut offset = 24
  while offset < base_address - 1 {
    let tag = match ascii_field(raw, offset, tag_width) {
      None => return Err(InvalidTag("non-ASCII tag"))
      Some(value) => value
    }
    let length_text = match ascii_field(raw, offset + tag_width, length_width) {
      None => return Err(InvalidFieldLength("non-ASCII length"))
      Some(value) => value
    }
    let start_text = match
      ascii_field(raw, offset + tag_width + length_width, start_width) {
      None => return Err(InvalidFieldStart("non-ASCII start"))
      Some(value) => value
    }
    let length = match digits(length_text) {
      None => return Err(InvalidFieldLength(length_text))
      Some(value) => value
    }
    let start = match digits(start_text) {
      None => return Err(InvalidFieldStart(start_text))
      Some(value) => value
    }
    if tag.length() != 3 {
      return Err(InvalidTag(tag))
    }
    entries.push({ tag, length, start })
    offset = offset + entry_width
  }
  Ok(entries)
}