///|
pub struct Url {
  path : Array[String]
  query : String
  hash : String
}

///|
fn parse_path(pathname : String) -> Array[String] {
  pathname
  .split("/")
  .map(fn(s) { s.to_string() })
  .filter(fn(s) { s.length() > 0 })
  .collect()
}

///|
fn url_from_location() -> Url {
  let loc = @webapi.window().location()
  { path: parse_path(loc.pathname()), query: loc.search(), hash: loc.hash() }
}

///|
fn url_from_hash() -> Url {
  let raw = @webapi.window().location().hash()
  let s = match raw.strip_prefix("#") {
    Some(v) => v.to_string()
    None => raw
  }
  let (path_part, query) = match s.find("?") {
    Some(qmark) =>
      (
        s.view(end_offset=qmark).to_string(),
        s.view(start_offset=qmark).to_string(),
      )
    None => (s, "")
  }
  { path: parse_path(path_part), query, hash: "" }
}

///|
/// Get the current URL from the browser location (for pushState-based routing).
pub fn url() -> Url {
  url_from_location()
}

///|
/// Get the current hash fragment parsed as a URL (for hash-based routing).
pub fn hash_url() -> Url {
  url_from_hash()
}

///|
fn dispatch_popstate() -> Unit {
  @webapi.window().dispatch_event(@webapi.PopStateEvent::new("popstate"))
  |> ignore
}

///|
/// Navigate to a new URL using pushState. Triggers `on_url_change` subscribers.
pub fn[Msg] Cmd::push_url(url : String) -> Cmd[Msg] {
  Cmd::task(fn(_dispatch) {
    @webapi.window().history().push_state(@webapi.JsValue::null(), "", url~)
    dispatch_popstate()
  })
}

///|
/// Replace the current URL using replaceState. Triggers `on_url_change` subscribers.
pub fn[Msg] Cmd::replace_url(url : String) -> Cmd[Msg] {
  Cmd::task(fn(_dispatch) {
    @webapi.window().history().replace_state(@webapi.JsValue::null(), "", url~)
    dispatch_popstate()
  })
}

///|
/// Set the location hash. Triggers `on_hash_change` subscribers natively.
pub fn[Msg] Cmd::push_hash(hash : String) -> Cmd[Msg] {
  Cmd::task(fn(_dispatch) { @webapi.window().location().set_hash(hash) })
}

///|
fn[Msg] on_window_event(
  key : String,
  event : String,
  parse : () -> Url,
  to_msg : (Url) -> Msg,
) -> Sub[Msg] {
  tracked_sub(key, "window:" + event, fn(dispatch) {
    let handler : (@webapi.Event) -> Unit = fn(_e) { dispatch(to_msg(parse())) }
    @webapi.window().add_event_listener(event, handler)
    fn() { @webapi.window().remove_event_listener(event, handler) }
  })
}

///|
/// Subscribe to URL changes (popstate events) for pushState-based routing.
pub fn[Msg] Sub::on_url_change(key : String, to_msg : (Url) -> Msg) -> Sub[Msg] {
  on_window_event(key, "popstate", fn() { url_from_location() }, to_msg)
}

///|
/// Subscribe to hash changes for hash-based routing.
pub fn[Msg] Sub::on_hash_change(
  key : String,
  to_msg : (Url) -> Msg,
) -> Sub[Msg] {
  on_window_event(key, "hashchange", fn() { url_from_hash() }, to_msg)
}

///|
fn is_plain_primary_click(e : @webapi.Event) -> Bool {
  let me : @webapi.MouseEvent = e.unsafe_into()
  me.button() == 0 &&
  not(me.meta_key()) &&
  not(me.ctrl_key()) &&
  not(me.shift_key()) &&
  not(me.alt_key())
}

///|
fn should_intercept_link_click(e : @webapi.Event) -> Bool {
  if e.default_prevented() || not(is_plain_primary_click(e)) {
    return false
  }
  let anchor : @webapi.HTMLAnchorElement = e.current_target().unsafe_into()
  let target = anchor.target()
  target == "" || target == "_self"
}

///|
fn[Msg] a_with_attrs(
  prefix : Array[Attr[Msg]],
  user : Array[Attr[Msg]],
  children : Array[VNode[Msg]],
) -> VNode[Msg] {
  el("a", prefix + user, children)
}

///|
/// Create a link that uses pushState navigation. Intercepts only plain primary
/// clicks in the same tab and dispatches the given message.
pub fn[Msg] link(
  url : String,
  attrs : Array[Attr[Msg]],
  children : Array[VNode[Msg]],
  on_nav~ : Msg,
) -> VNode[Msg] {
  a_with_attrs(
    [
      Attribute("href", url),
      EventMaybe("click", fn(e) {
        if should_intercept_link_click(e) {
          e.prevent_default()
          Some(on_nav)
        } else {
          None
        }
      }),
    ],
    attrs,
    children,
  )
}

///|
/// Create a link for hash-based navigation. Uses a plain ``.
pub fn[Msg] hash_link(
  hash : String,
  attrs : Array[Attr[Msg]],
  children : Array[VNode[Msg]],
) -> VNode[Msg] {
  a_with_attrs([Attribute("href", "#" + hash)], attrs, children)
}