///|
/// Minimal byte-oriented storage boundary for immutable index files.
pub(open) trait Directory {
  fn write(Self, String, Bytes) -> Unit raise PersistenceError
  fn read(Self, String) -> Bytes raise PersistenceError
  fn exists(Self, String) -> Bool
}

///|
fn validate_directory_name(name : String) -> Unit raise PersistenceError {
  guard name != "" &&
    name != "." &&
    name != ".." &&
    !name.contains("/") &&
    !name.contains("\\") else {
    raise PersistenceError::InvalidName(name)
  }
}

///|
/// In-memory Directory implementation. Reads and writes copy bytes so a
/// caller cannot mutate an already committed snapshot through an alias.
pub struct MemoryDirectory {
  names : Array[String]
  contents : Array[Bytes]
}

///|
pub fn MemoryDirectory::new() -> MemoryDirectory {
  { names: [], contents: [] }
}

///|
pub impl Directory for MemoryDirectory with fn write(self, name, bytes) {
  validate_directory_name(name)
  let snapshot = bytes[:].to_owned()
  match self.names.search_by(existing => existing == name) {
    Some(index) => self.contents[index] = snapshot
    None => {
      self.names.push(name)
      self.contents.push(snapshot)
    }
  }
}

///|
pub impl Directory for MemoryDirectory with fn read(self, name) {
  validate_directory_name(name)
  match self.names.search_by(existing => existing == name) {
    Some(index) => self.contents[index][:].to_owned()
    None => raise PersistenceError::NotFound(name)
  }
}

///|
pub impl Directory for MemoryDirectory with fn exists(self, name) {
  try validate_directory_name(name) catch {
    _ => false
  } noraise {
    _ => self.names.search_by(existing => existing == name) is Some(_)
  }
}

///|
/// Filesystem-backed Directory rooted at one directory.
pub struct FsDirectory {
  root : String
}

///|
fn[T] wrap_fs_error(
  operation : () -> T raise @fs.IOError,
) -> T raise PersistenceError {
  operation() catch {
    @fs.IOError::IOError(message) => raise PersistenceError::Io(message)
  }
}

///|
pub fn FsDirectory::new(root : String) -> FsDirectory raise PersistenceError {
  guard root != "" else { raise PersistenceError::InvalidName(root) }
  if @fs.path_exists(root) {
    guard wrap_fs_error(() => @fs.is_dir(root)) else {
      raise PersistenceError::Io("directory root is not a directory: \{root}")
    }
  } else {
    wrap_fs_error(() => @fs.create_dir(root))
  }
  { root, }
}

///|
fn FsDirectory::path(
  self : FsDirectory,
  name : String,
) -> String raise PersistenceError {
  validate_directory_name(name)
  "\{self.root}/\{name}"
}

///|
pub impl Directory for FsDirectory with fn write(self, name, bytes) {
  let path = self.path(name)
  wrap_fs_error(() => @fs.write_bytes_to_file(path, bytes))
}

///|
pub impl Directory for FsDirectory with fn read(self, name) {
  let path = self.path(name)
  if !@fs.path_exists(path) {
    raise PersistenceError::NotFound(name)
  }
  wrap_fs_error(() => @fs.read_file_to_bytes(path))
}

///|
pub impl Directory for FsDirectory with fn exists(self, name) {
  try self.path(name) catch {
    _ => false
  } noraise {
    path => @fs.path_exists(path)
  }
}