///|
pub(all) struct TorrentPlan {
  name : String
  mode : TorrentMode
  piece_length : Int32
  single_file : Bool
  file_count : Int32
  empty_files : Int32
  real_bytes : Int64
  storage_bytes : Int64
  padding_bytes : Int64
  v1_pieces : Int32
  v2_pieces : Int32
  v1_hash_bytes : Int64
  v2_piece_layer_bytes_upper_bound : Int64
}

///|
/// Plan the creator's layout without reading or hashing payload bytes. V2
/// piece-layer size is an upper bound because identical roots are deduplicated
/// only after hashing. This is not a prediction of complete metainfo size.
pub fn plan_torrent(
  files : Array[SourceFile],
  options : CreateOptions,
) -> TorrentPlan raise CrateError {
  validate_options(files, options)
  let piece = options.piece_length.to_int64()
  let mut real = 0L
  let mut padding = 0L
  let mut empty = 0
  let mut v2_count = 0L
  let mut layer_bound = 0L
  let padded = options.mode == Hybrid && !options.single_file
  let mut last = 0
  for index, file in files {
    let length = file.length
    real += length
    if length == 0L {
      empty += 1
    }
    if options.mode != V1 {
      let count = (length + piece - 1L) / piece
      v2_count += count
      if count > 1L {
        layer_bound += count * 32L
      }
    }
    if padded {
      if path_compare(file.path, files[last].path) > 0 {
        last = index
      }
      if length % piece != 0L {
        padding += piece - length % piece
      }
    }
  }
  // Only the last file in canonical path order avoids alignment padding.
  // Find it in one pass; planning does not need to sort the complete input.
  if padded && files[last].length % piece != 0L {
    padding -= piece - files[last].length % piece
  }
  let storage = real + padding
  if storage > 9007199254740991L {
    reject("limit", "planned storage exceeds exact host offsets")
  }
  let v1_count = if options.mode == V2 {
    0L
  } else {
    (storage + piece - 1L) / piece
  }
  if v1_count > 400000L || v2_count > 400000L {
    reject(
      "limit", "planned layout exceeds the piece limit; select larger pieces",
    )
  }
  {
    name: options.name,
    mode: options.mode,
    piece_length: options.piece_length,
    single_file: options.single_file,
    file_count: files.length(),
    empty_files: empty,
    real_bytes: real,
    storage_bytes: storage,
    padding_bytes: padding,
    v1_pieces: v1_count.to_int(),
    v2_pieces: v2_count.to_int(),
    v1_hash_bytes: v1_count * 20L,
    v2_piece_layer_bytes_upper_bound: layer_bound,
  }
}

///|
pub fn plan_json(plan : TorrentPlan) -> Json {
  j_object([
    ("schema", Json::string("swarmcrate.plan.v1")),
    ("name", Json::string(plan.name)),
    ("mode", Json::string(mode_name(plan.mode))),
    ("piece_length", Json::number(plan.piece_length.to_double())),
    ("single_file", Json::boolean(plan.single_file)),
    ("file_count", Json::number(plan.file_count.to_double())),
    ("empty_files", Json::number(plan.empty_files.to_double())),
    ("real_bytes", Json::string(plan.real_bytes.to_string())),
    ("storage_bytes", Json::string(plan.storage_bytes.to_string())),
    ("padding_bytes", Json::string(plan.padding_bytes.to_string())),
    ("v1_pieces", Json::number(plan.v1_pieces.to_double())),
    ("v2_pieces", Json::number(plan.v2_pieces.to_double())),
    ("v1_hash_bytes", Json::string(plan.v1_hash_bytes.to_string())),
    (
      "v2_piece_layer_bytes_upper_bound",
      Json::string(plan.v2_piece_layer_bytes_upper_bound.to_string()),
    ),
  ])
}