///|
#cfg(target="js")
fn ui_has_global(name : String) -> Bool {
  let value : @js.Value = @js.globalThis.get_with_string(name)
  !value.is_undefined() && !value.is_null()
}

///|
#cfg(target="js")
fn ui_has_window() -> Bool {
  ui_has_global("window")
}

///|
#cfg(target="js")
fn ui_has_document() -> Bool {
  ui_has_global("document")
}

///|
#cfg(target="js")
fn[E] ui_transition_event(event : E) -> @dom.TransitionEvent? {
  @js.Cast::into(@js.Value::cast_from(event))
}

///|
#cfg(target="js")
fn ui_element_by_id(id : String) -> @dom.Element? {
  @dom.document().get_element_by_id(id).to_option()
}

///|
#cfg(target="js")
fn ui_find_descendant(
  root : @dom.Element,
  predicate : (@dom.Element) -> Bool,
) -> @dom.Element? {
  for child in root.get_children() {
    if predicate(child) {
      return Some(child)
    }
    if ui_find_descendant(child, predicate) is Some(found) {
      return Some(found)
    }
  }
  None
}

///|
#cfg(target="js")
fn ui_find_element(
  elements : Array[@dom.Element],
  predicate : (@dom.Element) -> Bool,
) -> @dom.Element? {
  for element in elements {
    if predicate(element) {
      return Some(element)
    }
  }
  None
}

///|
#cfg(target="js")
fn ui_find_descendant_by_attribute(
  root : @dom.Element,
  name : String,
  value : String,
) -> @dom.Element? {
  ui_find_descendant(root, element => {
    element.get_attribute(name).unwrap_or("") == value
  })
}

///|
#cfg(target="js")
fn ui_element_contains_target(
  root : @dom.Element,
  target : @dom.EventTarget,
) -> Bool {
  if target.to_node() is Some(node) {
    root.contains(node)
  } else {
    false
  }
}

///|
#cfg(target="js")
fn ui_focus_element(element : @dom.Element) -> Unit {
  if element.to_html_element() is Some(html_element) {
    html_element.focus()
  }
}

///|
#cfg(target="js")
fn ui_focus_element_without_scroll(element : @dom.Element) -> Unit {
  if element.to_html_element() is Some(html_element) {
    html_element.focus_with_options(prevent_scroll=true)
  }
}

///|
#cfg(target="js")
fn ui_click_element(element : @dom.Element) -> Unit {
  if element.to_html_element() is Some(html_element) {
    html_element.click()
  }
}

///|
#cfg(target="js")
fn ui_set_element_tab_index(element : @dom.Element, value : Int) -> Unit {
  if element.to_html_element() is Some(html_element) {
    html_element.set_tab_index(value)
  }
}

///|
#cfg(target="js")
fn ui_element_is_enabled(element : @dom.Element) -> Bool {
  !element.has_attribute("disabled") &&
  element.get_attribute("aria-disabled").unwrap_or("") != "true"
}

///|
#cfg(target="js")
fn ui_element_is_rtl(element : @dom.Element) -> Bool {
  if ui_has_window() {
    let style = @dom.window().get_computed_style(element)
    style.get_property_value("direction") == "rtl"
  } else {
    false
  }
}

///|
#cfg(target="js")
priv struct UiRovingContext {
  current : @dom.Element
  root : @dom.Element
  items : Array[@dom.Element]
}

///|
#cfg(target="js")
fn ui_roving_context(
  event : @dom.KeyboardEvent,
  current_selector : String,
  root_selector : String,
  item_selector : String,
) -> UiRovingContext? {
  guard event.target().to_element() is Some(target) else { return None }
  guard target.closest(current_selector) is Some(current) else { return None }
  guard current.closest(root_selector) is Some(root) else { return None }
  let items = root
    .query_selector_all(item_selector)
    .filter(item => {
      item.closest(root_selector) is Some(owner) &&
      owner.is_same_node(root.as_node()) &&
      ui_element_is_enabled(item)
    })
  if items.length() == 0 {
    None
  } else {
    Some({ current, root, items })
  }
}

///|
#cfg(target="js")
fn ui_roving_target(
  context : UiRovingContext,
  key : String,
  orientation : String,
  rtl : Bool,
) -> @dom.Element? {
  let mut index = -1
  for item_index, item in context.items {
    if item.is_same_node(context.current.as_node()) {
      index = item_index
    }
  }
  if index < 0 {
    return None
  }
  let length = context.items.length()
  let previous = if index == 0 { length - 1 } else { index - 1 }
  let next = if index == length - 1 { 0 } else { index + 1 }
  let target_index = match key {
    "Home" => 0
    "End" => length - 1
    "ArrowUp" if orientation == "vertical" || orientation == "both" => previous
    "ArrowDown" if orientation == "vertical" || orientation == "both" => next
    "ArrowLeft" if orientation == "horizontal" || orientation == "both" =>
      if rtl {
        next
      } else {
        previous
      }
    "ArrowRight" if orientation == "horizontal" || orientation == "both" =>
      if rtl {
        previous
      } else {
        next
      }
    _ => -1
  }
  context.items.get(target_index)
}

///|
fn ui_parse_int_or(value : String, fallback : Int) -> Int {
  @string.parse_int(value) catch {
    _ => fallback
  }
}

///|
fn ui_parse_double_or(value : String, fallback : Double) -> Double {
  @string.parse_double(value) catch {
    _ => fallback
  }
}