// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// A route's own way of deciding which paths are its, in place of its template. The
// template stays what the route is called: the OpenAPI document and `url_for` read
// it, and neither has a way to say a regular expression.

///|
/// A regular expression or a glob, tried on what is left of the path once any
/// prefix the route was included under has been taken off.
pub struct Matcher {
  prefix : String
  source : String
  compiled : @moonregex.Pattern
}

///|
pub impl Eq for Matcher with fn equal(self, other) {
  self.prefix == other.prefix && self.source == other.source
}

///|
pub impl Debug for Matcher with fn to_repr(self) {
  Debug::to_repr(self.prefix + self.source)
}

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

///|
pub extend Matcher with Debug::{to_repr}

///|
fn matcher_of(route : String, source : String) -> Matcher {
  let compiled = @moonregex.compile(source) catch {
    e => abort("route path \{route}: `\{source}` does not compile: \{e}")
  }
  { prefix: "", source, compiled, }
}

///|
/// The named groups `path` binds, or `None` when it does not match. The whole
/// path has to match. A runaway expression meets moonregex's step limit, and a
/// request that makes one run away is a request this route does not answer.
fn Matcher::bind(self : Matcher, path : StringView) -> Map[String, String]? {
  guard path.has_prefix(self.prefix) else { return None }
  let rest = path.view(start_offset=self.prefix.length()).to_owned()
  let found = self.compiled.full(rest) catch { _ => return None }
  found.map(m => m.groupdict(default=Some("")))
}

///|
/// A glob as a regular expression. `*` is any run within a segment, `?` one
/// character of a segment, `**` any run across segments, `**/` any number of whole
/// segments, `[a-z]` and `[!a-z]` a class, and `{a,b}` either. Each `*` and `**`
/// binds the next name in `names`, in order, and is simply matched past the end of
/// them.
fn glob_regex(route : String, glob : String, names : Array[String]) -> String {
  let chars = glob.to_array()
  let out = StringBuilder()
  let mut bound = 0
  let capture = (body : String) => {
    if bound < names.length() {
      bound = bound + 1
      "(?P<\{names[bound - 1]}>\{body})"
    } else {
      "(?:\{body})"
    }
  }
  let plain = (c : Char) => out.write_string(@moonregex.escape(c.to_string()))
  let mut depth = 0
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    let next = if i + 1 < chars.length() { Some(chars[i + 1]) } else { None }
    match c {
      '*' if next is Some('*') => {
        let across = i + 2 < chars.length() && chars[i + 2] == '/'
        out.write_string(capture(if across { "(?:.*/)?" } else { ".*" }))
        i = i + (if across { 3 } else { 2 })
      }
      '*' => {
        out.write_string(capture("[^/]*"))
        i = i + 1
      }
      '?' => {
        out.write_string("[^/]")
        i = i + 1
      }
      '[' => {
        let mut close = i + 1
        while close < chars.length() && chars[close] != ']' {
          close = close + 1
        }
        if close == chars.length() {
          abort("route path \{route}: `[` in `\{glob}` is never closed")
        }
        out.write_char('[')
        for k in (i + 1).. {
        depth = depth + 1
        out.write_string("(?:")
        i = i + 1
      }
      '}' if depth > 0 => {
        depth = depth - 1
        out.write_char(')')
        i = i + 1
      }
      ',' if depth > 0 => {
        out.write_char('|')
        i = i + 1
      }
      _ => {
        plain(c)
        i = i + 1
      }
    }
  }
  if depth > 0 {
    abort("route path \{route}: `{` in `\{glob}` is never closed")
  }
  out.to_string()
}

///|
/// `path` with a regular expression deciding what it answers. The expression has
/// to match the whole path, and its named groups are the path's parameters.
/// `path` is still what the OpenAPI document and `url_for` show.
pub fn Pattern::regex(path : String, regex : String) -> Pattern {
  { ..Pattern::parse(path), matcher: Some(matcher_of(path, regex)), }
}

///|
/// `path` with a glob deciding what it answers, the whole path as with
/// `Pattern::regex`. Each `*` and `**` binds the next parameter `path` names.
pub fn Pattern::glob(path : String, glob : String) -> Pattern {
  let parsed = Pattern::parse(path)
  let names = parsed.params().map(p => p.0)
  { ..parsed, matcher: Some(matcher_of(path, glob_regex(path, glob, names))), }
}

///|
/// The pattern a registration asks for. Both at once is a mistake in the program,
/// so it aborts at registration, as `Pattern::parse` does.
pub fn Pattern::of(path : String, regex? : String, glob? : String) -> Pattern {
  match (regex, glob) {
    (Some(_), Some(_)) =>
      abort("route path \{path}: give `regex` or `glob`, not both")
    (Some(r), None) => Pattern::regex(path, r)
    (None, Some(g)) => Pattern::glob(path, g)
    (None, None) => Pattern::parse(path)
  }
}

///|
/// This pattern on a route moved to `path` by being included under `prefix`.
pub fn Pattern::moved(
  self : Pattern,
  path : String,
  prefix : String,
) -> Pattern {
  let parsed = Pattern::parse(path)
  guard self.matcher is Some(m) else { return parsed }
  let head = join_prefix(prefix, "").trim_end(chars="/").to_owned()
  { ..parsed, matcher: Some({ ..m, prefix: head + m.prefix, }), }
}