// Two ways to write routes besides one call per route: a type that answers the methods of one
// path (← Django's class-based views), and a route table kept apart from the handlers (← Django's
// `urlpatterns`, Starlette's `routes=[...]`).

///|
/// A type answering the methods of one path (← Django's class-based `View`). Implement the
/// methods it answers, each giving back its handler; the rest answer `None`, and a request for
/// one of those gets a `405` whose `Allow` lists what the view does answer.
///
/// ```moonbit nocheck
/// struct Articles { db : Db }
///
/// impl @moonapi.View for Articles with get(self) {
///   Some(ctx => self.db.list(ctx))
/// }
///
/// impl @moonapi.View for Articles with post(self) {
///   Some(ctx => self.db.create(ctx))
/// }
///
/// app.view("/articles", Articles::{ db })
/// ```
pub(open) trait View {
  fn get(Self) -> ApiHandler? = _
  fn head(Self) -> ApiHandler? = _
  fn post(Self) -> ApiHandler? = _
  fn put(Self) -> ApiHandler? = _
  fn patch(Self) -> ApiHandler? = _
  fn delete(Self) -> ApiHandler? = _
  fn options(Self) -> ApiHandler? = _
  fn trace(Self) -> ApiHandler? = _
}

///|
impl View with fn get(_) {
  None
}

///|
impl View with fn head(_) {
  None
}

///|
impl View with fn post(_) {
  None
}

///|
impl View with fn put(_) {
  None
}

///|
impl View with fn patch(_) {
  None
}

///|
impl View with fn delete(_) {
  None
}

///|
impl View with fn options(_) {
  None
}

///|
impl View with fn trace(_) {
  None
}

///|
/// Register the methods `view` answers at `path`, each as its own route sharing the arguments
/// given here. A view that answers no method is a mistake in the program and aborts.
pub fn Router::view(
  self : Router,
  path : String,
  view : &View,
  name~ : String = "",
  tags~ : Array[String] = [],
  security~ : Array[@security.SecurityRequirement] = [],
  dependencies~ : Array[String] = [],
  deprecated~ : Bool = false,
  include_in_schema~ : Bool = true,
  hoops~ : Array[Hoop] = [],
) -> Unit {
  // HEAD before GET: a GET route answers HEAD too, so an explicit one has to
  // be found first.
  let answers : Array[(Method, ApiHandler?)] = [
    (Head, view.head()),
    (Get, view.get()),
    (Post, view.post()),
    (Put, view.put()),
    (Patch, view.patch()),
    (Delete, view.delete()),
    (Options, view.options()),
    (Trace, view.trace()),
  ]
  let mut any = false
  for answer in answers {
    guard answer.1 is Some(handler) else { continue }
    any = true
    self.route(
      answer.0,
      path,
      handler,
      name~,
      tags~,
      security~,
      dependencies~,
      deprecated~,
      include_in_schema~,
      hoops~,
    )
  }
  if !any {
    abort("view at \{path} answers no method")
  }
}

///|
/// One entry of a route table: what it registers, given the router it lands on. `path`, `view`,
/// `websocket` and `group` make them.
pub type Url = (Router) -> Unit

///|
/// Register every entry of `urls`, in order — the order a request tries them in.
pub fn Router::urls(self : Router, urls : Array[Url]) -> Unit {
  for url in urls {
    url(self)
  }
}

///|
/// `handler` at `path` for each of `methods` (← Django's `path`, Starlette's `Route`, whose
/// default is the same `GET`, which answers `HEAD` too).
pub fn path(
  path : String,
  handler : ApiHandler,
  methods~ : Array[Method] = [Get],
  name~ : String = "",
) -> Url {
  router => {
    for verb in methods {
      router.route(verb, path, handler, name~)
    }
  }
}

///|
/// The methods `view` answers at `path`.
pub fn view(path : String, view : &View, name~ : String = "") -> Url {
  router => router.view(path, view, name~)
}

///|
/// A WebSocket route at `path` (← Starlette's `WebSocketRoute`).
pub fn websocket(
  path : String,
  handler : @socket.WsHandler,
  name~ : String = "",
) -> Url {
  router => router.websocket(path, handler, name~)
}

///|
/// `urls` under `prefix` (← Django's `include`, Starlette's `Mount` with routes; `include` is a
/// reserved word). The arguments reach every route of the group as
/// `Router::include_router` describes, and a group may hold groups.
pub fn group(
  prefix : String,
  urls : Array[Url],
  tags~ : Array[String] = [],
  security~ : Array[@security.SecurityRequirement] = [],
  dependencies~ : Array[String] = [],
  hoops~ : Array[Hoop] = [],
) -> Url {
  router => {
    let inner = Router::new()
    inner.urls(urls)
    router.include_router(
      inner,
      prefix~,
      tags~,
      security~,
      dependencies~,
      hoops~,
    )
  }
}