///|
/// The rule of a time limit that an input breaks.
pub enum TimeLimitRule {
  /// The text is empty.
  Empty
  /// A space is at the start or the end, two spaces are next to each other, or
  /// the text holds whitespace that is not a space, such as a tab or a newline.
  UnexpectedSpace
  /// A part does not start with a number such as `10` or `1.5`.
  NotANumber
  /// A number has no unit after it.
  MissingUnit
  /// The unit is not one of the known units.
  UnknownUnit(String)
  /// A part has a larger unit than the part before it.
  UnitsOutOfOrder
  /// Two parts have the same unit.
  RepeatedUnit
  /// A part is not a whole number of milliseconds.
  NotWholeMilliseconds
  /// The value is zero or less.
  NotPositive
  /// The value is more than the largest time limit.
  AboveLargest
} derive(Eq, Debug)

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

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

///|
/// An input that is not a valid time limit. Callers can match the cases, but
/// only the constructors of `TimeLimit` make them.
pub suberror TimeLimitError {
  /// The input `input` breaks the rule `rule`. For a constructor that takes a
  /// count, `input` is the count with its unit, for example `-1ms`.
  InvalidTimeLimit(input~ : String, rule~ : TimeLimitRule)
} derive(Debug)

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

///|
/// Text for people that describes the fault.
pub fn TimeLimitError::message(self : TimeLimitError) -> String {
  let InvalidTimeLimit(input~, rule~) = self
  let reason = match rule {
    Empty => "the text is empty"
    UnexpectedSpace =>
      "use at most one space, and only between a number and its unit or between parts"
    NotANumber => "each part must start with a number such as 10 or 1.5"
    MissingUnit => "each number needs a unit, such as ms, s, m or h"
    UnknownUnit(unit) =>
      "the unit \{unit.escape()} is not known; use ms, s, m or h, or millisecond(s), second(s), minute(s) or hour(s)"
    UnitsOutOfOrder => "the parts must go from the largest unit to the smallest"
    RepeatedUnit => "each unit can occur only one time"
    NotWholeMilliseconds => "each part must be a whole number of milliseconds"
    NotPositive => "the value must be greater than zero"
    AboveLargest =>
      "the value must not be more than \{MAX_MILLIS} milliseconds (\{LARGEST_TEXT})"
  }
  "Invalid time limit \{input.escape()}: \{reason}"
}

///|
/// A positive amount of time, from 1 millisecond to 2 147 483 647 milliseconds
/// (the largest `Int`, about 24.8 days). This is the range of the timeouts of
/// `moonbitlang/async`, so `to_millis` can give the value to it.
pub struct TimeLimit {
  priv millis : Int
} derive(Eq, Compare, Hash, Debug)

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

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

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

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

///|
/// The largest time limit in milliseconds.
const MAX_MILLIS : Int64 = 2147483647L

///|
/// The text form of the largest time limit.
const LARGEST_TEXT : String = "596h31m23s647ms"

///|
/// Make a time limit of `count` milliseconds.
pub fn TimeLimit::millis(count : Int) -> TimeLimit raise TimeLimitError {
  from_total("\{count}ms", count.to_int64())
}

///|
/// Make a time limit of `count` seconds.
pub fn TimeLimit::seconds(count : Int) -> TimeLimit raise TimeLimitError {
  from_total("\{count}s", count.to_int64() * 1000L)
}

///|
/// The time limit in milliseconds, from 1 to the largest `Int`.
pub fn TimeLimit::to_millis(self : TimeLimit) -> Int {
  self.millis
}

///|
fn from_total(input : String, total : Int64) -> TimeLimit raise TimeLimitError {
  guard total > 0L else { raise InvalidTimeLimit(input~, rule=NotPositive) }
  guard total <= MAX_MILLIS else {
    raise InvalidTimeLimit(input~, rule=AboveLargest)
  }
  { millis: total.to_int() }
}

///|
/// One part of the text form: the digits before and after the decimal point,
/// and the unit.
priv struct Part {
  whole : String
  fraction : String
  unit : Unit_
}

///|
/// A unit of the text form, from the smallest to the largest.
priv enum Unit_ {
  Millisecond
  Second
  Minute
  Hour
}

///|
fn Unit_::rank(self : Unit_) -> Int {
  match self {
    Millisecond => 0
    Second => 1
    Minute => 2
    Hour => 3
  }
}

///|
fn Unit_::millis(self : Unit_) -> Int64 {
  match self {
    Millisecond => 1L
    Second => 1000L
    Minute => 60_000L
    Hour => 3_600_000L
  }
}

///|
fn unit_of(text : String) -> Unit_? {
  match text {
    "ms" | "millisecond" | "milliseconds" => Some(Millisecond)
    "s" | "second" | "seconds" => Some(Second)
    "m" | "minute" | "minutes" => Some(Minute)
    "h" | "hour" | "hours" => Some(Hour)
    _ => None
  }
}

