///|
/// The front door.
///
/// Holds a `&Store` and does three things the trait deliberately does not:
/// normalises and validates every path, turns labelled optional arguments into
/// the option structs, and refuses up front whatever `Capability` says the
/// backend cannot do. A backend author writes `Store`; everyone else writes
/// this.
struct Operator {
  store : &Store
  info : StoreInfo
}

///|
pub fn Operator::new(store : &Store) -> Operator {
  { store, info: store.info(), }
}

///|
pub fn Operator::info(self : Operator) -> StoreInfo {
  self.info
}

///|
pub fn Operator::capability(self : Operator) -> Capability {
  self.info.capability
}

///|
/// The store underneath, for a caller that needs the raw seam.
pub fn Operator::store(self : Operator) -> &Store {
  self.store
}

///|
/// Raise `Unsupported` unless the backend declared it can do this.
///
/// Checked here rather than in each backend so an unsupported option fails
/// naming the operation the caller asked for, instead of somewhere inside a
/// backend that had to invent an error for a case it does not implement.
fn Operator::require(
  self : Operator,
  supported : Bool,
  operation~ : String,
  path~ : String,
  what~ : String,
) -> Unit raise SosError {
  guard supported else {
    raise SosError::new(
      Unsupported,
      operation~,
      path~,
      message="the \{self.info.scheme} store does not support \{what}",
    )
  }
}

///|
pub async fn Operator::read(
  self : Operator,
  path : String,
  range? : Range,
) -> Bytes raise SosError {
  let path = validate_file(path, operation="read")
  let cap = self.info.capability
  self.require(cap.read, operation="read", path~, what="read")
  if range is Some(_) {
    self.require(
      cap.read_with_range,
      operation="read",
      path~,
      what="ranged reads",
    )
  }
  self.store.read(path, { range, })
}

///|
/// `read`, decoded as UTF-8.
pub async fn Operator::read_text(
  self : Operator,
  path : String,
  range? : Range,
) -> String raise SosError {
  let bytes = self.read(path, range?)
  @utf8.decode(bytes) catch {
    _ =>
      raise SosError::new(
        Unexpected,
        operation="read_text",
        path~,
        message="content is not valid UTF-8",
      )
  }
}

///|
pub async fn Operator::write(
  self : Operator,
  path : String,
  content : Bytes,
  content_type? : String,
  append? : Bool = false,
  if_not_exists? : Bool = false,
) -> Unit raise SosError {
  let path = validate_file(path, operation="write")
  let cap = self.info.capability
  self.require(cap.write, operation="write", path~, what="write")
  if content_type is Some(_) {
    self.require(
      cap.write_with_content_type,
      operation="write",
      path~,
      what="content types",
    )
  }
  if append {
    self.require(cap.write_can_append, operation="write", path~, what="append")
  }
  if if_not_exists {
    self.require(
      cap.write_with_if_not_exists,
      operation="write",
      path~,
      what="conditional create",
    )
  }
  self.store.write(path, content, { content_type, append, if_not_exists, })
}

///|
/// `write`, encoding the string as UTF-8.
pub async fn Operator::write_text(
  self : Operator,
  path : String,
  content : String,
  content_type? : String,
  append? : Bool = false,
  if_not_exists? : Bool = false,
) -> Unit raise SosError {
  self.write(
    path,
    @utf8.encode(content),
    content_type?,
    append~,
    if_not_exists~,
  )
}

///|
pub async fn Operator::stat(
  self : Operator,
  path : String,
) -> Metadata raise SosError {
  let path = validate(path, operation="stat")
  self.require(self.info.capability.stat, operation="stat", path~, what="stat")
  self.store.stat(path)
}

///|
/// Does it resolve? `stat` reduced to the question most callers were asking.
pub async fn Operator::exists(
  self : Operator,
  path : String,
) -> Bool raise SosError {
  try {
    self.stat(path) |> ignore
    true
  } catch {
    err => if err.is_not_found() { false } else { raise err }
  }
}

///|
pub async fn Operator::list(
  self : Operator,
  path : String,
  recursive? : Bool = false,
  limit? : Int,
  start_after? : String,
) -> Array[Entry] raise SosError {
  let path = validate_dir(path, operation="list")
  let cap = self.info.capability
  self.require(cap.list, operation="list", path~, what="list")
  if recursive {
    self.require(
      cap.list_with_recursive,
      operation="list",
      path~,
      what="recursive listing",
    )
  }
  if limit is Some(_) {
    self.require(cap.list_with_limit, operation="list", path~, what="limit")
  }
  if start_after is Some(_) {
    self.require(
      cap.list_with_start_after,
      operation="list",
      path~,
      what="start_after",
    )
  }
  self.store.list(path, { recursive, limit, start_after, })
}

///|
pub async fn Operator::delete(
  self : Operator,
  path : String,
  recursive? : Bool = false,
) -> Unit raise SosError {
  let path = validate(path, operation="delete")
  let cap = self.info.capability
  self.require(cap.delete, operation="delete", path~, what="delete")
  if recursive {
    self.require(
      cap.delete_with_recursive,
      operation="delete",
      path~,
      what="recursive delete",
    )
  }
  self.store.delete(path, { recursive, })
}

///|
pub async fn Operator::create_dir(
  self : Operator,
  path : String,
) -> Unit raise SosError {
  let path = validate_dir(path, operation="create_dir")
  self.require(
    self.info.capability.create_dir,
    operation="create_dir",
    path~,
    what="create_dir",
  )
  self.store.create_dir(path)
}

///|
pub async fn Operator::copy(
  self : Operator,
  from : String,
  to : String,
) -> Unit raise SosError {
  let from = validate_file(from, operation="copy")
  let to = validate_file(to, operation="copy")
  self.require(
    self.info.capability.copy,
    operation="copy",
    path=from,
    what="copy",
  )
  self.store.copy(from, to)
}

///|
pub async fn Operator::rename(
  self : Operator,
  from : String,
  to : String,
) -> Unit raise SosError {
  let from = validate_file(from, operation="rename")
  let to = validate_file(to, operation="rename")
  self.require(
    self.info.capability.rename,
    operation="rename",
    path=from,
    what="rename",
  )
  self.store.rename(from, to)
}