///|
/// Represent the URL change request from the user, trigger by `@html.a` tag.
/// The `Internal` means the target URL is in the same domain.
/// The `External` means the target URL is in another site.
pub(all) enum UrlRequest {
  Internal(Url)
  External(String)
} derive(Debug, Eq, Compare)

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

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

///|
pub extend UrlRequest with Compare::{compare, op_ge, op_gt, op_le, op_lt}

///|
pub impl Show for UrlRequest with fn output(self, buf) {
  Show::output(@debug.Debug::to_repr(self), buf)
}

///|
pub extend UrlRequest with Show::{output, to_string}

///|
/// Url
/// 
/// ```text
///  https://example.com:8042/over/there?name=ferret#nose
///  \___/   \______________/\_________/ \_________/ \__/
///    |            |            |            |        |
///  scheme     authority       path        query   fragment
/// ```
/// 
/// This diagram is from https://package.elm-lang.org/packages/elm/url/latest/Url
pub(all) struct Url {
  protocol : Protocol
  host : String
  port : Int?
  path : String
  query : String?
  fragment : String?
} derive(Debug, Eq, Compare)

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

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

///|
pub extend Url with Compare::{compare, op_ge, op_gt, op_le, op_lt}

///|
pub impl Show for Url with fn output(self, buf) {
  Show::output(@debug.Debug::to_repr(self), buf)
}

///|
pub extend Url with Show::{output}

///|
pub(all) enum Protocol {
  Http
  Https
  Other(String)
} derive(Debug, Eq, Compare)

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

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

///|
pub extend Protocol with Compare::{compare, op_ge, op_gt, op_le, op_lt}

///|
pub impl Show for Protocol with fn output(self, buf) {
  Show::output(@debug.Debug::to_repr(self), buf)
}

///|
pub extend Protocol with Show::{output, to_string}

///|
pub fn Url::to_string(self : Url) -> String {
  let protocol = match self.protocol {
    Http => "http"
    Https => "https"
    _ => panic() // TODO: fix this
  }
  let port = match self.port {
    Some(p) => ":\{p}"
    None => ""
  }
  let query = match self.query {
    Some(q) => "?\{q}"
    None => ""
  }
  let fragment = match self.fragment {
    Some(f) => "#\{f}"
    None => ""
  }
  "\{protocol}://\{self.host}\{port}/\{self.path}\{query}\{fragment}"
}

///|
pub fn parse(url : String) -> Url raise Error {
  // ⚠️ **先切 `#`,再在它之前那半段切 `?`**(2026-10-07 修)。
  //
  // 老实现反着来(先 `?` 再在"问号之前"找 `#`),于是 `#/s/baz?s=sec-2` 被拆成
  // "query 在外、fragment 在内" —— **与真实地址不符**,`to_string()` 还回去是另一条 URL。
  // 真浏览器实测(`examples/apps/route-spike/`):
  //   地址栏 `http://h/#/s/baz?s=sec-2&v=raw` ⇒ 老实现读出 `?s=sec-2&v=raw` + `#/s/baz`。
  // RFC 3986 里 **fragment 是最后一段、可以含 `?`**,所以 `#` 必须先切。
  let (protocol, remain) = match url.split("://").collect() {
    ["http", remain] => (Http, remain)
    ["https", remain] => (Https, remain)
    [x, remain] => (Other(x.to_owned()), remain)
    [remain] => (Other(""), remain)
    _ => fail("Invalid protocol")
  }
  // ① 先切 fragment(第一个 `#` 之后**全部**算 fragment,允许里面再有 `#` 与 `?`)
  // `[head, .. rest]` 已经覆盖了"没有 `#`"(那时 rest 为空)⇒ 不需要单独的 `[head]` 分支
  // (实测加了它只会得到一条 `unreachable_code` 警告)。
  let (before_hash, fragment) = match remain.split("#").collect() {
    [head, .. rest] =>
      if rest.is_empty() {
        (head, None)
      } else {
        (head, Some(rest.iter().map(StringView::to_owned).join("#")))
      }
    _ => fail("Invalid fragment")
  }
  // ② 再在 `#` 之前那半段里切 query(没有 `?` ⇒ 没有 query)
  let (mid, query) = match before_hash.split("?").collect() {
    [mid, q] => (mid, if q.is_empty() { None } else { Some(q.to_owned()) })
    [mid] => (mid, None)
    _ => fail("Invalid query")
  }
  let (mid, path) = match mid.split("/").collect() {
    [mid] => (mid, "")
    [mid, .. paths] => (mid, paths.iter().map(StringView::to_owned).join("/"))
    _ => fail("Invalid host")
  }
  let (host, port) = match mid.split(":").collect() {
    [host, port] => {
      let port = try @string.parse_int(port.to_owned()) catch {
        _ => Option::None
      } noraise {
        number => Some(number)
      }
      (host.to_owned(), port)
    }
    [host] => (host.to_owned(), None)
    _ => fail("Invalid host")
  }
  { protocol, host, port, path, query, fragment, }
}