///|
// Render root selection and root layout adjustment helpers.

///|
/// Find the deepest body element in HTML tree
/// If there are nested body elements (invalid HTML), find the innermost one
fn find_body(elem : @html.Element) -> @html.Element? {
  if elem.tag == "body" {
    // Check if there's a nested body inside this one
    match find_body_in_children(elem) {
      Some(inner_body) => Some(inner_body)
      None => Some(elem)
    }
  } else {
    find_body_in_children(elem)
  }
}

///|
/// Helper to find body in children
fn find_body_in_children(elem : @html.Element) -> @html.Element? {
  for child in elem.children {
    match child {
      @html.Node::Element(child_elem) =>
        match find_body(child_elem) {
          Some(body) => return Some(body)
          None => ()
        }
      _ => ()
    }
  }
  None
}

///|
/// Resolve zoom on the document root (html element).
/// We render from body in most paths, so root zoom needs to be propagated.
fn resolve_document_root_zoom(
  doc_root : @html.Element,
  ctx : RenderContext,
  indexed_stylesheets : Array[@css.IndexedStylesheet],
  css_vars : Map[String, String],
) -> Double {
  if doc_root.tag.to_lower() != "html" {
    return 1.0
  }
  let root_selector = if indexed_stylesheets.length() == 0 {
    html_to_selector_element_minimal(doc_root, None)
  } else {
    html_to_selector_element(doc_root, None)
  }
  let root_style = compute_element_style_indexed(
    root_selector,
    doc_root.style,
    indexed_stylesheets,
    true,
    ctx,
    None,
    css_vars,
  )
  root_style.zoom
}

///|
/// We render from  in most paths. When  establishes multicol
/// formatting, mirror it onto body root unless body already establishes one.
fn propagate_document_root_multicol_to_body(
  body_style : @style.Style,
  doc_root_style : @style.Style,
) -> @style.Style {
  if body_style.has_multicol() || !doc_root_style.has_multicol() {
    return body_style
  }
  {
    ..body_style,
    column_count: doc_root_style.column_count,
    column_width: doc_root_style.column_width,
    column_gap: doc_root_style.column_gap,
    column_gap_is_normal: doc_root_style.column_gap_is_normal,
    column_fill: doc_root_style.column_fill,
  }
}

///|
fn should_layout_document_root(
  doc_root : @html.Element,
  doc_root_style : @style.Style,
) -> Bool {
  if doc_root.tag.to_lower() != "html" {
    return false
  }
  doc_root_style.position == @types.Absolute ||
  doc_root_style.position == @types.Fixed
}

///|
fn select_render_root(
  doc_root : @html.Element,
  doc_root_style : @style.Style,
) -> @html.Element {
  if should_layout_document_root(doc_root, doc_root_style) {
    return doc_root
  }
  match find_body(doc_root) {
    Some(body) => body
    None => doc_root
  }
}

///|
fn resolve_root_available_width(
  viewport_width : Double,
  root_margin : @types.Rect[Double],
  doc_root_style : @style.Style?,
) -> Double {
  // The available width is the root's containing block (the initial containing
  // block = the viewport). The root element's own margin is subtracted once by
  // its block layout, so it must NOT be subtracted here as well, or a body with
  // the UA default 8px margin ends up 2x16px narrower than the viewport.
  let base = viewport_width
  let base = if base > 0.0 { base } else { 0.0 }
  match doc_root_style {
    None => base
    Some(style) => {
      let mut doc_width = match style.width {
        @types.Length(v) => v
        @types.Percent(p) => viewport_width * p
        @types.Calc(px, pct) => viewport_width * pct + px
        @types.MathFn(__mop, __mterms) =>
          @types.apply_math_op(
            __mop,
            __mterms.map(fn(__t) { viewport_width * __t.1 + __t.0 }),
          )
        @types.Auto
        | @types.MinContent
        | @types.MaxContent
        | @types.FitContent(_) => viewport_width
      }
      let min_width = match style.min_width {
        @types.Length(v) => v
        @types.Percent(p) => viewport_width * p
        @types.Calc(px, pct) => viewport_width * pct + px
        @types.MathFn(__mop, __mterms) =>
          @types.apply_math_op(
            __mop,
            __mterms.map(fn(__t) { viewport_width * __t.1 + __t.0 }),
          )
        @types.Auto
        | @types.MinContent
        | @types.MaxContent
        | @types.FitContent(_) => 0.0
      }
      let max_width = match style.max_width {
        @types.Length(v) => v
        @types.Percent(p) => viewport_width * p
        @types.Calc(px, pct) => viewport_width * pct + px
        @types.MathFn(__mop, __mterms) =>
          @types.apply_math_op(
            __mop,
            __mterms.map(fn(__t) { viewport_width * __t.1 + __t.0 }),
          )
        @types.Auto
        | @types.MinContent
        | @types.MaxContent
        | @types.FitContent(_) => 1.0e10
      }
      if doc_width > max_width {
        doc_width = max_width
      }
      if doc_width < min_width {
        doc_width = min_width
      }
      // Margin is subtracted by the root's block layout (see note above), not
      // here.
      let constrained = if doc_width > 0.0 { doc_width } else { 0.0 }
      if constrained < base {
        constrained
      } else {
        base
      }
    }
  }
}

