///|
/// A rectangle in logical px, relative to the screen's top-left corner.
pub(all) struct LayoutRect {
  left : Double
  top : Double
  width : Double
  height : Double
} derive(Eq, Debug, ToJson)

///|
/// Synchronous window information. The safe area may be unavailable.
pub(all) struct WindowMetrics {
  window_width : Double
  window_height : Double
  screen_width : Double
  screen_height : Double
  screen_top : Double
  status_bar_height : Double
  safe_area : LayoutRect?
} derive(Eq, Debug, ToJson)

///|
/// Page-owned layout facts, separate from immutable route input.
pub(all) struct PageLayout {
  window : WindowMetrics?
  menu_button : LayoutRect?
} derive(Eq, Debug, ToJson)

///|
pub fn PageLayout::unavailable() -> PageLayout {
  { window: None, menu_button: None, }
}

///|
fn layout_number(value : Double) -> Bool {
  // The upper bound also rejects infinities; comparisons reject NaN.
  value >= 0.0 && value <= 1000000.0
}

///|
fn LayoutRect::valid(self : LayoutRect) -> Bool {
  layout_number(self.left) &&
  layout_number(self.top) &&
  layout_number(self.width) &&
  layout_number(self.height) &&
  self.width > 0.0 &&
  self.height > 0.0
}

///|
fn LayoutRect::fits(self : LayoutRect, window : WindowMetrics) -> Bool {
  self.valid() &&
  self.left + self.width <= window.screen_width &&
  self.top + self.height <= window.screen_height
}

///|
fn WindowMetrics::valid(self : WindowMetrics) -> Bool {
  layout_number(self.window_width) &&
  self.window_width > 0.0 &&
  layout_number(self.window_height) &&
  self.window_height > 0.0 &&
  layout_number(self.screen_width) &&
  self.screen_width >= self.window_width &&
  layout_number(self.screen_height) &&
  self.screen_height >= self.window_height &&
  layout_number(self.screen_top) &&
  self.screen_top + self.window_height <= self.screen_height &&
  layout_number(self.status_bar_height) &&
  self.status_bar_height <= self.screen_height
}

///|
fn merge_layout(next : PageLayout, previous : PageLayout) -> PageLayout {
  let window = match next.window {
    Some(window) if window.valid() => {
      let safe_area = match window.safe_area {
        Some(rect) if rect.fits(window) => Some(rect)
        _ =>
          match previous.window {
            Some(previous) =>
              match previous.safe_area {
                Some(rect) if rect.fits(window) => Some(rect)
                _ => None
              }
            None => None
          }
      }
      Some({ ..window, safe_area, })
    }
    _ => previous.window
  }
  let valid_menu = fn(rect : LayoutRect) {
    rect.valid() &&
    (match window {
      Some(window) => rect.fits(window)
      None => true
    })
  }
  let menu_button = match next.menu_button {
    Some(rect) if valid_menu(rect) => Some(rect)
    _ =>
      match previous.menu_button {
        Some(rect) if valid_menu(rect) => Some(rect)
        _ => None
      }
  }
  { window, menu_button, }
}

///|
fn layout_json_number(value : Json, key : String) -> Double {
  // Negative values are rejected by the geometry validators below. Keeping the
  // failure sentinel private avoids allocating a separate Option for every px.
  match value {
    Object(fields) =>
      match fields.get(key) {
        Some(Number(value, ..)) => value
        _ => -1.0
      }
    _ => -1.0
  }
}

///|
fn layout_json_field(value : Json, key : String) -> Json {
  match value {
    Object(fields) => fields.get(key).unwrap_or(Json::null())
    _ => Json::null()
  }
}

///|
fn decode_layout_rect(value : Json) -> LayoutRect? {
  let rect = LayoutRect::{
    left: layout_json_number(value, "left"),
    top: layout_json_number(value, "top"),
    width: layout_json_number(value, "width"),
    height: layout_json_number(value, "height"),
  }
  if rect.valid() {
    Some(rect)
  } else {
    None
  }
}

///|
fn decode_window_metrics(value : Json) -> WindowMetrics? {
  let window = WindowMetrics::{
    window_width: layout_json_number(value, "window_width"),
    window_height: layout_json_number(value, "window_height"),
    screen_width: layout_json_number(value, "screen_width"),
    screen_height: layout_json_number(value, "screen_height"),
    screen_top: layout_json_number(value, "screen_top"),
    status_bar_height: layout_json_number(value, "status_bar_height"),
    safe_area: decode_layout_rect(layout_json_field(value, "safe_area")),
  }
  if window.valid() {
    Some(window)
  } else {
    None
  }
}

///|
/// Decode the owned host layout envelope, dropping unavailable/invalid parts.
/// This never substitutes device dimensions or reads a host API.
pub fn PageLayout::from_json(value : Json) -> PageLayout {
  merge_layout(
    {
      window: decode_window_metrics(layout_json_field(value, "window")),
      menu_button: decode_layout_rect(layout_json_field(value, "menu_button")),
    },
    PageLayout::unavailable(),
  )
}

///|
priv struct PageLayoutState {
  value : Val[PageLayout]
  emit : Emit[PageLayout]
  pending : Ref[PageLayout?]
  visible : Ref[Bool]
}

///|
fn PageLayoutState::new(
  graph : @val.Graph,
  initial : PageLayout,
) -> PageLayoutState {
  // Host input needs the graph's existing transactional slot, not a second
  // generic application-state update/subscription adapter.
  let (value, emit) = graph.create_input(initial)
  {
    value: Val(value),
    emit: wrap_emit(emit),
    pending: Ref(None),
    visible: Ref(true),
  }
}

///|
fn PageLayoutState::command(self : PageLayoutState) -> Cmd {
  Cmd(
    @val.CmdMessage(fn() {
      if self.visible.val && self.pending.val is Some(layout) {
        self.pending.val = None
        // The message is consumed once, but its state write remains transactional.
        // Rejected projections roll back the Val, just like any business message.
        (self.emit)(layout).0
      } else {
        @val.CmdNone
      }
    }),
  )
}

///|
pub fn PageContext::layout(self : PageContext) -> Val[PageLayout] {
  self.layout_value
}

///|
fn PageRuntime::queue_layout(self : PageRuntime, layout : PageLayout) -> Unit {
  let previous = self.5.pending.val.unwrap_or_else(fn() {
    self.1.run(fn() { self.5.value.0.read() })
  })
  self.5.pending.val = Some(merge_layout(layout, previous))
}

///|
/// Update visible layout transactionally; hidden pages retain only the latest
/// metrics until Show. Invalid readings retain the last usable information.
pub fn PageRuntime::update_layout(
  self : PageRuntime,
  layout : PageLayout,
) -> String {
  if self.entry_error() is Some(error) {
    return error
  }
  self.queue_layout(layout)
  if self.5.visible.val {
    self.0.refresh()
  } else {
    "[]"
  }
}