///|
/// Errors reported while writing deterministic newc archives.
///
/// `FieldOverflow` carries the newc field index: ino(0), mode(1), uid(2),
/// gid(3), mtime(4), file_size(5), or name_size(6).
pub(all) enum WriteError {
  EmptyName
  EmbeddedNul(Int32)
  FieldOverflow(Int32)
} derive(Eq, Debug)

///|
/// Minimal deterministic newc entry input.
pub(all) struct NewcWriteEntry {
  name : String
  data : Bytes
  mode : UInt64
  uid : UInt64
  gid : UInt64
  mtime : UInt64
} derive(Eq, Debug)

///|
pub fn NewcWriteEntry::new(
  name : String,
  data : Bytes,
  mode~ : UInt64 = 0x81A4UL,
  uid~ : UInt64 = 0UL,
  gid~ : UInt64 = 0UL,
  mtime~ : UInt64 = 0UL,
) -> NewcWriteEntry {
  { name, data, mode, uid, gid, mtime, }
}

///|
fn write_hex8(buffer : Buffer, value : UInt64) -> Unit {
  buffer.write_string_utf8(value.to_string(radix=16).pad_start(8, '0'))
}

///|
fn write_zero_bytes(buffer : Buffer, count : Int32) -> Unit {
  for i = 0; i < count; i = i + 1 {
    buffer.write_byte(b'\x00')
  }
}

///|
fn check_u32(value : UInt64, field_index : Int32) -> Result[Unit, WriteError] {
  if value > 0xFFFFFFFFUL {
    Err(FieldOverflow(field_index))
  } else {
    Ok(())
  }
}

///|
fn write_newc_header(
  buffer : Buffer,
  format : NewcFormat,
  ino : UInt64,
  mode : UInt64,
  uid : UInt64,
  gid : UInt64,
  nlink : UInt64,
  mtime : UInt64,
  file_size : UInt64,
  name_size : UInt64,
  check : UInt64,
) -> Unit {
  buffer.write_string_utf8(
    if format == NewcFormat::Newc {
      "070701"
    } else {
      "070702"
    },
  )
  write_hex8(buffer, ino)
  write_hex8(buffer, mode)
  write_hex8(buffer, uid)
  write_hex8(buffer, gid)
  write_hex8(buffer, nlink)
  write_hex8(buffer, mtime)
  write_hex8(buffer, file_size)
  write_hex8(buffer, 0UL)
  write_hex8(buffer, 0UL)
  write_hex8(buffer, 0UL)
  write_hex8(buffer, 0UL)
  write_hex8(buffer, name_size)
  write_hex8(buffer, check)
}

///|
/// Writes a deterministic `070701` or `070702` newc archive.
///
/// Inodes are assigned sequentially from 1, device fields are zero, and the
/// caller supplies mode, uid, gid, and mtime. No wall clock or random value is
/// read, so identical inputs always produce identical bytes.
pub fn write_newc(
  entries : ArrayView[NewcWriteEntry],
  format~ : NewcFormat = NewcFormat::Newc,
) -> Result[Bytes, WriteError] {
  let buffer = @buffer.Buffer()
  for index = 0; index < entries.length(); index = index + 1 {
    let entry = entries[index]
    let name_bytes = @utf8.encode(entry.name)
    if name_bytes.is_empty() {
      return Err(EmptyName)
    }
    for i = 0; i < name_bytes.length(); i = i + 1 {
      if name_bytes[i] == b'\x00' {
        return Err(EmbeddedNul(i))
      }
    }
    let name_size = name_bytes.length() + 1
    let file_size = entry.data.length()
    let ino = (index + 1).to_uint64()
    let name_size_u = name_size.to_uint64()
    let file_size_u = file_size.to_uint64()
    let checks = [
      check_u32(ino, 0),
      check_u32(entry.mode, 1),
      check_u32(entry.uid, 2),
      check_u32(entry.gid, 3),
      check_u32(entry.mtime, 4),
      check_u32(file_size_u, 5),
      check_u32(name_size_u, 6),
    ]
    for result in checks {
      match result {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
    }
    let check = if format == NewcFormat::NewcCrc {
      newc_checksum(entry.data[:])
    } else {
      0UL
    }
    write_newc_header(
      buffer,
      format,
      ino,
      entry.mode,
      entry.uid,
      entry.gid,
      1UL,
      entry.mtime,
      file_size_u,
      name_size_u,
      check,
    )
    buffer.write_bytes(name_bytes)
    buffer.write_byte(b'\x00')
    write_zero_bytes(buffer, newc_name_padding(name_size))
    buffer.write_bytes(entry.data)
    write_zero_bytes(buffer, padding_for(file_size))
  }
  write_newc_header(
    buffer, format, 0UL, 0UL, 0UL, 0UL, 1UL, 0UL, 0UL, 11UL, 0UL,
  )
  buffer.write_bytes(b"TRAILER!!!\x00")
  write_zero_bytes(buffer, newc_name_padding(11))
  Ok(buffer.to_bytes())
}