///|
priv struct Member {
  id : Bytes
  name : String
  length : Int32
  hash : Bytes
  desc : Bytes
  sums : Bytes
}

///|
pub fn safe_name(name : String) -> Bool {
  if name.is_empty() ||
    name.length() > 180 ||
    name.has_prefix(".") ||
    name.has_suffix(".") ||
    name.has_suffix(" ") {
    return false
  }
  for c in name {
    if !((c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') ||
      c == '-' ||
      c == '_' ||
      c == '.' ||
      c == ' ') {
      return false
    }
  }
  let base = name.to_lower().split(".").next().unwrap().to_owned()
  if ["con", "prn", "aux", "nul"].contains(base) {
    return false
  }
  for i = 0; i <= 9; i = i + 1 {
    if base == "com\{i}" || base == "lpt\{i}" {
      return false
    }
  }
  true
}

///|
fn slice_at(data : Bytes, start : Int32, width : Int32) -> BytesView {
  if data.length() - start >= width {
    return data[start:start + width]
  }
  let padded = Bytes::makei(width, fn(j) {
    if start + j < data.length() {
      data[start + j]
    } else {
      b'\x00'
    }
  })
  padded[:]
}

///|
fn xor_slice(
  row : Array[Int32],
  offset : Int32,
  data : BytesView,
  factor : Int32,
) -> Unit {
  if factor == 0 {
    return
  }
  let log = field_logs[factor]
  for j = 0; j < data.length() / 2; j = j + 1 {
    let word = data[j * 2].to_int() | (data[j * 2 + 1].to_int() << 8)
    if word != 0 {
      let e = log + field_logs[word]
      row[offset + j] = row[offset + j] ^
        field_powers[if e >= 65535 { e - 65535 } else { e }]
    }
  }
}

///|
fn as_words(block : Bytes) -> Array[Int32] {
  Array::makei(block.length() / 2, fn(i) {
    block[i * 2].to_int() | (block[i * 2 + 1].to_int() << 8)
  })
}

///|
fn as_bytes(words : Array[Int32]) -> Bytes {
  Bytes::makei(words.length() * 2, fn(i) {
    (words[i / 2] >> (8 * (i % 2))).to_byte()
  })
}

///|
fn describe(name : String, data : Bytes, width : Int32) -> Member {
  let prefix = md5(data[:data.length().min(16384)].to_owned())
  let buf = @buffer.Buffer()
  buf.write_bytes(prefix)
  buf.write_uint64_le(data.length().to_uint64())
  let filename = @utf8.encode(name)
  buf.write_bytes(filename)
  let id = md5(buf.to_bytes())
  for i = 0; i < (4 - filename.length() % 4) % 4; i = i + 1 {
    buf.write_byte(b'\x00')
  }
  let hash = md5(data)
  let desc = joined([id, hash, buf.to_bytes()])
  let sums = @buffer.Buffer()
  sums.write_bytes(id)
  for at = 0; at < data.length(); at = at + width {
    let b = slice_at(data, at, width)
    sums.write_bytes(md5(b))
    sums.write_uint_le(crc32(b))
  }
  { id, name, length: data.length(), hash, desc, sums: sums.to_bytes(), }
}

///|
test "portable names and partial slice padding" {
  assert_true(safe_name("photo 1.jpg"))
  for bad in ["../x", "NUL.txt", ".", "AUX", "COM1.cfg", "a/b", "a:"] {
    assert_false(safe_name(bad))
  }
  assert_eq(slice_at(b"12345", 4, 4).to_owned(), b"5\x00\x00\x00")
}