///|
/// BiDi handler for browsingContext.getCssRuleViewportMap
fn BidiProtocol::handle_get_css_rule_viewport_map(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  match self.resolve_get_css_rule_viewport_map(request_id, params) {
    Some(result) => self.send_success(request_id, Some(result))
    None => ()
  }
}

///|
fn BidiProtocol::resolve_get_css_rule_viewport_map(
  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", "Missing context")
      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
    }
  }
  let viewport_widths : Array[Int] = match params.get("viewportWidths") {
    Some(Array(items)) => {
      let widths : Array[Int] = []
      for item in items {
        match item {
          Number(n, ..) => widths.push(n.to_int())
          _ => ()
        }
      }
      widths
    }
    None => [375, 640, 768, 1024, 1280, 1440]
    _ => {
      self.send_error(
        request_id, "invalid argument", "viewportWidths must be an array of numbers",
      )
      return None
    }
  }
  let html = responsive_breakpoint_source_html("live-inline")
  let map = @responsive.discover_css_rule_viewport_map(html, viewport_widths)
  let rules_json : Array[Json] = []
  for entry in map.rules {
    let item : Map[String, Json] = {}
    item["selector"] = Json::string(entry.selector)
    item["properties"] = string_array_to_json(entry.properties)
    item["mediaCondition"] = Json::string(entry.media_condition)
    let active : Array[Json] = []
    for w in entry.active_at_widths {
      active.push(Json::number(w.to_double()))
    }
    item["activeAtWidths"] = Json::array(active)
    let inactive : Array[Json] = []
    for w in entry.inactive_at_widths {
      inactive.push(Json::number(w.to_double()))
    }
    item["inactiveAtWidths"] = Json::array(inactive)
    rules_json.push(make_object(item))
  }
  let result : Map[String, Json] = {}
  result["rules"] = Json::array(rules_json)
  Some(make_object(result))
}

///|
/// BiDi handler for browsingContext.getCssRuleUsage
fn BidiProtocol::handle_get_css_rule_usage(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  match self.resolve_get_css_rule_usage(request_id, params) {
    Some(result) => self.send_success(request_id, Some(result))
    None => ()
  }
}

///|
fn BidiProtocol::resolve_get_css_rule_usage(
  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", "Missing context")
      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
    }
  }
  let html = responsive_breakpoint_source_html("live-inline")
  let usage = @responsive.discover_css_rule_usage(html)
  let effective_sources = @responsive.discover_css_effective_property_sources(
    html,
  )
  let rules_json : Array[Json] = []
  for entry in usage {
    let item : Map[String, Json] = {}
    item["selector"] = Json::string(entry.selector)
    item["matched"] = Json::boolean(entry.matched)
    item["elements"] = Json::number(entry.elements.to_double())
    item["overridden"] = Json::boolean(entry.overridden)
    match entry.overridden_by {
      Some(value) => item["overriddenBy"] = Json::string(value)
      None => ()
    }
    item["noEffect"] = Json::boolean(entry.no_effect)
    match entry.no_effect_reason {
      Some(value) => item["noEffectReason"] = Json::string(value)
      None => ()
    }
    rules_json.push(make_object(item))
  }
  let elements_json : Map[String, Json] = {}
  effective_sources.each(fn(selector, sources) {
    let source_json : Map[String, Json] = {}
    sources.each(fn(property, label) {
      source_json[property] = Json::string(label)
    })
    elements_json[selector] = make_object(source_json)
  })
  Some(
    make_object({
      "rules": Json::array(rules_json),
      "elements": make_object(elements_json),
    }),
  )
}

///|
/// BiDi handler for browsingContext.getRequiredTestViewports
fn BidiProtocol::handle_get_required_test_viewports(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  match self.resolve_get_required_test_viewports(request_id, params) {
    Some(result) => self.send_success(request_id, Some(result))
    None => ()
  }
}

