///|
pub struct Query {
  name : String
  value : String
}

///|
pub struct Route {
  path : String
  query : Array[Query]
}

///|
fn valid_route_char(char : Char) -> Bool {
  (char >= 'a' && char <= 'z') ||
  (char >= 'A' && char <= 'Z') ||
  (char >= '0' && char <= '9') ||
  char == '_' ||
  char == '-'
}

///|
pub fn route(path : String) -> Route {
  guard path != "" && !path.has_prefix("/") && !path.has_suffix("/") else {
    abort("MiniApp route must be a non-empty relative path: " + path)
  }
  guard !path.contains("//") && !path.contains("?") && !path.contains("#") else {
    abort(
      "MiniApp route must not contain empty segments, query, or fragment: " +
      path,
    )
  }
  for segment in path.split("/") {
    guard segment != "." && segment != ".." && segment != "" else {
      abort("MiniApp route contains an unsafe segment: " + path)
    }
    for char in segment {
      guard valid_route_char(char) else {
        abort("MiniApp route contains an unsupported character: " + path)
      }
    }
  }
  { path, query: [], }
}

///|
pub fn query(name : String, value : String) -> Query {
  guard name != "" else { abort("MiniApp query name must not be empty") }
  { name, value, }
}

///|
pub fn Route::path(self : Route) -> String {
  self.path
}

///|
pub fn Route::has_query(self : Route) -> Bool {
  self.query.length() > 0
}

///|
pub fn Route::with_query(self : Route, query : Array[Query]) -> Route {
  { ..self, query, }
}

///|
fn percent_encode(value : String) -> String {
  let parts : Array[String] = []
  let hex = "0123456789ABCDEF"
  for byte in @utf8.encode(value) {
    let code = byte.to_int()
    let unreserved = (code >= 65 && code <= 90) ||
      (code >= 97 && code <= 122) ||
      (code >= 48 && code <= 57) ||
      code == 45 ||
      code == 46 ||
      code == 95 ||
      code == 126
    if unreserved {
      parts.push(String::make(1, byte.to_char()))
    } else {
      parts.push(
        "%" +
        String::make(1, hex[code / 16].to_int().unsafe_to_char()) +
        String::make(1, hex[code % 16].to_int().unsafe_to_char()),
      )
    }
  }
  parts.join("")
}

///|
pub fn Route::url(self : Route) -> String {
  let base = "/" + self.path
  if self.query.length() == 0 {
    base
  } else {
    base +
    "?" +
    self.query
    .map(item => percent_encode(item.name) + "=" + percent_encode(item.value))
    .join("&")
  }
}

///|
pub(all) enum NavigatorMode {
  Navigate
  Redirect
} derive(Eq)

///|
pub fn[C : IsChildren] navigator(
  target~ : Route,
  mode? : NavigatorMode = Navigate,
  id? : String = "",
  class? : String = "",
  style? : String = "",
  children : C,
) -> Node {
  let children = children.to_nodes()
  let attrs = raw_element_attrs(style, "")
  attrs.push(@ui.attr("url", target.url()))
  attrs.push(
    @ui.attr(
      "open-type",
      if mode == Navigate {
        "navigate"
      } else {
        "redirect"
      },
    ),
  )
  wrap_node(
    @ui.element("navigator", id~, class~, attrs~, children=child_raw(children)),
    child_bindings(children),
  )
}