///|
priv struct DataEntry {
  source : Int32
  length : Int64
  padding : Bool
}

///|
pub(all) struct PieceSpan {
  file : Int32
  offset : Int64
  length : Int32
}

///|
fn check_piece_length(piece : Int32) -> Unit raise CrateError {
  if piece < 16384 || piece > 16777216 || (piece & (piece - 1)) != 0 {
    reject("piece_length", "expected a power of two in 16 KiB..16 MiB")
  }
}

///|
fn read_exact(
  read : (Int32, Int64, Int32) -> Bytes raise CrateError,
  file : Int32,
  offset : Int64,
  size : Int32,
) -> Bytes raise CrateError {
  let data = read(file, offset, size)
  if data.length() != size {
    reject("read", "short or oversized read at file \{file} offset \{offset}")
  }
  data
}

///|
priv struct Sha1Pieces {
  buffer : FixedArray[Byte]
  mut used : Int32
  hashes : Array[Byte]
}

///|
fn new_sha1_pieces(piece_length : Int32) -> Sha1Pieces raise CrateError {
  check_piece_length(piece_length)
  { buffer: FixedArray::make(piece_length, 0), used: 0, hashes: [], }
}

///|
fn flush_sha1(state : Sha1Pieces, padded : Bool) -> Unit raise CrateError {
  if state.used == 0 {
    return
  }
  let hash = if padded {
    for i in state.used..= 400000 {
    reject("limit", "too many pieces")
  }
  append_bytes(state.hashes, hash)
  state.used = 0
}

///|
fn push_sha1(state : Sha1Pieces, data : Bytes) -> Unit raise CrateError {
  let mut offset = 0
  while offset < data.length() {
    let capacity = state.buffer.length() - state.used
    let n = if data.length() - offset < capacity {
      data.length() - offset
    } else {
      capacity
    }
    state.buffer.blit_from_bytes(state.used, data, offset, n)
    state.used += n
    offset += n
    if state.used == state.buffer.length() {
      flush_sha1(state, true)
    }
  }
}

///|
fn v1_hash_entries(
  entries : Array[DataEntry],
  piece_length : Int32,
  read : (Int32, Int64, Int32) -> Bytes raise CrateError,
) -> Bytes raise CrateError {
  let state = new_sha1_pieces(piece_length)
  for entry in entries {
    let mut offset = 0L
    while offset < entry.length {
      let remaining = entry.length - offset
      let size = if remaining < (piece_length - state.used).to_int64() {
        remaining.to_int()
      } else {
        piece_length - state.used
      }
      // Read in at most 1 MiB chunks even when a piece is larger.
      let size = if size > 1048576 { 1048576 } else { size }
      let data = if entry.padding {
        Bytes::make(size, 0)
      } else {
        read_exact(read, entry.source, offset, size)
      }
      push_sha1(state, data)
      offset += size.to_int64()
    }
  }
  flush_sha1(state, false)
  Bytes::from_array(state.hashes)
}

///|
/// Hash the exact caller-supplied file order, crossing file boundaries (BEP 3).
/// The callback must return exactly the requested bytes; only one piece is buffered.
/// Reject layouts above 400,000 pieces before calling the reader.
pub fn sha1_pieces(
  files : Array[SourceFile],
  piece_length : Int32,
  read : (Int32, Int64, Int32) -> Bytes raise CrateError,
) -> Bytes raise CrateError {
  let total = validate_sources(files)
  check_piece_length(piece_length)
  if (total + piece_length.to_int64() - 1L) / piece_length.to_int64() > 400000L {
    reject("limit", "too many pieces; select a larger piece length")
  }
  let entries = files.mapi(fn(i, f) {
    DataEntry::{ source: i, length: f.length, padding: false, }
  })
  v1_hash_entries(entries, piece_length, read)
}

///|
/// Map a v1 piece to its contributing real file ranges, including boundary pieces.
pub fn piece_spans(
  files : Array[SourceFile],
  piece_length : Int32,
  piece : Int32,
) -> Array[PieceSpan] raise CrateError {
  let total = validate_sources(files)
  check_piece_length(piece_length)
  let start = piece.to_int64() * piece_length.to_int64()
  if piece < 0 || start >= total {
    reject("piece", "piece index is outside payload")
  }
  let end = start + piece_length.to_int64()
  let out : Array[PieceSpan] = []
  let mut cursor = 0L
  for i, file in files {
    let next = cursor + file.length
    if file.length > 0 && next > start && cursor < end {
      let lo = if start > cursor { start } else { cursor }
      let hi = if end < next { end } else { next }
      out.push({ file: i, offset: lo - cursor, length: (hi - lo).to_int(), })
    }
    cursor = next
  }
  out
}