///|
/// Raised when a text breaks the rules of an OKF value type. Each case names
/// the input.
pub(all) suberror ValueError {
  /// A bundle label with an empty segment, or a segment with `/`, `:` or white space.
  InvalidLabel(String)
  /// A concept type that is empty or only white space.
  BlankConceptType(String)
  /// A frontmatter key that is not a plain key: a letter, a digit or `_`
  /// first, then letters, digits, `_`, `.` or `-`.
  InvalidFieldKey(String)
  /// A rule name that is not lower-case kebab case: letters, digits and single
  /// `-` between them.
  InvalidRuleName(String)
  /// A search limit outside 1 to `max_search_limit`.
  InvalidSearchLimit(Int32)
  /// A date that is not a real calendar date in the form `YYYY-MM-DD`.
  InvalidDate(String)
  /// A text that is not an ISO 8601 instant or a `YYYY-MM-DD` date.
  InvalidInstant(String)
  /// A register, state or record name that is empty or only white space.
  BlankName(String)
  /// A record ID width outside 1 to 9.
  InvalidIdWidth(Int32)
  /// A register transition or superseded state that names a state the
  /// register does not have.
  UnknownState(register~ : String, state~ : String)
  /// A reference field that names a register that no profile defines.
  UnknownRegister(register~ : String)
} derive(Eq, Debug)

///|
fn is_space_char(c : Char) -> Bool {
  c == ' ' || c == '\t' || c == '\n' || c == '\r'
}

///|
/// A bundle label: its group segments and its name, joined with `/`, such as
/// `morphir/ir` or `my notes`. No segment is empty or holds `/`, `:` or a
/// line break.
struct BundleLabel {
  segments : ReadOnlyArray[String]
} derive(Eq, Hash, Debug)

///|
/// Parses a bundle label such as `morphir/ir`, or a bare name such as `ir`.
pub fn BundleLabel::BundleLabel(text : String) -> BundleLabel raise ValueError {
  let segments = text.split("/").map(s => s.to_owned()).collect()
  for s in segments {
    if s.length() == 0 ||
      s.contains(":") ||
      s.contains("\n") ||
      s.contains("\r") {
      raise InvalidLabel(text)
    }
  }
  { segments: ReadOnlyArray::from_array(segments), }
}

///|
/// The label of a bundle from its group directories and its directory name.
/// A store path segment is not empty and has no `/`, but it can hold `:` or a
/// line break, which a label cannot. Each `:` becomes `-` and each line break
/// becomes a space, so that every directory gives a label that parses back.
fn BundleLabel::of_parts(
  group : ArrayView[String],
  name : String,
) -> BundleLabel {
  let segments = group.to_owned()
  segments.push(name)
  let segments = segments.map(s => {
    s
    .replace_all(old=":", new="-")
    .replace_all(old="\r", new=" ")
    .replace_all(old="\n", new=" ")
  })
  { segments: ReadOnlyArray::from_array(segments), }
}

///|
/// The last segment, the bundle name.
pub fn BundleLabel::name(self : BundleLabel) -> String {
  self.segments[self.segments.length() - 1]
}

///|
/// The segments before the name.
pub fn BundleLabel::group(self : BundleLabel) -> ReadOnlyArray[String] {
  ReadOnlyArray::from_array(
    self.segments.exact_view(end=self.segments.length() - 1).to_owned(),
  )
}

///|
/// True when the label has only a name, so it can match a bundle by name.
pub fn BundleLabel::is_bare(self : BundleLabel) -> Bool {
  self.segments.length() == 1
}

///|
pub fn BundleLabel::to_string(self : BundleLabel) -> String {
  self.segments.join("/")
}

///|
pub impl Show for BundleLabel with fn output(self, logger) {
  logger.write_string(self.to_string())
}

///|
pub impl Compare for BundleLabel with fn compare(self, other) {
  lexical_compare(self.to_string(), other.to_string())
}

///|
/// A directory below the root of a document set or store, given as segments.
/// The root itself has no segments. No segment is empty, `.` or `..`.
struct StoreDir {
  segments : ReadOnlyArray[String]
} derive(Eq, Hash, Debug)

