///|
/// browsingContext.diffPaintTree (#23): render two HTML variants and diff their
/// paint trees at the property/layout level, surfacing changes that a pixel
/// diff misses (e.g. a background color removed where the parent has the same
/// color, or a layout-affecting property that shifts boxes). Nodes are matched
/// positionally — the CSS-mutation use case keeps the same DOM structure and
/// only varies CSS.

///|
fn BidiProtocol::render_html_to_paint_node(
  self : BidiProtocol,
  html : String,
  width : Int,
  height : Int,
) -> @paint_model.PaintNode? {
  let _ = self
  let ctx = @browser_helpers.create_render_context(width, height, false)
  let (node, layout) = @renderer.render_to_node_and_layout(html, ctx)
  @tui_paint_viewport.from_node_and_layout_with_viewport_rect(
    node,
    layout,
    0.0,
    0.0,
    width.to_double(),
    height.to_double(),
    0.0,
    0.0,
  )
}

///|
fn paint_diff_push_change(
  changes : Array[Json],
  node_id : String,
  property : String,
  before : String,
  after : String,
) -> Unit {
  changes.push(
    make_object({
      "nodeSelector": Json::string(node_id),
      "property": Json::string(property),
      "before": Json::string(before),
      "after": Json::string(after),
    }),
  )
}

///|
fn paint_diff_color_change(
  changes : Array[Json],
  node_id : String,
  property : String,
  before : @types.Color,
  after : @types.Color,
) -> Unit {
  if before != after {
    paint_diff_push_change(
      changes,
      node_id,
      property,
      before.to_rgba_string(),
      after.to_rgba_string(),
    )
  }
}

///|
fn paint_diff_double_change(
  changes : Array[Json],
  node_id : String,
  property : String,
  before : Double,
  after : Double,
) -> Unit {
  if (before - after).abs() > 0.001 {
    paint_diff_push_change(
      changes,
      node_id,
      property,
      before.to_string(),
      after.to_string(),
    )
  }
}

///|
/// Walk two structurally-aligned paint trees, collecting property `changes`
/// and rect `layoutShifts`. Children are matched by index.
fn collect_paint_tree_diff(
  a : @paint_model.PaintNode,
  b : @paint_model.PaintNode,
  changes : Array[Json],
  shifts : Array[Json],
) -> Unit {
  let dx = b.x - a.x
  let dy = b.y - a.y
  let dw = b.width - a.width
  let dh = b.height - a.height
  if dx.abs() > 0.001 ||
    dy.abs() > 0.001 ||
    dw.abs() > 0.001 ||
    dh.abs() > 0.001 {
    shifts.push(
      make_object({
        "nodeSelector": Json::string(b.id),
        "dx": Json::number(dx),
        "dy": Json::number(dy),
        "dw": Json::number(dw),
        "dh": Json::number(dh),
      }),
    )
  }
  paint_diff_color_change(changes, b.id, "color", a.paint.color, b.paint.color)
  paint_diff_color_change(
    changes,
    b.id,
    "background-color",
    a.paint.background_color,
    b.paint.background_color,
  )
  paint_diff_double_change(
    changes,
    b.id,
    "opacity",
    a.paint.opacity,
    b.paint.opacity,
  )
  paint_diff_double_change(
    changes,
    b.id,
    "font-size",
    a.paint.font_size,
    b.paint.font_size,
  )
  paint_diff_double_change(
    changes,
    b.id,
    "font-weight",
    a.paint.font_weight,
    b.paint.font_weight,
  )
  let n = if a.children.length() < b.children.length() {
    a.children.length()
  } else {
    b.children.length()
  }
  for i = 0; i < n; i = i + 1 {
    collect_paint_tree_diff(a.children[i], b.children[i], changes, shifts)
  }
}

///|
fn paint_diff_variant_html(params : Map[String, Json], key : String) -> String? {
  match params.get(key) {
    Some(Object(map)) =>
      match map.get("html") {
        Some(String(html)) => Some(html)
        _ => None
      }
    Some(String(html)) => Some(html)
    _ => None
  }
}

///|
fn BidiProtocol::handle_diff_paint_tree(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  match self.resolve_diff_paint_tree(request_id, params) {
    Some(result) => self.send_success(request_id, Some(result))
    None => ()
  }
}

///|
fn BidiProtocol::resolve_diff_paint_tree(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Json? {
  let params = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return None
    }
  }
  let ctx_id = match params.get("context") {
    Some(String(id)) => id
    _ => {
      self.send_error(
        request_id, "invalid argument", "context must be a string",
      )
      return None
    }
  }
  let baseline_html = match paint_diff_variant_html(params, "baseline") {
    Some(html) => html
    None => {
      self.send_error(
        request_id, "invalid argument", "baseline.html must be a string",
      )
      return None
    }
  }
  let current_html = match paint_diff_variant_html(params, "current") {
    Some(html) => html
    None => {
      self.send_error(
        request_id, "invalid argument", "current.html must be a string",
      )
      return None
    }
  }
  let _session = match self.manager.get_session(ctx_id) {
    Some(session) => session
    None => {
      self.send_error(request_id, "no such frame", "Unknown context: " + ctx_id)
      return None
    }
  }
  set_runtime_context(ctx_id)
  self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
  let width = self.resolve_effective_viewport_width(ctx_id)
  let height = self.resolve_effective_viewport_height(ctx_id)
  let baseline_node = match
    self.render_html_to_paint_node(baseline_html, width, height) {
    Some(node) => node
    None => {
      self.send_error(
        request_id, "unknown error", "Failed to build baseline paint tree",
      )
      return None
    }
  }
  let current_node = match
    self.render_html_to_paint_node(current_html, width, height) {
    Some(node) => node
    None => {
      self.send_error(
        request_id, "unknown error", "Failed to build current paint tree",
      )
      return None
    }
  }
  let changes : Array[Json] = []
  let shifts : Array[Json] = []
  collect_paint_tree_diff(baseline_node, current_node, changes, shifts)
  Some(
    make_object({
      "changes": Json::array(changes),
      "layoutShifts": Json::array(shifts),
    }),
  )
}

