///|
/// Makes the parts of a `Captures` decoder. Each part decodes one capture
/// group. Join parts with `zip` and make a value with `map`:
///
/// ```moonbit nocheck
/// Captures::int().zip(Captures::string()).map((n, s) => Item::new(n, s))
/// ```
pub enum Captures {}

///|
/// A capture group that did not match, with its 1-based index.
priv suberror MissingGroup {
  MissingGroup(Int)
}

///|
/// A decoder of capture group values into one value of type `T`.
///
/// `count` is the number of capture groups it decodes. `decode` gets the
/// values of those groups and the 1-based index of the first one.
pub struct Captures1[T] {
  priv count : Int
  priv decode : (ArrayView[String?], Int) -> T raise
}

///|
/// A part that decodes one capture group with `parse`.
fn[T] Captures1::part(parse : (String) -> T raise) -> Captures1[T] {
  {
    count: 1,
    decode: (values, first) => {
      match values[0] {
        Some(text) => parse(text)
        None => raise MissingGroup(first)
      }
    },
  }
}

///|
/// The text of the capture group.
pub fn Captures::string() -> Captures1[String] {
  Captures1::part(text => text)
}

///|
/// The capture group as an `Int`.
pub fn Captures::int() -> Captures1[Int] {
  Captures1::part(text => @string.parse_int(text))
}

///|
/// The capture group as an `Int64`.
pub fn Captures::long() -> Captures1[Int64] {
  Captures1::part(text => @string.parse_int64(text))
}

///|
/// The capture group as a `Double`.
pub fn Captures::float() -> Captures1[Double] {
  Captures1::part(text => @string.parse_double(text))
}

///|
/// The capture group as a `Double`.
pub fn Captures::double() -> Captures1[Double] {
  Captures1::part(text => @string.parse_double(text))
}

///|
/// The capture group, converted by `f`.
pub fn[T] Captures::custom(f : (String) -> T raise) -> Captures1[T] {
  Captures1::part(f)
}

///|
/// `None` when none of the groups of `part` matched. Otherwise `part`
/// decodes as usual, so a group that did not match raises
/// `GroupDidNotMatch`.
pub fn[T] Captures::optional(part : Captures1[T]) -> Captures1[T?] {
  {
    count: part.count,
    decode: (values, first) => {
      if values.iter().all(v => v is None) {
        None
      } else {
        Some((part.decode)(values, first))
      }
    },
  }
}

///|
/// Change the value with `f`. The result decodes the same capture groups.
pub fn[A, T] Captures1::map(
  self : Captures1[A],
  f : (A) -> T raise,
) -> Captures1[T] {
  {
    count: self.count,
    decode: (values, first) => f((self.decode)(values, first)),
  }
}

///|
/// Decode the values of the capture groups of the parameter type `name`.
/// Raises `WrongGroupCount` when the number of values is not `count`.
fn[T] Captures1::run(
  self : Captures1[T],
  name : String,
  values : Array[String?],
) -> T raise {
  if values.length() != self.count {
    raise WrongGroupCount(name~, expected=self.count, actual=values.length())
  }
  (self.decode)(values[:], 1) catch {
    MissingGroup(index) => raise GroupDidNotMatch(name~, index~)
    e => raise e
  }
}