///|
/// Parses a directory path such as `kb/bundles`. `/` and `\` are both
/// separators; `""` is the root. A path that escapes the root is refused.
pub fn StoreDir::StoreDir(text : String) -> StoreDir raise PathError {
  let normalized = normalize_path(text)
  if normalized.above > 0 {
    raise EscapesRoot(text)
  }
  { segments: normalized.segments, }
}

///|
/// The root directory.
pub fn StoreDir::root() -> StoreDir {
  { segments: ReadOnlyArray::from_array([]), }
}

///|
fn StoreDir::of_segments(segments : ArrayView[String]) -> StoreDir {
  { segments: ReadOnlyArray::from_array(segments.to_owned()), }
}

///|
pub fn StoreDir::segments(self : StoreDir) -> ReadOnlyArray[String] {
  self.segments
}

///|
pub fn StoreDir::is_root(self : StoreDir) -> Bool {
  self.segments.is_empty()
}

///|
/// The last segment, or `None` for the root.
pub fn StoreDir::name(self : StoreDir) -> String? {
  self.segments.last()
}

///|
/// The child directory with the name of one entry, such as a listed directory.
pub fn StoreDir::child(self : StoreDir, entry : EntryName) -> StoreDir {
  let segments = self.segments.iter().collect()
  segments.push(entry.to_string())
  { segments: ReadOnlyArray::from_array(segments), }
}

///|
/// The path of the file `entry` in this directory.
pub fn StoreDir::file(self : StoreDir, entry : EntryName) -> StorePath {
  let segments = self.segments.iter().collect()
  segments.push(entry.to_string())
  StorePath::of_segments(segments)
}

///|
/// The store path of a bundle path, with this directory as the bundle root.
pub fn StoreDir::resolve(self : StoreDir, path : BundlePath) -> StorePath {
  let segments = self.segments.iter().collect()
  for segment in path.segments() {
    segments.push(segment)
  }
  StorePath::of_segments(segments)
}

///|
/// True when `path` is inside this directory, at any depth.
pub fn StoreDir::contains(self : StoreDir, path : StorePath) -> Bool {
  path.is_under(self)
}

///|
/// True when `other` is this directory or below it.
pub fn StoreDir::encloses(self : StoreDir, other : StoreDir) -> Bool {
  if self.segments.length() > other.segments.length() {
    return false
  }
  for i, s in self.segments {
    if other.segments[i] != s {
      return false
    }
  }
  true
}

///|
pub fn StoreDir::to_string(self : StoreDir) -> String {
  self.segments.join("/")
}

///|
pub impl Show for StoreDir with fn output(self, logger) {
  logger.write_string(self.to_string())
}

///|
pub impl Compare for StoreDir with fn compare(self, other) {
  compare_segments(self.segments, other.segments)
}

///|
/// One name in a directory listing: one segment that is not empty, `.` or
/// `..` and has no `/` or `\`.
struct EntryName {
  text : String
} derive(Eq, Hash, Debug)

///|
pub fn EntryName::EntryName(text : String) -> EntryName raise PathError {
  if text.length() == 0 || text == "." || text == ".." {
    raise EmptyPath(text)
  }
  if text.contains("/") || text.contains("\\") {
    raise EscapesRoot(text)
  }
  { text, }
}

///|
pub fn EntryName::to_string(self : EntryName) -> String {
  self.text
}

///|
pub impl Show for EntryName with fn output(self, logger) {
  logger.write_string(self.text)
}

///|
/// An OKF concept `type`, such as `Decision Record`. It is not blank, and two
/// concept types are equal when they differ only in letter case.
struct ConceptType {
  text : String
} derive(Debug)

///|
pub fn ConceptType::ConceptType(text : String) -> ConceptType raise ValueError {
  if text.iter().all(is_space_char) {
    raise BlankConceptType(text)
  }
  { text, }
}

///|
/// The type as written.
pub fn ConceptType::to_string(self : ConceptType) -> String {
  self.text
}