///|
fn BidiProtocol::resolve_get_required_test_viewports(
  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", "Missing context")
      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
    }
  }
  let html = responsive_breakpoint_source_html("live-inline")
  let result = @responsive.discover_required_test_viewports(html)
  let viewports_json : Array[Json] = []
  for v in result.viewports {
    let item : Map[String, Json] = {}
    item["width"] = Json::number(v.width.to_double())
    item["reason"] = Json::string(v.reason)
    viewports_json.push(make_object(item))
  }
  let out : Map[String, Json] = {}
  out["viewports"] = Json::array(viewports_json)
  Some(make_object(out))
}

///|
/// BiDi handler for browsingContext.batchRender
fn BidiProtocol::handle_batch_render(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  match self.resolve_batch_render(request_id, params) {
    Some(result) => self.send_success(request_id, Some(result))
    None => ()
  }
}

///|
fn BidiProtocol::resolve_batch_render(
  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", "Missing context")
      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
    }
  }
  let options = match
    @rendering.normalize_batch_render_options(
      params,
      responsive_breakpoint_source_html("live-inline"),
    ) {
    Ok(options) => options
    Err(error) => {
      self.send_error(request_id, error.name(), error.message())
      return None
    }
  }
  let results = @vrt.render_html_batch_variants(
    options.base_html,
    options.viewport,
    options.variants,
  )
  Some(@rendering.batch_render_results_to_json(results))
}

///|
/// BiDi handler for browsingContext.getComputedStylesWithState
fn BidiProtocol::handle_get_computed_styles_with_state(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Unit {
  match self.resolve_get_computed_styles_with_state(request_id, params) {
    Some(result) => self.send_success(request_id, Some(result))
    None => ()
  }
}

///|
fn BidiProtocol::resolve_get_computed_styles_with_state(
  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", "Missing context")
      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
    }
  }
  let selector = match params.get("selector") {
    Some(String(s)) => s
    _ => {
      self.send_error(
        request_id, "invalid argument", "selector must be a string",
      )
      return None
    }
  }
  // Parse forced states
  let mut hover = false
  let mut focus = false
  let mut active = false
  let mut focus_visible = false
  let mut focus_within = false
  match params.get("forcedStates") {
    Some(Array(states)) =>
      for state in states {
        match state {
          String("hover") => hover = true
          String("focus") => focus = true
          String("active") => active = true
          String("focus-visible") => focus_visible = true
          String("focus-within") => focus_within = true
          _ => ()
        }
      }
    _ => ()
  }
  let forced_state_names : Array[String] = []
  if hover {
    forced_state_names.push("hover")
  }
  if focus {
    forced_state_names.push("focus")
  }
  if active {
    forced_state_names.push("active")
  }
  if focus_visible {
    forced_state_names.push("focus-visible")
  }
  if focus_within {
    forced_state_names.push("focus-within")
  }
  // Parse properties to check
  let properties : Array[String] = []
  match params.get("properties") {
    Some(Array(props)) =>
      for prop in props {
        match prop {
          String(s) => properties.push(s)
          _ => ()
        }
      }
    _ => ()
  }
  let html = responsive_breakpoint_source_html("live-inline")
  let result = @responsive.discover_computed_styles_with_state_names(
    html, selector, forced_state_names, properties,
  )
  // Build JSON response
  let normal_json : Map[String, Json] = {}
  result.normal.each(fn(prop, val) { normal_json.set(prop, Json::string(val)) })
  let forced_json : Map[String, Json] = {}
  result.forced.each(fn(prop, val) { forced_json.set(prop, Json::string(val)) })
  let diff_json : Array[Json] = []
  for d in result.diff {
    diff_json.push(
      make_object({
        "property": Json::string(d.property),
        "normal": Json::string(d.normal),
        "forced": Json::string(d.forced),
      }),
    )
  }
  let out : Map[String, Json] = {}
  out["normal"] = make_object(normal_json)
  out["forced"] = make_object(forced_json)
  out["diff"] = Json::array(diff_json)
  Some(make_object(out))
}