///|
// Decode only bounded values: every count, string index and member offset must
// fit within this input, including values stored in 64-bit table fields.
fn symbol_number(
  data : Bytes,
  pos : Int,
  width : Int,
  little : Bool,
  end : Int,
  limit : Int,
) -> Int raise InvalidArchive {
  if pos > end || end - pos < width {
    reject("AR009", pos, "truncated symbol index integer")
  }
  let mut value = 0
  for n = 0; n < width; n = n + 1 {
    let i = if little { pos + width - 1 - n } else { pos + n }
    let digit = data[i].to_int()
    if digit > limit || value > (limit - digit) / 256 {
      reject(
        "AR009", pos, "symbol index integer is outside its permitted range",
      )
    }
    value = value * 256 + digit
  }
  value
}

///|
fn symbol_target(
  archive : Archive,
  headers : Map[Int, Int],
  offset : Int,
  field_offset : Int,
) -> Int raise InvalidArchive {
  match headers.get(offset) {
    Some(index) if archive.records[index].kind == Regular => index
    _ =>
      reject(
        "AR009", field_offset, "symbol target is not a regular member header",
      )
  }
}

///|
fn symbol_padding(
  data : Bytes,
  start : Int,
  end : Int,
  maximum : Int,
) -> Unit raise InvalidArchive {
  if end - start > maximum {
    reject("AR009", start, "unexpected bytes after symbol index")
  }
  for i = start; i < end; i = i + 1 {
    if data[i] != b'\x00' {
      reject("AR009", i, "symbol index padding must be zero")
    }
  }
}

///|
fn gnu_symbol_index(
  archive : Archive,
  index : Int,
  headers : Map[Int, Int],
  max_symbols : Int,
  max_names : Int,
) -> SymbolIndex raise InvalidArchive {
  let entry = archive.records[index]
  let data = archive.data
  let start = entry.data_offset
  let end = start + entry.size
  let width = if entry.name == b"/SYM64/" { 8 } else { 4 }
  let count = symbol_number(data, start, width, false, end, entry.size / width)
  if count > (entry.size - width) / width {
    reject("AR009", start, "symbol count exceeds index size")
  }
  if count > max_symbols {
    reject("AR008", start, "symbol count limit exceeded")
  }
  let symbols : Array[Symbol] = []
  let mut cursor = start + width + count * width
  let mut name_bytes = 0
  for n = 0; n < count; n = n + 1 {
    let field_offset = start + width + n * width
    let target = symbol_number(
      data,
      field_offset,
      width,
      false,
      end,
      data.length(),
    )
    let member_index = symbol_target(archive, headers, target, field_offset)
    let name_offset = cursor
    while cursor < end && data[cursor] != b'\x00' {
      cursor += 1
    }
    if cursor == end || cursor == name_offset {
      reject("AR009", name_offset, "missing, empty or unterminated symbol name")
    }
    let length = cursor - name_offset
    if length > max_names - name_bytes {
      reject("AR008", name_offset, "expanded symbol-name byte limit exceeded")
    }
    name_bytes += length
    symbols.push({
      name: data[name_offset:cursor].to_owned(),
      member_index,
      name_offset,
      offset_field_offset: field_offset,
    })
    cursor += 1
  }
  symbol_padding(data, cursor, end, 1)
  {
    record_index: index,
    format: if width == 4 {
      "gnu32"
    } else {
      "gnu64"
    },
    symbols,
  }
}

