///|
/// A rule of a log directory.
pub enum LogDirectoryRule {
  /// The path has a NUL character, which no file system accepts.
  NulCharacter
} derive(Eq, Debug)

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

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

///|
/// A session setting that breaks a rule. Each case names the input and the rule.
pub suberror SettingsError {
  /// The outstanding record limit is not 1 or more.
  InvalidQueueEventLimit(count~ : Int)
  /// The outstanding payload limit is not 1 byte or more.
  InvalidQueueByteLimit(bytes~ : Int64)
  /// The limit of one record is not 1 byte or more.
  InvalidRecordByteLimit(bytes~ : Int64)
  /// The limit of one record is larger than the outstanding payload limit, so a
  /// record at that limit could never enter the queue.
  RecordLimitAboveQueueLimit(record~ : Int64, queue~ : Int64)
  /// The log directory breaks the named rule.
  InvalidLogDirectory(path~ : String, rule~ : LogDirectoryRule)
  /// Two file destinations use the same directory, stem and format. One file
  /// set has one writer.
  DuplicateFileSet(directory~ : String, name~ : String)
} derive(Debug)

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

///|
/// Text for people that names the input and the broken rule.
pub fn SettingsError::message(self : SettingsError) -> String {
  match self {
    InvalidQueueEventLimit(count~) =>
      "Invalid queue event limit \{count}: it must be 1 or more"
    InvalidQueueByteLimit(bytes~) =>
      "Invalid queue byte limit \{bytes}: it must be 1 or more"
    InvalidRecordByteLimit(bytes~) =>
      "Invalid record byte limit \{bytes}: it must be 1 or more"
    RecordLimitAboveQueueLimit(record~, queue~) =>
      "Invalid record byte limit \{record}: it must not be more than the queue byte limit \{queue}"
    InvalidLogDirectory(path~, rule~) => {
      let reason = match rule {
        NulCharacter => "the path has a NUL character"
      }
      "Invalid log directory \{path.escape()}: \{reason}"
    }
    DuplicateFileSet(directory~, name~) =>
      "Two file destinations use the file set \{name.escape()} in \{directory.escape()}"
  }
}

///|
/// The largest count of records that are accepted but not yet written, including
/// the record that the writer writes now.
pub struct QueueEventLimit {
  priv count : Int
} derive(Eq, Debug)

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

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

///|
/// Make a queue event limit. It raises `InvalidQueueEventLimit` when `count` is
/// less than 1.
pub fn QueueEventLimit::count(count : Int) -> QueueEventLimit raise SettingsError {
  guard count >= 1 else { raise InvalidQueueEventLimit(count~) }
  { count, }
}

///|
/// The default limit: 1024 records.
pub fn QueueEventLimit::default() -> QueueEventLimit {
  { count: 1024 }
}

///|
/// The limit as a count of records.
pub fn QueueEventLimit::to_int(self : QueueEventLimit) -> Int {
  self.count
}

///|
/// The largest count of encoded payload bytes that are accepted but not yet
/// written. The count includes the payload of each destination and the record
/// that the writer writes now.
pub struct QueueByteLimit {
  priv bytes : Int64
} derive(Eq, Debug)

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

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

///|
/// Make a queue byte limit. It raises `InvalidQueueByteLimit` when `bytes` is
/// less than 1.
pub fn QueueByteLimit::bytes(bytes : Int64) -> QueueByteLimit raise SettingsError {
  guard bytes >= 1L else { raise InvalidQueueByteLimit(bytes~) }
  { bytes, }
}

///|
/// The default limit: 8 MiB.
pub fn QueueByteLimit::default() -> QueueByteLimit {
  { bytes: 8L * 1024L * 1024L }
}

///|
/// The limit in bytes.
pub fn QueueByteLimit::to_int64(self : QueueByteLimit) -> Int64 {
  self.bytes
}

///|
/// The largest count of encoded bytes of one record, for all its destinations
/// together.
pub struct RecordByteLimit {
  priv bytes : Int64
} derive(Eq, Debug)

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

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

///|
/// Make a record byte limit. It raises `InvalidRecordByteLimit` when `bytes` is
/// less than 1.
pub fn RecordByteLimit::bytes(bytes : Int64) -> RecordByteLimit raise SettingsError {
  guard bytes >= 1L else { raise InvalidRecordByteLimit(bytes~) }
  { bytes, }
}

///|
/// The default limit: 1 MiB.
pub fn RecordByteLimit::default() -> RecordByteLimit {
  { bytes: 1024L * 1024L }
}

///|
/// The limit in bytes.
pub fn RecordByteLimit::to_int64(self : RecordByteLimit) -> Int64 {
  self.bytes
}

///|
/// An absolute directory for log files. It holds an already parsed
/// `AbsolutePath`, so it does not parse the path again; it adds only the rules of
/// file I/O.
pub struct LogDirectory {
  priv path : @lander_core.AbsolutePath
} derive(Eq, Debug)

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

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

