///|
/// What a path parameter accepts (← Starlette's convertors). `Str` is one
/// segment or part of one; `Path` is the rest of the path, slashes included.
pub(all) enum Convertor {
  Str
  Int
  Float
  Uuid
  Path
} derive(Eq, Debug)

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

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

///|
/// The convertor named `name`, as a template spells it after the colon.
pub fn Convertor::of(name : StringView) -> Convertor? {
  match name {
    "str" => Some(Str)
    "int" => Some(Int)
    "float" => Some(Float)
    "uuid" => Some(Uuid)
    "path" => Some(Path)
    _ => None
  }
}

///|
/// One piece of a path template: text matched as written, or a parameter.
pub(all) enum Piece {
  Literal(String)
  Param(name~ : String, convertor~ : Convertor)
} derive(Eq, Debug)

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

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

///|
/// A route path read as a template: `/users/{id:int}`, `/files/{name}.json`,
/// `/static/{rest:path}`. A parameter without a convertor is `str`.
pub(all) struct Pattern {
  source : String
  pieces : Array[Piece]
} derive(Eq, Debug)

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

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

///|
/// Read `path` as a template. An unclosed brace, an empty name, an unknown
/// convertor or two parameters with nothing between them is a mistake in the
/// program, not in a request, so it aborts at registration — the way Starlette
/// refuses such a route when the application starts.
pub fn Pattern::parse(path : String) -> Pattern {
  let pieces : Array[Piece] = []
  let mut from = 0
  let mut i = 0
  while i < path.length() {
    if path.unsafe_get(i) != '{'.to_int().to_uint16() {
      i += 1
      continue
    }
    let close = match path.view(start_offset=i).find("}") {
      Some(k) => i + k
      None => abort("route path \{path}: `{` is never closed")
    }
    let inside = path.view(start_offset=i + 1, end_offset=close)
    let (name, convertor) = match inside.find(":") {
      Some(k) => {
        let conv = inside.view(start_offset=k + 1)
        match Convertor::of(conv) {
          Some(c) => (inside.view(end_offset=k).to_owned(), c)
          None => abort("route path \{path}: no convertor called `\{conv}`")
        }
      }
      None => (inside.to_owned(), Str)
    }
    if name.is_empty() {
      abort("route path \{path}: a parameter has no name")
    }
    if i == from && pieces.last() is Some(Param(..)) {
      abort("route path \{path}: two parameters with nothing between them")
    }
    if i > from {
      pieces.push(
        Literal(path.view(start_offset=from, end_offset=i).to_owned()),
      )
    }
    pieces.push(Param(name~, convertor~))
    i = close + 1
    from = i
  }
  if from < path.length() {
    pieces.push(Literal(path.view(start_offset=from).to_owned()))
  }
  { source: path, pieces, }
}

///|
/// The parameters `path` binds, or `None` when it does not match. The whole
/// path must match: a trailing slash is a different path.
pub fn Pattern::bind(self : Pattern, path : StringView) -> Map[String, String]? {
  let params : Map[String, String] = Map([])
  if bind_from(self.pieces, 0, path, 0, params) {
    Some(params)
  } else {
    None
  }
}

///|
fn bind_from(
  pieces : Array[Piece],
  i : Int32,
  path : StringView,
  at : Int32,
  params : Map[String, String],
) -> Bool {
  if i == pieces.length() {
    return at == path.length()
  }
  match pieces[i] {
    Literal(text) => {
      let rest = path.view(start_offset=at)
      rest.has_prefix(text) &&
      bind_from(pieces, i + 1, path, at + text.length(), params)
    }
    Param(name~, convertor~) => {
      for end in ends(convertor, path, at) {
        params[name] = path.view(start_offset=at, end_offset=end).to_owned()
        if bind_from(pieces, i + 1, path, end, params) {
          return true
        }
      }
      params.remove(name)
      false
    }
  }
}