///|
fn bsd_symbol_candidate(
  archive : Archive,
  index : Int,
  headers : Map[Int, Int],
  width : Int,
  little : Bool,
  max_symbols : Int,
  max_names : Int,
) -> SymbolIndex raise InvalidArchive {
  let entry = archive.records[index]
  let data = archive.data
  let start = entry.data_offset
  let end = start + entry.size
  let table_bytes = symbol_number(data, start, width, little, end, entry.size)
  if entry.size < width * 2 ||
    table_bytes > entry.size - width * 2 ||
    table_bytes % (width * 2) != 0 {
    reject("AR009", start, "invalid BSD ranlib table byte count")
  }
  let count = table_bytes / (width * 2)
  if count > max_symbols {
    reject("AR008", start, "symbol count limit exceeded")
  }
  let string_size_pos = start + width + table_bytes
  let string_start = string_size_pos + width
  let string_size = symbol_number(
    data,
    string_size_pos,
    width,
    little,
    end,
    end - string_start,
  )
  let string_end = string_start + string_size
  symbol_padding(data, string_end, end, 7)
  let references : Array[(Int, Int, Int)] = []
  let wanted : Map[Int, Unit] = Map([])
  for n = 0; n < count; n = n + 1 {
    let pos = start + width + n * width * 2
    let name_index = symbol_number(data, pos, width, little, end, string_size)
    let target = symbol_number(
      data,
      pos + width,
      width,
      little,
      end,
      data.length(),
    )
    let member_index = symbol_target(archive, headers, target, pos + width)
    references.push((name_index, member_index, pos + width))
    wanted[name_index] = ()
  }
  // Scan the string table once; repeated references share immutable name bytes.
  let names : Map[Int, Bytes] = Map([])
  let mut cursor = string_start
  let mut unique_name_bytes = 0
  while cursor < string_end {
    let first = cursor
    while cursor < string_end && data[cursor] != b'\x00' {
      cursor += 1
    }
    if cursor == string_end {
      reject("AR009", first, "unterminated BSD symbol string")
    }
    if wanted.get(first - string_start) is Some(_) {
      if first == cursor {
        reject("AR009", first, "empty symbol name")
      }
      if cursor - first > max_names - unique_name_bytes {
        reject("AR008", first, "expanded symbol-name byte limit exceeded")
      }
      unique_name_bytes += cursor - first
      names[first - string_start] = data[first:cursor].to_owned()
    }
    cursor += 1
  }
  let symbols : Array[Symbol] = []
  let mut name_bytes = 0
  for (name_index, member_index, field_offset) in references {
    guard names.get(name_index) is Some(name) else {
      reject(
        "AR009",
        field_offset - width,
        "symbol name is not a string-table entry boundary",
      )
    }
    if name.length() > max_names - name_bytes {
      reject(
        "AR008",
        string_start + name_index,
        "expanded symbol-name byte limit exceeded",
      )
    }
    name_bytes += name.length()
    symbols.push({
      name,
      member_index,
      name_offset: string_start + name_index,
      offset_field_offset: field_offset,
    })
  }
  if entry.name == b"__.SYMDEF SORTED" || entry.name == b"__.SYMDEF_64 SORTED" {
    for n = 1; n < symbols.length(); n = n + 1 {
      if symbols[n - 1].name > symbols[n].name {
        reject(
          "AR009",
          symbols[n].name_offset,
          "sorted BSD symbol index is out of order",
        )
      }
    }
  }
  let bits = if width == 4 { "32" } else { "64" }
  let endian = if little { "le" } else { "be" }
  { record_index: index, format: "bsd\{bits}-\{endian}", symbols, }
}

///|
fn bsd_symbol_index(
  archive : Archive,
  index : Int,
  headers : Map[Int, Int],
  max_symbols : Int,
  max_names : Int,
) -> SymbolIndex raise InvalidArchive {
  let name = archive.records[index].name
  let width = if name == b"__.SYMDEF_64" || name == b"__.SYMDEF_64 SORTED" {
    8
  } else {
    4
  }
  let little = Ok(
    bsd_symbol_candidate(
      archive, index, headers, width, true, max_symbols, max_names,
    ),
  ) catch {
    InvalidArchive(error) => Err(error)
  }
  let big = Ok(
    bsd_symbol_candidate(
      archive, index, headers, width, false, max_symbols, max_names,
    ),
  ) catch {
    InvalidArchive(error) => Err(error)
  }
  match (little, big) {
    (Ok(a), Err(error)) => {
      if error.code == "AR008" {
        raise InvalidArchive(error)
      }
      a
    }
    (Err(error), Ok(b)) => {
      if error.code == "AR008" {
        raise InvalidArchive(error)
      }
      b
    }
    (Ok(a), Ok(b)) => {
      if a.symbols != b.symbols {
        reject(
          "AR010",
          archive.records[index].data_offset,
          "BSD symbol index byte order is ambiguous",
        )
      }
      {
        ..a,
        format: if width == 4 {
          "bsd32-neutral"
        } else {
          "bsd64-neutral"
        },
      }
    }
    (Err(a), Err(b)) => {
      let error = if a.code == "AR008" {
        a
      } else if b.code == "AR008" || b.offset > a.offset {
        b
      } else {
        a
      }
      raise InvalidArchive(error)
    }
  }
}

///|
fn parse_symbol_indexes(
  archive : Archive,
  max_symbols : Int,
  max_names : Int,
) -> Array[SymbolIndex] raise InvalidArchive {
  let headers : Map[Int, Int] = Map([])
  let tables : Array[Int] = []
  for i, entry in archive.records {
    headers[entry.header_offset] = i
    if entry.kind == SymbolTable {
      tables.push(i)
    }
  }
  if tables.length() > 1 {
    reject(
      "AR010",
      archive.records[tables[1]].header_offset,
      "multiple symbol indexes, including COFF linker members, are unsupported",
    )
  }
  tables.map(index => {
    let name = archive.records[index].name
    if name == b"/" || name == b"/SYM64/" {
      gnu_symbol_index(archive, index, headers, max_symbols, max_names)
    } else {
      bsd_symbol_index(archive, index, headers, max_symbols, max_names)
    }
  })
}