///|
pub(all) struct ParseError {
  field : String
  message : String
} derive(Eq, Debug)

///|
pub fn ParseError::new(field : String, message : String) -> ParseError {
  ParseError::{ field, message }
}

///|
pub fn ParseError::field(self : ParseError) -> String {
  self.field
}

///|
pub fn ParseError::message(self : ParseError) -> String {
  self.message
}

///|
pub(all) enum ParseResult {
  ParseOk(LimitSpec)
  ParseFailed(ParseError)
} derive(Eq, Debug)

///|
pub fn ParseResult::parse_limit(text : String) -> ParseResult {
  let words = split_words(text)
  if words.length() == 0 {
    return ParseFailed(ParseError::new("rule", "rule must not be empty"))
  }
  match words[0] {
    "token_bucket" => parse_token_bucket_words(words)
    "fixed_window" => parse_fixed_window_words(words)
    "sliding_log" => parse_sliding_log_words(words)
    _ =>
      ParseFailed(ParseError::new("kind", "unknown limiter kind: \{words[0]}"))
  }
}

///|
pub fn ParseResult::is_ok(self : ParseResult) -> Bool {
  match self {
    ParseOk(_) => true
    ParseFailed(_) => false
  }
}

///|
pub fn ParseResult::spec(self : ParseResult) -> LimitSpec {
  match self {
    ParseOk(spec) => spec
    ParseFailed(_) => LimitSpec::token_bucket("invalid", 1, 1, 1000)
  }
}

///|
pub fn ParseResult::error_field(self : ParseResult) -> String {
  match self {
    ParseOk(_) => ""
    ParseFailed(error) => error.field()
  }
}

///|
pub fn ParseResult::error_message(self : ParseResult) -> String {
  match self {
    ParseOk(_) => ""
    ParseFailed(error) => error.message()
  }
}

///|
fn parse_token_bucket_words(words : Array[String]) -> ParseResult {
  if words.length() == 4 {
    match parse_positive_field(words[2], "capacity") {
      ParseIntOk(capacity) =>
        match parse_rate_field(words[3], "refill") {
          ParseRateOk(rate) =>
            ParseOk(
              LimitSpec::token_bucket(
                words[1],
                capacity,
                rate.events(),
                rate.period_ms(),
              ),
            )
          ParseRateFailed(error) => ParseFailed(error)
        }
      ParseIntFailed(error) => ParseFailed(error)
    }
  } else if words.length() >= 5 {
    match parse_positive_field(words[2], "capacity") {
      ParseIntOk(capacity) =>
        match parse_positive_field(words[3], "refill_tokens") {
          ParseIntOk(refill_tokens) =>
            match parse_duration_field(words[4], "refill_period_ms") {
              ParseDurationOk(period) =>
                ParseOk(
                  LimitSpec::token_bucket(
                    words[1],
                    capacity,
                    refill_tokens,
                    period.millis(),
                  ),
                )
              ParseDurationFailed(error) => ParseFailed(error)
            }
          ParseIntFailed(error) => ParseFailed(error)
        }
      ParseIntFailed(error) => ParseFailed(error)
    }
  } else {
    ParseFailed(
      ParseError::new(
        "token_bucket", "token_bucket requires: name capacity refill_tokens refill_period_ms",
      ),
    )
  }
}

///|
fn parse_fixed_window_words(words : Array[String]) -> ParseResult {
  if words.length() < 4 {
    ParseFailed(
      ParseError::new(
        "fixed_window", "fixed_window requires: name limit window",
      ),
    )
  } else {
    match parse_positive_field(words[2], "limit") {
      ParseIntOk(limit) =>
        match parse_duration_field(words[3], "window") {
          ParseDurationOk(window) =>
            ParseOk(LimitSpec::fixed_window(words[1], limit, window.millis()))
          ParseDurationFailed(error) => ParseFailed(error)
        }
      ParseIntFailed(error) => ParseFailed(error)
    }
  }
}

///|
fn parse_sliding_log_words(words : Array[String]) -> ParseResult {
  if words.length() < 4 {
    ParseFailed(
      ParseError::new("sliding_log", "sliding_log requires: name limit window"),
    )
  } else {
    match parse_positive_field(words[2], "limit") {
      ParseIntOk(limit) =>
        match parse_duration_field(words[3], "window") {
          ParseDurationOk(window) =>
            ParseOk(LimitSpec::sliding_log(words[1], limit, window.millis()))
          ParseDurationFailed(error) => ParseFailed(error)
        }
      ParseIntFailed(error) => ParseFailed(error)
    }
  }
}

///|
priv enum ParseIntResult {
  ParseIntOk(Int)
  ParseIntFailed(ParseError)
}

///|
priv enum ParseDurationResult {
  ParseDurationOk(Duration)
  ParseDurationFailed(ParseError)
}

///|
priv enum ParseRateResult {
  ParseRateOk(Rate)
  ParseRateFailed(ParseError)
}

///|
fn parse_positive_field(text : String, field : String) -> ParseIntResult {
  let value = parse_required_positive_prefix(text)
  if value <= 0 {
    ParseIntFailed(
      ParseError::new(field, "\{field} must be a positive integer"),
    )
  } else {
    ParseIntOk(value)
  }
}

///|
fn parse_duration_field(text : String, field : String) -> ParseDurationResult {
  let value = parse_required_positive_prefix(text)
  if value <= 0 {
    ParseDurationFailed(
      ParseError::new(field, "\{field} must be a positive duration"),
    )
  } else if !is_duration_suffix(suffix_after_digit_prefix(text)) {
    ParseDurationFailed(
      ParseError::new(field, "\{field} supports duration suffixes: ms, s, m, h"),
    )
  } else {
    ParseDurationOk(Duration::parse(text))
  }
}

///|
fn parse_rate_field(text : String, field : String) -> ParseRateResult {
  let value = parse_required_positive_prefix(text)
  if value <= 0 {
    ParseRateFailed(ParseError::new(field, "\{field} must be a positive rate"))
  } else if !is_rate_suffix(suffix_after_digit_prefix(text)) {
    ParseRateFailed(
      ParseError::new(
        field,
        "\{field} supports rate suffixes: /ms, /s, /min, /h",
      ),
    )
  } else {
    ParseRateOk(Rate::parse(text))
  }
}

///|
fn parse_required_positive_prefix(text : String) -> Int {
  let mut value = 0
  let mut seen = false
  for i in 0.. String {
  let mut index = 0
  for i in 0.. Bool {
  suffix == "" ||
  suffix == "ms" ||
  suffix == "s" ||
  suffix == "m" ||
  suffix == "h"
}

///|
fn is_rate_suffix(suffix : String) -> Bool {
  suffix == "" ||
  suffix == "/ms" ||
  suffix == "/s" ||
  suffix == "/min" ||
  suffix == "/h"
}

///|
fn is_ascii_digit(ch : UInt16) -> Bool {
  ch.to_int() >= '0'.to_int() && ch.to_int() <= '9'.to_int()
}