///|
/// Where a value of `convertor` starting at `at` may end, longest first.
fn ends(convertor : Convertor, path : StringView, at : Int32) -> Array[Int32] {
  let n = path.length()
  let run = fn(ok : (UInt16) -> Bool) {
    let mut k = at
    while k < n && ok(path.unsafe_get(k)) {
      k += 1
    }
    k
  }
  let digit = (c : UInt16) => {
    c >= '0'.to_int().to_uint16() && c <= '9'.to_int().to_uint16()
  }
  let longest_first = fn(last : Int32, min : Int32) {
    let out = []
    let mut k = last
    while k >= at + min {
      out.push(k)
      k -= 1
    }
    out
  }
  match convertor {
    Str => longest_first(run(c => c != '/'.to_int().to_uint16()), 1)
    Path => longest_first(n, 0)
    Int => longest_first(run(digit), 1)
    Float => {
      let whole = run(digit)
      if whole == at {
        return []
      }
      let out = []
      if whole < n &&
        path.unsafe_get(whole) == '.'.to_int().to_uint16() &&
        whole + 1 < n &&
        digit(path.unsafe_get(whole + 1)) {
        let mut k = whole + 1
        while k < n && digit(path.unsafe_get(k)) {
          k += 1
        }
        out.push(k)
      }
      out.push(whole)
      out
    }
    Uuid =>
      if at + 36 <= n && is_uuid(path.view(start_offset=at, end_offset=at + 36)) {
        [at + 36]
      } else {
        []
      }
  }
}

///|
fn is_uuid(s : StringView) -> Bool {
  for i in 0..<36 {
    let c = s.unsafe_get(i).to_int()
    let ok = if i == 8 || i == 13 || i == 18 || i == 23 {
      c == '-'.to_int()
    } else {
      (c >= '0'.to_int() && c <= '9'.to_int()) ||
      (c >= 'a'.to_int() && c <= 'f'.to_int()) ||
      (c >= 'A'.to_int() && c <= 'F'.to_int())
    }
    if !ok {
      return false
    }
  }
  true
}

///|
/// The path with `params` substituted (← Starlette's `url_path_for`); `None`
/// when one is missing or does not fit its convertor.
pub fn Pattern::fill(self : Pattern, params : Map[String, String]) -> String? {
  let out = StringBuilder()
  for piece in self.pieces {
    match piece {
      Literal(text) => out.write_string(text)
      Param(name~, convertor~) => {
        guard params.get(name) is Some(value) else { return None }
        guard ends(convertor, value.view(), 0) is [whole, ..] &&
          whole == value.length() &&
          (convertor is Path || !value.contains("/")) else {
          return None
        }
        out.write_string(value)
      }
    }
  }
  Some(out.to_string())
}

///|
/// The parameters in the order the path names them.
pub fn Pattern::params(self : Pattern) -> Array[(String, Convertor)] {
  let out = []
  for piece in self.pieces {
    if piece is Param(name~, convertor~) {
      out.push((name, convertor))
    }
  }
  out
}

///|
/// The path as an OpenAPI template expression: every parameter as `{name}`,
/// the convertor left to the parameter's schema.
pub fn Pattern::template(self : Pattern) -> String {
  let out = StringBuilder()
  for piece in self.pieces {
    match piece {
      Literal(text) => out.write_string(text)
      Param(name~, ..) => {
        out.write_char('{')
        out.write_string(name)
        out.write_char('}')
      }
    }
  }
  out.to_string()
}

///|
/// A route `path` under `prefix`, written as FastAPI writes it: the prefix with
/// its trailing slashes dropped, then the path as given, so `("/sub", "/items/")`
/// is `/sub/items/` and `("/sub", "/")` is `/sub/`. A path keeps its own trailing
/// slash because a trailing slash is part of what a route matches.
pub fn join_prefix(prefix : String, path : String) -> String {
  let head = prefix.trim_end(chars="/").to_owned()
  let head = if head.is_empty() || head.has_prefix("/") {
    head
  } else {
    "/" + head
  }
  let joined = if path.is_empty() || path.has_prefix("/") {
    head + path
  } else {
    head + "/" + path
  }
  if joined.is_empty() {
    "/"
  } else {
    joined
  }
}