///|
/// Make a log directory from an absolute path. It raises `InvalidLogDirectory`
/// when the path has a NUL character. Parse relative text against an explicit
/// base at the application boundary first.
pub fn LogDirectory::from_absolute(
  path : @lander_core.AbsolutePath,
) -> LogDirectory raise SettingsError {
  let text = path.to_string()
  guard !text.contains_char('\u{0}') else {
    raise InvalidLogDirectory(path=text, rule=NulCharacter)
  }
  { path, }
}

///|
/// The absolute path of the directory.
pub fn LogDirectory::path(self : LogDirectory) -> @lander_core.AbsolutePath {
  self.path
}

///|
/// The path of the owned log file `name` in the directory, with a `/`
/// separator.
pub fn LogDirectory::file(
  self : LogDirectory,
  name : @rolling.LogFileName,
) -> String {
  self.join(name.to_string())
}

///|
/// The path of the ownership marker of `files` in the directory.
pub fn LogDirectory::marker(
  self : LogDirectory,
  files : @rolling.FileSet,
) -> String {
  self.join(files.marker_name())
}

///|
/// The path of the entry `name` in the directory. Only the typed paths above
/// call it.
fn LogDirectory::join(self : LogDirectory, name : String) -> String {
  let text = self.path.to_string()
  if text.has_suffix("/") {
    text + name
  } else {
    text + "/" + name
  }
}

///|
/// The directory as text.
pub impl Show for LogDirectory with output(self, logger) {
  logger.write_string(self.path.to_string())
}

///|
pub extend LogDirectory with Show::{output, to_string}

///|
/// The process stream of a console destination.
pub(all) enum ConsoleStream {
  /// Standard error, the default, so the program output on standard output
  /// stays free of log records.
  StandardError
  /// Standard output, by explicit selection.
  StandardOutput
} derive(Eq, Debug)

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

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

///|
/// The stream name: `stderr` or `stdout`.
pub impl Show for ConsoleStream with output(self, logger) {
  logger.write_string(
    match self {
      StandardError => "stderr"
      StandardOutput => "stdout"
    },
  )
}

///|
pub extend ConsoleStream with Show::{output, to_string}

///|
/// When the console writes ANSI styles.
pub(all) enum ColorMode {
  /// Write styles only when the stream is a color terminal, is not redirected,
  /// `TERM` is not `dumb` and `NO_COLOR` is empty or not set.
  Auto
  /// Always write styles, by explicit request.
  Always
  /// Never write styles.
  Never
} derive(Eq, Debug)

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

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

///|
/// What the runtime knows about the terminal of a console stream.
pub(all) enum TerminalCapability {
  /// The stream is a terminal that shows colors.
  ColorTerminal
  /// The stream is a terminal without colors.
  PlainTerminal
  /// The stream goes to a file or a pipe.
  Redirected
  /// The runtime cannot tell. Auto writes no styles.
  Unknown
} derive(Eq, Debug)

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

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

///|
/// The environment hints for `Auto`, read once at the boundary.
pub struct ColorHints {
  priv no_color : Bool
  priv dumb : Bool
} derive(Eq, Debug)

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

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

///|
/// The hints from the values of `NO_COLOR` and `TERM`. A nonempty `NO_COLOR`
/// turns off Auto colors. `TERM=dumb` turns them off too.
pub fn ColorHints::from_env(no_color~ : String?, term~ : String?) -> ColorHints {
  {
    no_color: no_color is Some(text) && !text.is_empty(),
    dumb: term is Some("dumb"),
  }
}

///|
/// No hint against colors.
pub fn ColorHints::none() -> ColorHints {
  { no_color: false, dumb: false }
}

///|
/// Whether the console writes styles for this mode, terminal and hints.
pub fn ColorMode::styles(
  self : ColorMode,
  capability : TerminalCapability,
  hints : ColorHints,
) -> Bool {
  match self {
    Always => true
    Never => false
    Auto => capability is ColorTerminal && !hints.no_color && !hints.dumb
  }
}

///|
/// A console destination: a stream, a minimum level, a color mode and a theme.
pub struct ConsoleSink {
  priv stream : ConsoleStream
  priv minimum : @logging.Level
  priv color : ColorMode
  priv theme : @formatting.Theme
} derive(Eq, Debug)

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

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

///|
/// A console destination. The defaults are standard error, `Information`, `Auto`
/// colors and the `Literate` theme.
pub fn ConsoleSink::new(
  stream? : ConsoleStream = StandardError,
  minimum? : @logging.Level = Information,
  color? : ColorMode = Auto,
  theme? : @formatting.Theme = Literate,
) -> ConsoleSink {
  { stream, minimum, color, theme }
}

///|
/// The stream of the destination.
pub fn ConsoleSink::stream(self : ConsoleSink) -> ConsoleStream {
  self.stream
}

///|
/// The lowest level that the destination writes.
pub fn ConsoleSink::minimum(self : ConsoleSink) -> @logging.Level {
  self.minimum
}

///|
/// The color mode of the destination.
pub fn ConsoleSink::color(self : ConsoleSink) -> ColorMode {
  self.color
}

