///|
/// What went wrong, in terms a caller can branch on.
///
/// The set is OpenDAL's, minus the kinds only a network backend can produce,
/// plus two this library needs and OpenDAL folds into `Unexpected`:
///
/// - `InvalidPath`, because a rejected path is the caller's bug, and a caller
///   that cannot tell it from an I/O failure will retry it forever.
/// - `DirectoryNotEmpty`, because the only other honest answer is `Unexpected`,
///   and then a caller cannot tell "pass `recursive`" from "something broke".
pub(all) enum ErrorKind {
  /// Nothing at that path.
  NotFound
  /// Something is already there, and the caller asked not to overwrite it.
  AlreadyExists
  PermissionDenied
  /// A file operation was aimed at a directory.
  IsADirectory
  /// A directory operation was aimed at a file.
  NotADirectory
  /// A directory still has children, and `recursive` was not set.
  DirectoryNotEmpty
  /// The path could never name anything in a store. See `validate`.
  InvalidPath
  /// The backend does not do this. Raised by `Operator` from `Capability`,
  /// before the call reaches the store.
  Unsupported
  ConfigInvalid
  /// A ranged read started past the end of the object.
  RangeNotSatisfied
  /// Everything else, including whatever a backend's own dependency raised.
  Unexpected
} derive(Eq, Debug)

///|
/// Written by hand rather than derived, because these names end up in the
/// message a person reads when a store call fails.
pub impl Show for ErrorKind with fn output(self, logger) {
  logger.write_string(
    match self {
      NotFound => "not found"
      AlreadyExists => "already exists"
      PermissionDenied => "permission denied"
      IsADirectory => "is a directory"
      NotADirectory => "not a directory"
      DirectoryNotEmpty => "directory not empty"
      InvalidPath => "invalid path"
      Unsupported => "unsupported"
      ConfigInvalid => "invalid config"
      RangeNotSatisfied => "range not satisfied"
      Unexpected => "unexpected"
    },
  )
}

///|
/// Every failure this library raises.
///
/// One error type, not one per backend: the point of the `Store` seam is that a
/// caller's `catch` sees the same thing whether the bytes came from a `Map`, a
/// disk or a browser database. A backend that lets `@os_error.OSError` or a JS
/// error escape has broken the abstraction, so each translates at its boundary.
pub(all) suberror SosError {
  SosError(
    kind~ : ErrorKind,
    /// The operation that failed: "read", "write", "list", ...
    operation~ : String,
    /// The store path it was on. Empty when there is none.
    path~ : String,
    /// One sentence for a person. May be empty.
    message~ : String
  )
}

///|
pub fn SosError::new(
  kind : ErrorKind,
  operation~ : String,
  path? : String = "",
  message? : String = "",
) -> SosError {
  SosError(kind~, operation~, path~, message~)
}

///|
pub fn SosError::kind(self : SosError) -> ErrorKind {
  let SosError(kind~, ..) = self
  kind
}

///|
pub fn SosError::operation(self : SosError) -> String {
  let SosError(operation~, ..) = self
  operation
}

///|
pub fn SosError::path(self : SosError) -> String {
  let SosError(path~, ..) = self
  path
}

///|
pub fn SosError::message(self : SosError) -> String {
  let SosError(message~, ..) = self
  message
}

///|
/// True when the store said "there is nothing here".
///
/// The one kind callers branch on often enough to deserve a name; the rest go
/// through `kind()`.
pub fn SosError::is_not_found(self : SosError) -> Bool {
  self.kind() is NotFound
}

///|
pub impl Show for SosError with fn output(self, logger) {
  let SosError(kind~, operation~, path~, message~) = self
  logger.write_string("\{kind} in \{operation}")
  if path != "" {
    logger.write_string(" at '\{path}'")
  }
  if message != "" {
    logger.write_string(": \{message}")
  }
}