///|
fn node_with_style(node : @node.Node, style : @style.Style) -> @node.Node {
  @node.Node::with_uid_and_measure(
    node.id,
    node.uid,
    style,
    node.children,
    node.measure,
    node.text,
  )
}

///|
/// Clamp body height only when author explicitly requests viewport-relative
/// percentage sizing.
fn should_clamp_body_to_viewport(root : @node.Node) -> Bool {
  if root.id != "body" {
    return false
  }
  let has_viewport_height_request = match root.style.height {
    @types.Percent(p) => p >= 1.0
    _ =>
      match root.style.min_height {
        @types.Percent(p) => p >= 1.0
        _ => false
      }
  }
  has_viewport_height_request
}

///|
fn adjust_root_height_for_viewport(
  layout : @layout_types.Layout,
  root : @node.Node,
  render_root_tag : String,
  viewport_height : Double,
) -> @layout_types.Layout {
  let content_extent = calculate_content_extent(layout)
  if should_clamp_body_to_viewport(root) {
    let min_viewport_height = if layout.height < viewport_height {
      viewport_height
    } else {
      layout.height
    }
    let final_height = if content_extent > min_viewport_height {
      content_extent
    } else {
      min_viewport_height
    }
    return { ..layout, height: final_height }
  }
  let keeps_empty_viewport_height = (
      render_root_tag == "body" || render_root_tag == "html"
    ) &&
    root.style.display != @types.Display::Contents &&
    root.style.position != @types.Absolute &&
    root.style.position != @types.Fixed &&
    layout.height <= 0.0 &&
    content_extent <= 0.0
  if keeps_empty_viewport_height {
    return { ..layout, height: viewport_height }
  }
  layout
}

///|
fn stretch_single_frameset_child_to_root(
  layout : @layout_types.Layout,
) -> @layout_types.Layout {
  if layout.children.length() != 1 {
    return layout
  }
  let child = layout.children[0]
  let is_frameset = child.id == "frameset" ||
    child.id.has_prefix("frameset#") ||
    child.id.has_prefix("frameset.")
  if !is_frameset || child.height > 0.0 {
    return layout
  }
  let adjusted_children : Array[@layout_types.Layout] = []
  adjusted_children.push({
    ..child,
    width: if child.width > 0.0 {
      child.width
    } else {
      layout.width
    },
    height: layout.height,
  })
  { ..layout, children: adjusted_children }
}

///|
/// Create a recursive zero-sized layout for nodes that don't generate a box
/// (e.g. display:none on root/body).
fn create_zero_layout_from_node(node : @node.Node) -> @layout_types.Layout {
  let children : Array[@layout_types.Layout] = []
  for i = 0; i < node.children.length(); i = i + 1 {
    children.push(create_zero_layout_from_node(node.children[i]))
  }
  {
    id: node.id,
    x: 0.0,
    y: 0.0,
    width: 0.0,
    height: 0.0,
    margin: @types.Rect::zero(),
    padding: @types.Rect::zero(),
    border: @types.Rect::zero(),
    overflow_x: @types.Visible,
    overflow_y: @types.Visible,
    scroll_width: 0.0,
    scroll_height: 0.0,
    children,
    text: node.text,
  }
}

///|
let active_before_index : Ref[PseudoRuleIndex] = {
  val: { rules: [], by_id: {}, by_class: {}, by_tag: {}, universal: [] },
}

///|
let active_after_index : Ref[PseudoRuleIndex] = {
  val: { rules: [], by_id: {}, by_class: {}, by_tag: {}, universal: [] },
}