///|
/// The theme that the destination uses when it writes styles.
pub fn ConsoleSink::theme(self : ConsoleSink) -> @formatting.Theme {
  self.theme
}

///|
/// The theme to render with: the destination theme when the mode, terminal and
/// hints permit styles, else `Plain`.
pub fn ConsoleSink::render_theme(
  self : ConsoleSink,
  capability : TerminalCapability,
  hints : ColorHints,
) -> @formatting.Theme {
  if self.color.styles(capability, hints) {
    self.theme
  } else {
    Plain
  }
}

///|
/// A file destination: one rolling file set in one directory.
pub struct FileSink {
  priv directory : LogDirectory
  priv files : @rolling.FileSet
  priv retention : @rolling.Retention
  priv minimum : @logging.Level
}

///|
/// A file destination. The defaults are the `Text` format, daily and 10 MiB
/// rolling, the newest 31 files including the active file, and `Information`.
pub fn FileSink::new(
  directory : LogDirectory,
  stem : @rolling.LogFileStem,
  format? : @rolling.FileFormat = Text,
  policy? : @rolling.RollingPolicy = @rolling.RollingPolicy::default(),
  retention? : @rolling.Retention = @rolling.Retention::default(),
  minimum? : @logging.Level = Information,
) -> FileSink {
  {
    directory,
    files: @rolling.FileSet::new(stem, format, policy),
    retention,
    minimum,
  }
}

///|
/// The directory of the file set.
pub fn FileSink::directory(self : FileSink) -> LogDirectory {
  self.directory
}

///|
/// The file set: stem, format and rolling policy.
pub fn FileSink::files(self : FileSink) -> @rolling.FileSet {
  self.files
}

///|
/// The retention of old files.
pub fn FileSink::retention(self : FileSink) -> @rolling.Retention {
  self.retention
}

///|
/// The lowest level that the destination writes.
pub fn FileSink::minimum(self : FileSink) -> @logging.Level {
  self.minimum
}

///|
/// The path of the ownership marker of the file set.
pub fn FileSink::marker_path(self : FileSink) -> String {
  self.directory.marker(self.files)
}

///|
/// The target of the file set in failures and reports.
pub fn FileSink::target(self : FileSink) -> SinkTarget {
  Files(self.directory, self.files)
}

///|
/// The settings of one logging session: its destinations and its limits.
pub struct SessionSettings {
  priv console : ConsoleSink?
  priv files : Array[FileSink]
  priv event_limit : QueueEventLimit
  priv byte_limit : QueueByteLimit
  priv record_limit : RecordByteLimit
  priv drain_limit : @lander_core.TimeLimit
}

///|
/// The default final drain limit: 5 seconds.
pub fn default_drain_limit() -> @lander_core.TimeLimit {
  @lander_core.TimeLimit::seconds(5) catch {
    _ => abort("5 seconds is a valid time limit")
  }
}

///|
/// Make session settings. The defaults are no console, no files, 1024
/// outstanding records, 8 MiB outstanding payload, 1 MiB per record and a
/// 5-second drain. It raises `RecordLimitAboveQueueLimit` when one record could
/// never enter the queue, and `DuplicateFileSet` when two file destinations use
/// one file set.
pub fn SessionSettings::new(
  console? : ConsoleSink,
  files? : Array[FileSink] = [],
  event_limit? : QueueEventLimit = QueueEventLimit::default(),
  byte_limit? : QueueByteLimit = QueueByteLimit::default(),
  record_limit? : RecordByteLimit = RecordByteLimit::default(),
  drain_limit? : @lander_core.TimeLimit = default_drain_limit(),
) -> SessionSettings raise SettingsError {
  guard record_limit.bytes <= byte_limit.bytes else {
    raise RecordLimitAboveQueueLimit(
      record=record_limit.bytes,
      queue=byte_limit.bytes,
    )
  }
  for i, file in files {
    for j in 0.. SessionSettings {
  {
    console: Some(ConsoleSink::new()),
    files: [],
    event_limit: QueueEventLimit::default(),
    byte_limit: QueueByteLimit::default(),
    record_limit: RecordByteLimit::default(),
    drain_limit: default_drain_limit(),
  }
}

///|
/// The console destination, if any.
pub fn SessionSettings::console(self : SessionSettings) -> ConsoleSink? {
  self.console
}

///|
/// The file destinations, in order. The array is a copy.
pub fn SessionSettings::files(self : SessionSettings) -> Array[FileSink] {
  self.files.copy()
}

///|
/// The outstanding record limit.
pub fn SessionSettings::event_limit(self : SessionSettings) -> QueueEventLimit {
  self.event_limit
}

///|
/// The outstanding payload limit.
pub fn SessionSettings::byte_limit(self : SessionSettings) -> QueueByteLimit {
  self.byte_limit
}

///|
/// The limit of one record.
pub fn SessionSettings::record_limit(self : SessionSettings) -> RecordByteLimit {
  self.record_limit
}

///|
/// The time limit of a final drain.
pub fn SessionSettings::drain_limit(
  self : SessionSettings,
) -> @lander_core.TimeLimit {
  self.drain_limit
}