// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// Build archives and read them back.
///
///     moon run src/examples/archive
///
/// Both containers are directories that are files, and both are written here the
/// same way: hand the writer members, take the bytes it produces, hand them back
/// to a reader that was told nothing about them.
fn members() -> Array[@zip.Entry] {
  [
    @zip.Entry::new(
      "readme.txt",
      b"a member of an archive\n",
      mtime=1700000000L,
    ),
    @zip.Entry::new("data/table.csv", b"name,size\nreadme,24\n"),
    @zip.Entry::new("data/already-gzipped.png", Bytes::make(600, b'\x89')),
  ]
}

///|
fn main raise {
  let entries = members()

  let plain = @zip.zip(entries[:])
  println(
    "zip: \{plain.length()} bytes, \{@zip.names(plain[:]).length()} members",
  )

  // The same archive in the wide form: sixty-four-bit sizes and offsets, for a
  // member or an archive that outgrew the thirty-two-bit fields.
  let wide = @zip.zip(entries[:], zip64=true)
  println("zip64: \{wide.length()} bytes")

  // With a data descriptor: each member's sizes follow its data rather than
  // preceding it, which is what a writer that has not finished reading must do.
  let streamed = @zip.zip(entries[:], descriptor=true)
  println("with descriptors: \{streamed.length()} bytes")

  // And encrypted, with the cipher the format called encryption for fifteen
  // years. Not a security boundary; see the `zip` documentation.
  let locked = @zip.zip(entries[:], password="moonzip")
  let opened = @zip.unzip(locked[:], password="moonzip")
  println(
    "with a password: \{locked.length()} bytes, readable: \{opened[0].data == entries[0].data}",
  )
  let refused = try {
    @zip.unzip(locked[:], password="wrong") |> ignore
    "no"
  } catch {
    _ => "yes"
  }
  println("the wrong password is refused: \{refused}")

  // Reading from the front, which is all a reader receiving an archive can do.
  let reader = @zip.Reader::new(plain[:])
  let seen : Array[String] = []
  while true {
    match reader.next() {
      None => break
      Some(entry) => seen.push(entry.name)
    }
  }
  let walked = StringBuilder()
  let mut first_one = true
  for name in seen {
    if !first_one {
      walked.write_string(", ")
    }
    first_one = false
    walked.write_string(name)
  }
  println("walked from the front: \{walked}")

  // 7z is the third: a directory of coders, where the compression method is a
  // property of the folder rather than of the archive.
  let solid = [
    @sevenz.Entry::new(
      "readme.txt",
      b"a member of an archive\n",
      mtime=1700000000L,
    ),
    @sevenz.Entry::new("data/table.csv", b"name,size\nreadme,24\n"),
    @sevenz.Entry::directory("empty"),
  ]
  for how in [@sevenz.Method::Copy, @sevenz.Method::Lzma, @sevenz.Method::Lzma2] {
    let archive = @sevenz.sevenz(solid[:], how~)
    let members_back = @sevenz.un7z(archive[:])
    println(
      "7z/\{how.label()}: \{archive.length()} bytes, \{members_back.length()} members, matches: \{members_back[0].data == solid[0].data}",
    )
  }

  // The same archive written a member at a time. The bytes each member produced
  // leave as it is added; only the directory and the header wait for the end,
  // because the header's first field is how much packed data there is.
  let writer = @sevenz.Writer::new(how=@sevenz.Method::Lzma)
  let body : Array[Byte] = []
  for entry in solid {
    for b in writer.add(entry) {
      body.push(b)
    }
  }
  let directory = writer.finish()
  let start = writer.signature()
  let assembled : Array[Byte] = []
  for b in start {
    assembled.push(b)
  }
  for b in body {
    assembled.push(b)
  }
  for b in directory {
    assembled.push(b)
  }
  let streamed = Bytes::from_array(assembled)
  let streaming = @sevenz.Reader::new()
  streaming.push(streamed[:])
  let read_back = streaming.finish()
  println(
    "7z streamed: \{streamed.length()} bytes, \{read_back.length()} members, matches: \{read_back[0].data == solid[0].data}",
  )

  // tar is the other half of the same idea, without the compressed members: one
  // header per file, then the bytes. A tarball is that archive through gzip.
  let files = [
    @tar.Entry::new(
      "readme.txt",
      b"a member of an archive\n",
      mtime=1700000000L,
    ),
    @tar.Entry::new("data/table.csv", b"name,size\nreadme,24\n"),
    @tar.Entry::directory("empty"),
    @tar.Entry::symlink("latest", "readme.txt"),
  ]
  let tarball = @tar.tar(files[:])
  println("tar: \{tarball.length()} bytes")
  let gzipped = @tar.targz(files[:])
  println("tar.gz: \{gzipped.length()} bytes")
  let back = @tar.untargz(gzipped[:])
  let listed = StringBuilder()
  let mut leading = true
  for entry in back {
    if !leading {
      listed.write_string(", ")
    }
    leading = false
    listed.write_string(entry.name)
  }
  println("read back: \{listed}")
  println("the symlink survived: \{back[3].link}")
}