///|
pub fn build_render_root_node(
  doc : @html.Document,
  ctx : RenderContext,
  prepared : PreparedRenderDocument,
) -> @node.Node {
  active_before_index.val = prepared.before_index
  active_after_index.val = prepared.after_index
  current_cellpadding.val = -1.0 // Reset cellpadding for new render
  // Viewport culling: skip expensive processing for below-fold elements
  viewport_estimated_y.val = 0.0
  // Keep several screens of full cascade, then switch long pages to skeleton
  // nodes. Real-world VRT captures only the viewport, and large docs pages can
  // otherwise spend minutes styling content far below the fold.
  let conservative_cutoff = ctx.viewport_height * 4.0
  viewport_cutoff.val = if conservative_cutoff > 4096.0 {
    conservative_cutoff
  } else {
    4096.0
  }
  viewport_skeleton_count.val = 0
  viewport_full_node_count.val = 0
  let root = if prepared.body_uses_document_root_style {
    let body_selector = if prepared.indexed_stylesheets.length() == 0 {
      html_to_selector_element_minimal(
        prepared.render_root,
        Some(prepared.doc_root_selector),
      )
    } else {
      html_to_selector_element(
        prepared.render_root,
        Some(prepared.doc_root_selector),
      )
    }
    element_to_node_with_styles_internal(
      prepared.render_root,
      body_selector,
      ctx,
      prepared.stylesheets,
      prepared.indexed_stylesheets,
      Some(prepared.doc_root_style),
      prepared.css_vars,
      {},
      "root",
    )
  } else {
    element_to_node_with_styles(
      prepared.render_root,
      None,
      ctx,
      prepared.stylesheets,
      prepared.indexed_stylesheets,
      prepared.css_vars,
      {},
      "root",
    )
  }
  let root = if prepared.body_uses_document_root_style {
    node_with_style(
      root,
      propagate_document_root_multicol_to_body(
        root.style,
        prepared.doc_root_style,
      ),
    )
  } else {
    root
  }
  if prepared.render_root_tag == "body" {
    let root_zoom = resolve_document_root_zoom(
      doc.root,
      ctx,
      prepared.indexed_stylesheets,
      prepared.css_vars,
    )
    if root_zoom != 1.0 {
      root.style.zoom = root.style.zoom * root_zoom
    }
  }
  maybe_log_perf(
    "[perf] viewport_culling: estimated_y=" +
    viewport_estimated_y.val.to_int().to_string() +
    " cutoff=" +
    viewport_cutoff.val.to_int().to_string() +
    " skeleton=" +
    viewport_skeleton_count.val.to_string() +
    " full_nodes=" +
    viewport_full_node_count.val.to_string(),
  )
  root
}

///|
pub fn compute_layout_from_render_root(
  root : @node.Node,
  prepared : PreparedRenderDocument,
  ctx : RenderContext,
) -> @layout_types.Layout {
  if prepared.doc_root_style.display == @types.Display::None ||
    root.style.display == @types.Display::None {
    return create_zero_layout_from_node(root)
  }

  let root_margin = @types.resolve_rect(root.style.margin, ctx.viewport_width)
  let available_width = resolve_root_available_width(
    ctx.viewport_width,
    root_margin,
    if prepared.body_uses_document_root_style {
      Some(prepared.doc_root_style)
    } else {
      None
    },
  )
  let root_available_height : Double? = if prepared.render_root_tag == "body" {
    match root.style.height {
      @types.Length(_) | @types.Percent(_) => Some(ctx.viewport_height)
      _ =>
        if root.style.writing_mode.is_vertical() {
          Some(ctx.viewport_height)
        } else {
          None
        }
    }
  } else if prepared.render_root_tag == "html" &&
    root.style.position != @types.Absolute &&
    root.style.position != @types.Fixed {
    Some(ctx.viewport_height)
  } else {
    match root.style.height {
      @types.Length(_) | @types.Percent(_) => Some(ctx.viewport_height)
      _ => None
    }
  }
  let layout_ctx : @layout_types.LayoutContext = {
    available_width,
    available_height: root_available_height,
    sizing_mode: @layout_types.SizingMode::Definite,
    viewport_width: ctx.viewport_width,
    viewport_height: ctx.viewport_height,
    stretch_width: false,
    stretch_height: false,
  }

  @node.setup()

  let layout = @node.compute_layout_in_context(root, layout_ctx)
  let scaled_layout = apply_zoom_and_scale(
    layout,
    root,
    1.0,
    1.0,
    1.0,
    0.0,
    0.0,
    false,
    0.0,
    0.0,
    ctx.viewport_width,
    ctx.viewport_height,
  )
  let scaled_layout = adjust_root_height_for_viewport(
    scaled_layout,
    root,
    prepared.render_root_tag,
    ctx.viewport_height,
  )
  let scaled_layout = stretch_single_frameset_child_to_root(scaled_layout)

  let margin_left_is_auto = match root.style.margin.left {
    @types.Dimension::Auto => true
    _ => false
  }
  let margin_right_is_auto = match root.style.margin.right {
    @types.Dimension::Auto => true
    _ => false
  }
  let x_offset = if margin_left_is_auto && margin_right_is_auto {
    (ctx.viewport_width - scaled_layout.width) / 2.0
  } else if margin_left_is_auto {
    ctx.viewport_width - scaled_layout.width - root_margin.right
  } else {
    root_margin.left
  }
  let y_offset = if prepared.suppress_quirks_body_ua_top_margin {
    root_margin.top
  } else {
    root_margin.top + scaled_layout.y
  }
  {
    id: scaled_layout.id,
    x: x_offset,
    y: y_offset,
    width: scaled_layout.width,
    height: scaled_layout.height,
    margin: scaled_layout.margin,
    padding: scaled_layout.padding,
    border: scaled_layout.border,
    overflow_x: scaled_layout.overflow_x,
    overflow_y: scaled_layout.overflow_y,
    scroll_width: scaled_layout.scroll_width,
    scroll_height: scaled_layout.scroll_height,
    children: scaled_layout.children,
    text: scaled_layout.text,
  }
}