///|
pub struct ObjectId {
  hex : String
} derive(Eq, Debug)

///|
pub fn ObjectId::to_string(self : ObjectId) -> String {
  self.hex
}

///|
pub fn ObjectId::parse(value : String) -> ObjectId raise VcsError {
  if value.length() != 64 {
    raise VcsError::InvalidObjectId(value)
  }
  for byte in @utf8.encode(value) {
    let n = byte.to_int()
    if !((n >= 48 && n <= 57) || (n >= 97 && n <= 102)) {
      raise VcsError::InvalidObjectId(value)
    }
  }
  { hex: value, }
}

///|
pub(all) struct Blob {
  content : Bytes
} derive(Eq, Debug)

///|
pub(all) enum TreeEntryKind {
  BlobEntry
  TreeEntry
} derive(Eq, Debug)

///|
pub(all) struct TreeEntry {
  name : String
  kind : TreeEntryKind
  object_id : ObjectId
} derive(Eq, Debug)

///|
pub(all) struct Tree {
  entries : Array[TreeEntry]
} derive(Eq, Debug)

///|
pub fn Tree::new(entries : Array[TreeEntry]) -> Tree raise VcsError {
  let normalized = entries.copy()
  normalized.sort_by((left, right) => compare_utf8(left.name, right.name))
  let mut previous : String? = None
  for entry in normalized {
    validate_tree_name(entry.name)
    if previous == Some(entry.name) {
      raise VcsError::InvalidTree("duplicate tree entry: \{entry.name}")
    }
    previous = Some(entry.name)
  }
  { entries: normalized, }
}

///|
fn compare_utf8(left : String, right : String) -> Int32 {
  let a = @utf8.encode(left)
  let b = @utf8.encode(right)
  let common_length = if a.length() < b.length() {
    a.length()
  } else {
    b.length()
  }
  for index in 0.. Unit raise VcsError {
  if name.length() == 0 ||
    name == "." ||
    name == ".." ||
    name.contains("/") ||
    name.contains("\\") ||
    name.contains("\u0000") {
    raise VcsError::InvalidTree("invalid tree entry name: \{name}")
  }
}

///|
pub(all) struct Commit {
  tree_id : ObjectId
  parents : Array[ObjectId]
  author : String
  timestamp : UInt64
  message : String
} derive(Eq, Debug)

///|
pub(all) struct Ref {
  name : String
  target : ObjectId
} derive(Eq, Debug)

///|
pub(all) struct CandidateFile {
  path : String
  blob_id : ObjectId
  content : Bytes
} derive(Eq, Debug)

///|
pub(all) struct CommitCandidate {
  tree_id : ObjectId
  parents : Array[ObjectId]
  author : String
  timestamp : UInt64
  message : String
  staged_files : Array[CandidateFile]
} derive(Eq, Debug)

///|
pub(all) enum VcsObject {
  BlobObject(Blob)
  TreeObject(Tree)
  CommitObject(Commit)
} derive(Eq, Debug)

///|
pub(all) suberror VcsError {
  InvalidObjectId(String)
  InvalidObject(String)
  InvalidTree(String)
  InvalidPath(String)
  ObjectNotFound(String)
  UnknownObject(String)
  ObjectUnavailableOffline(String)
  RemoteUnavailable(String)
  RemoteObjectMissing(String)
  InvalidPromise(String)
  RemoteRefChanged(String)
  RepositoryNotFound(String)
  RepositoryExists(String)
  RefConflict(String)
  StagingBusy(String)
  PublicationUncertain(String)
  ReceiveRejected(String)
  ReceiveInvalid(String)
  ReceivePolicyError(String)
  ReceiveProcessExit(Int32)
  PolicyRejected(String)
  PolicyExecutionFailed(String)
  InvalidCommand(String)
  DiffFailed(String)
  CheckoutFailed(String)
  SwitchIncomplete(String)
} derive(Debug)

///|
pub(all) enum PolicyDecision {
  PolicyPass
  PolicyReject(String)
  PolicyError(String)
} derive(Eq, Debug)

///|
pub(open) trait PolicyValidator {
  async fn validate(Self, CommitCandidate) -> PolicyDecision
}

///|
pub struct MockPolicyValidator {
  allow : Bool
  reason : String
}

///|
pub fn MockPolicyValidator::pass() -> MockPolicyValidator {
  { allow: true, reason: "", }
}

///|
pub fn MockPolicyValidator::reject(reason : String) -> MockPolicyValidator {
  { allow: false, reason, }
}

///|
pub impl PolicyValidator for MockPolicyValidator with fn validate(self, _) {
  if self.allow {
    PolicyPass
  } else {
    PolicyReject(self.reason)
  }
}