///|
/// Parse a time limit from its text form: one or more parts, each a decimal
/// number, an optional space and a unit, from the largest unit to the smallest.
/// For example `9ms`, `10 seconds`, `1.5s` and `2m30s`. The units are `ms`,
/// `s`, `m` and `h`, and the lower-case words `millisecond`, `second`, `minute`
/// and `hour`, each also with a final `s`. The checks of the shape come first,
/// then the checks of the unit order, then the checks of the value.
pub fn TimeLimit::parse(text : String) -> TimeLimit raise TimeLimitError {
  fn invalid(rule : TimeLimitRule) -> TimeLimitError {
    InvalidTimeLimit(input=text, rule~)
  }

  let chars = Array::from_iter(text.iter())
  guard chars.length() > 0 else { raise invalid(Empty) }
  guard chars[0] != ' ' &&
    chars[chars.length() - 1] != ' ' &&
    !chars.iter().any(char => char != ' ' && char.is_whitespace()) else {
    raise invalid(UnexpectedSpace)
  }
  // The shape: read each part.
  let parts : Array[Part] = []
  let mut index = 0
  while index < chars.length() {
    let whole = take_while(chars, index, is_digit)
    guard whole.length() > 0 else { raise invalid(NotANumber) }
    index += whole.length()
    let mut fraction = ""
    if index < chars.length() && chars[index] == '.' {
      fraction = take_while(chars, index + 1, is_digit)
      guard fraction.length() > 0 else { raise invalid(NotANumber) }
      index += 1 + fraction.length()
    }
    guard index >= chars.length() || chars[index] != '.' else {
      raise invalid(NotANumber)
    }
    index = skip_one_space(chars, index) catch { _ => raise invalid(UnexpectedSpace) }
    let unit_text = take_while(chars, index, is_unit_char)
    guard unit_text.length() > 0 else { raise invalid(MissingUnit) }
    guard unit_of(unit_text) is Some(unit) else {
      raise invalid(UnknownUnit(unit_text))
    }
    index += unit_text.length()
    parts.push({ whole, fraction, unit })
    index = skip_one_space(chars, index) catch { _ => raise invalid(UnexpectedSpace) }
  }
  // The order: each unit is smaller than the unit before it.
  for i in 1.. before {
      raise invalid(UnitsOutOfOrder)
    }
  }
  // The value. Each part is checked in Int64 after its zeros at the start of
  // the whole digits and at the end of the fraction digits go, so the work is
  // linear in the text and nothing can overflow. A fraction with more than 7
  // digits after that is never a whole number of milliseconds, because the
  // largest unit, 3 600 000 ms, is 2^7 * 3^2 * 5^5. A whole number with more
  // than 10 digits is above the largest limit for every unit.
  let mut total = 0L
  let mut above = false
  for part in parts {
    let whole = trim_char(part.whole, '0', start=true)
    let fraction = trim_char(part.fraction, '0', start=false)
    guard fraction.length() <= 7 else { raise invalid(NotWholeMilliseconds) }
    let unit = part.unit.millis()
    let divisor = power_of_ten(fraction.length())
    let scaled_fraction = digits_value(fraction) * unit
    guard scaled_fraction % divisor == 0L else {
      raise invalid(NotWholeMilliseconds)
    }
    if whole.length() > 10 {
      above = true
    } else {
      total += digits_value(whole) * unit + scaled_fraction / divisor
    }
  }
  guard above || total > 0L else { raise invalid(NotPositive) }
  guard !above && total <= MAX_MILLIS else { raise invalid(AboveLargest) }
  { millis: total.to_int() }
}

///|
/// `text` without the characters `char` at its start or at its end.
fn trim_char(text : String, char : Char, start~ : Bool) -> String {
  let chars = Array::from_iter(text.iter())
  let mut from = 0
  let mut to = chars.length()
  if start {
    while from < to && chars[from] == char {
      from += 1
    }
  } else {
    while to > from && chars[to - 1] == char {
      to -= 1
    }
  }
  String::from_array(chars[from:to].to_owned())
}

///|
/// The value of at most 10 ASCII digits. The empty text is 0.
fn digits_value(digits : String) -> Int64 {
  let mut value = 0L
  for char in digits {
    value = value * 10L + (char.to_int() - '0'.to_int()).to_int64()
  }
  value
}

///|
/// 10 to the power `exponent`, for an exponent from 0 to 7.
fn power_of_ten(exponent : Int) -> Int64 {
  let mut value = 1L
  for _ in 0.. Bool) -> String {
  let builder = StringBuilder()
  let mut index = start
  while index < chars.length() && accept(chars[index]) {
    builder.write_char(chars[index])
    index += 1
  }
  builder.to_string()
}

///|
/// The index after one optional space at `index`. Two spaces raise.
fn skip_one_space(chars : Array[Char], index : Int) -> Int raise {
  guard index < chars.length() && chars[index] == ' ' else { return index }
  guard index + 1 < chars.length() && chars[index + 1] != ' ' else {
    fail("two spaces")
  }
  index + 1
}

///|
fn is_digit(char : Char) -> Bool {
  char >= '0' && char <= '9'
}

///|
/// A character of a unit: anything except an ASCII digit, a space and `.`. So
/// the unit text in an error is the full text that the user wrote, for example
/// `µs` or `séconds`.
fn is_unit_char(char : Char) -> Bool {
  !is_digit(char) && char != ' ' && char != '.'
}

///|
/// The canonical text: the parts from hours to milliseconds with short units
/// and no spaces, without the zero parts, for example `2m30s` or `1s500ms`.
/// `TimeLimit::parse` reads it back to an equal limit.
pub impl Show for TimeLimit with output(self, logger) {
  let mut rest = self.millis
  for unit in [Hour, Minute, Second, Millisecond] {
    let size = unit.millis().to_int()
    let count = rest / size
    rest = rest % size
    if count > 0 {
      logger.write_string(count.to_string())
      logger.write_string(
        match unit {
          Hour => "h"
          Minute => "m"
          Second => "s"
          Millisecond => "ms"
        },
      )
    }
  }
}

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