///|
pub impl Eq for ConceptType with fn equal(self, other) {
  self.text.to_lower() == other.text.to_lower()
}

///|
pub impl Hash for ConceptType with fn hash_combine(self, hasher) {
  hasher.combine_string(self.text.to_lower())
}

///|
pub impl Show for ConceptType with fn output(self, logger) {
  logger.write_string(self.text)
}

///|
pub impl Compare for ConceptType with fn compare(self, other) {
  lexical_compare(self.text.to_lower(), other.text.to_lower())
}

///|
fn is_key_char(c : Char, first : Bool) -> Bool {
  (c >= 'a' && c <= 'z') ||
  (c >= 'A' && c <= 'Z') ||
  (c >= '0' && c <= '9') ||
  c == '_' ||
  (!first && (c == '.' || c == '-'))
}

///|
/// A plain frontmatter key that edits can write and profiles can define: a
/// letter, a digit or `_` first, then letters, digits, `_`, `.` or `-`. Reading
/// metadata takes any YAML key as text.
struct FieldKey {
  text : String
} derive(Eq, Hash, Debug)

///|
pub fn FieldKey::FieldKey(text : String) -> FieldKey raise ValueError {
  let chars = text.to_array()
  let mut ok = chars.length() > 0
  for i, c in chars {
    if !is_key_char(c, i == 0) {
      ok = false
    }
  }
  if !ok {
    raise InvalidFieldKey(text)
  }
  { text, }
}

///|
pub fn FieldKey::to_string(self : FieldKey) -> String {
  self.text
}

///|
pub impl Show for FieldKey with fn output(self, logger) {
  logger.write_string(self.text)
}

///|
pub impl Compare for FieldKey with fn compare(self, other) {
  lexical_compare(self.text, other.text)
}

///|
/// The name of a validation rule, in lower-case kebab case, such as `link-broken`.
struct RuleName {
  text : String
} derive(Eq, Hash, Debug)