///|
fn paint_diff_string_field(map : Map[String, Json], key : String) -> String {
  match map.get(key) {
    Some(String(value)) => value
    _ => ""
  }
}

///|
/// Reject tokens that could break out of the injected `")
  buf.to_string()
}

///|
fn paint_node_rect_json(node : @paint_model.PaintNode) -> Json {
  make_object({
    "x": Json::number(node.x),
    "y": Json::number(node.y),
    "width": Json::number(node.width),
    "height": Json::number(node.height),
  })
}

///|
fn paint_node_paint_changed(
  a : @paint_model.PaintNode,
  b : @paint_model.PaintNode,
) -> Bool {
  a.paint.color != b.paint.color ||
  a.paint.background_color != b.paint.background_color ||
  (a.paint.opacity - b.paint.opacity).abs() > 0.001 ||
  (a.paint.font_size - b.paint.font_size).abs() > 0.001 ||
  (a.paint.font_weight - b.paint.font_weight).abs() > 0.001
}

///|
fn collect_paint_mutation_changes(
  a : @paint_model.PaintNode,
  b : @paint_model.PaintNode,
  seen : Map[String, Bool],
  affected : Array[Json],
  layout_changes : Array[Json],
) -> Unit {
  let rect_changed = (a.x - b.x).abs() > 0.001 ||
    (a.y - b.y).abs() > 0.001 ||
    (a.width - b.width).abs() > 0.001 ||
    (a.height - b.height).abs() > 0.001
  if rect_changed {
    layout_changes.push(
      make_object({
        "selector": Json::string(b.id),
        "before": paint_node_rect_json(a),
        "after": paint_node_rect_json(b),
      }),
    )
  }
  if rect_changed || paint_node_paint_changed(a, b) {
    match seen.get(b.id) {
      Some(_) => ()
      None => {
        seen[b.id] = true
        affected.push(Json::string(b.id))
      }
    }
  }
  let n = if a.children.length() < b.children.length() {
    a.children.length()
  } else {
    b.children.length()
  }
  for i = 0; i < n; i = i + 1 {
    collect_paint_mutation_changes(
      a.children[i],
      b.children[i],
      seen,
      affected,
      layout_changes,
    )
  }
}

///|
fn BidiProtocol::handle_mutate_style(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  match self.resolve_mutate_style(request_id, params) {
    Some(result) => self.send_success(request_id, Some(result))
    None => ()
  }
}

///|
/// browsingContext.mutateStyle (#24): re-render the current page with CSS
/// property mutations applied (as injected !important overrides) and report
/// the affected nodes and their layout changes, for CSS impact / dead-code
/// analysis without editing the source.
fn BidiProtocol::resolve_mutate_style(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Json? {
  let params = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return None
    }
  }
  let ctx_id = match params.get("context") {
    Some(String(id)) => id
    _ => {
      self.send_error(
        request_id, "invalid argument", "context must be a string",
      )
      return None
    }
  }
  let mutations = match params.get("mutations") {
    Some(Array(items)) => items
    _ => {
      self.send_error(
        request_id, "invalid argument", "mutations must be an array",
      )
      return None
    }
  }
  let _session = match self.manager.get_session(ctx_id) {
    Some(session) => session
    None => {
      self.send_error(request_id, "no such frame", "Unknown context: " + ctx_id)
      return None
    }
  }
  set_runtime_context(ctx_id)
  self.apply_effective_viewport_to_runtime_context(ctx_id, ctx_id)
  let width = self.resolve_effective_viewport_width(ctx_id)
  let height = self.resolve_effective_viewport_height(ctx_id)
  let baseline_html = @protocol.extract_string_value_from_evaluate_result(
    evaluate_js(serialize_live_document_html_expr()),
  )
  let mutated_html = baseline_html + build_mutation_override_css(mutations)
  let baseline_node = match
    self.render_html_to_paint_node(baseline_html, width, height) {
    Some(node) => node
    None => {
      self.send_error(
        request_id, "unknown error", "Failed to build baseline paint tree",
      )
      return None
    }
  }
  let mutated_node = match
    self.render_html_to_paint_node(mutated_html, width, height) {
    Some(node) => node
    None => {
      self.send_error(
        request_id, "unknown error", "Failed to build mutated paint tree",
      )
      return None
    }
  }
  let seen : Map[String, Bool] = {}
  let affected : Array[Json] = []
  let layout_changes : Array[Json] = []
  collect_paint_mutation_changes(
    baseline_node, mutated_node, seen, affected, layout_changes,
  )
  Some(
    make_object({
      "affectedNodes": Json::array(affected),
      "layoutChanges": Json::array(layout_changes),
    }),
  )
}