///|
pub fn RuleName::RuleName(text : String) -> RuleName raise ValueError {
  let ok = text.length() > 0 &&
    !text.has_prefix("-") &&
    !text.has_suffix("-") &&
    !text.contains("--") &&
    text
    .iter()
    .all(c => (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-')
  if !ok {
    raise InvalidRuleName(text)
  }
  { text, }
}

///|
pub fn RuleName::to_string(self : RuleName) -> String {
  self.text
}

///|
pub impl Show for RuleName with fn output(self, logger) {
  logger.write_string(self.text)
}

///|
pub impl Compare for RuleName with fn compare(self, other) {
  lexical_compare(self.text, other.text)
}

///|
/// The largest search limit.
pub let max_search_limit : Int32 = 1000

///|
/// The number of results that a search request may give: 1 to `max_search_limit`.
struct SearchLimit {
  value : Int32
} derive(Eq, Hash, Debug)

///|
pub fn SearchLimit::SearchLimit(value : Int32) -> SearchLimit raise ValueError {
  if value < 1 || value > max_search_limit {
    raise InvalidSearchLimit(value)
  }
  { value, }
}

///|
/// The default limit, 20.
pub fn SearchLimit::default() -> SearchLimit {
  { value: 20, }
}

///|
pub fn SearchLimit::to_int(self : SearchLimit) -> Int32 {
  self.value
}

///|
/// A calendar date in the form `YYYY-MM-DD`, as log headings and log edits use.
struct LogDate {
  text : String
  instant : Instant
} derive(Eq, Hash, Debug)

///|
/// Parses a real calendar date written exactly as `YYYY-MM-DD`.
pub fn LogDate::LogDate(text : String) -> LogDate raise ValueError {
  match Instant::parse_date(text) {
    Some(instant) => { text, instant, }
    None => raise InvalidDate(text)
  }
}

///|
/// The UTC calendar date of an instant.
pub fn LogDate::of_instant(instant : Instant) -> LogDate {
  let text = instant.date_text()
  { text, instant: Instant::parse_date(text).unwrap_or(instant), }
}

///|
pub fn LogDate::to_string(self : LogDate) -> String {
  self.text
}

///|
/// The start of the date in UTC.
pub fn LogDate::instant(self : LogDate) -> Instant {
  self.instant
}

///|
pub impl Show for LogDate with fn output(self, logger) {
  logger.write_string(self.text)
}

///|
pub impl Compare for LogDate with fn compare(self, other) {
  self.instant.compare(other.instant)
}

///|
/// The OKF version that a bundle declares in its root index. Only
/// `OkfVersion::parse` makes values, so `Unknown` never holds a known version.
pub enum OkfVersion {
  V01
  V02
  /// A version that this library does not know, with its text.
  Unknown(String)
} derive(Eq, Debug)

///|
pub fn OkfVersion::parse(text : String) -> OkfVersion {
  match text {
    "0.1" => V01
    "0.2" => V02
    _ => Unknown(text)
  }
}

///|
pub fn OkfVersion::to_string(self : OkfVersion) -> String {
  match self {
    V01 => "0.1"
    V02 => "0.2"
    Unknown(text) => text
  }
}

///|
pub fn OkfVersion::is_known(self : OkfVersion) -> Bool {
  !(self is Unknown(_))
}

///|
pub extend ValueError with Eq::{not_equal, equal}

///|
pub extend ValueError with @debug.Debug::{to_repr}

///|
pub extend BundleLabel with Eq::{not_equal, equal}

///|
pub extend BundleLabel with Hash::{hash, hash_combine}

///|
pub extend BundleLabel with @debug.Debug::{to_repr}

///|
pub extend BundleLabel with Show::{output}

///|
pub extend BundleLabel with Compare::{op_lt, op_le, op_ge, compare, op_gt}

///|
pub extend StoreDir with Eq::{not_equal, equal}

///|
pub extend StoreDir with Hash::{hash, hash_combine}

///|
pub extend StoreDir with @debug.Debug::{to_repr}

///|
pub extend StoreDir with Show::{output}

///|
pub extend StoreDir with Compare::{op_lt, op_le, op_ge, compare, op_gt}

///|
pub extend EntryName with Eq::{not_equal, equal}

///|
pub extend EntryName with Hash::{hash, hash_combine}

///|
pub extend EntryName with @debug.Debug::{to_repr}

///|
pub extend EntryName with Show::{output}

///|
pub extend ConceptType with @debug.Debug::{to_repr}

///|
pub extend ConceptType with Eq::{not_equal, equal}

///|
pub extend ConceptType with Hash::{hash, hash_combine}

///|
pub extend ConceptType with Show::{output}

///|
pub extend ConceptType with Compare::{op_lt, op_le, op_ge, compare, op_gt}

///|
pub extend FieldKey with Eq::{not_equal, equal}

///|
pub extend FieldKey with Hash::{hash, hash_combine}

///|
pub extend FieldKey with @debug.Debug::{to_repr}

///|
pub extend FieldKey with Show::{output}

///|
pub extend FieldKey with Compare::{op_lt, op_le, op_ge, compare, op_gt}

///|
pub extend RuleName with Eq::{not_equal, equal}

///|
pub extend RuleName with Hash::{hash, hash_combine}

///|
pub extend RuleName with @debug.Debug::{to_repr}

///|
pub extend RuleName with Show::{output}

///|
pub extend RuleName with Compare::{op_lt, op_le, op_ge, compare, op_gt}

///|
pub extend SearchLimit with Eq::{not_equal, equal}

///|
pub extend SearchLimit with Hash::{hash, hash_combine}

///|
pub extend SearchLimit with @debug.Debug::{to_repr}

///|
pub extend LogDate with Eq::{not_equal, equal}

///|
pub extend LogDate with Hash::{hash, hash_combine}

///|
pub extend LogDate with @debug.Debug::{to_repr}

///|
pub extend LogDate with Show::{output}

///|
pub extend LogDate with Compare::{op_lt, op_le, op_ge, compare, op_gt}

///|
pub extend OkfVersion with Eq::{not_equal, equal}

///|
pub extend OkfVersion with @debug.Debug::{to_repr}