///|
fn BidiProtocol::resolve_effective_geolocation_override(
  self : BidiProtocol,
  ctx_id : String,
) -> Json? {
  self.resolve_effective_scoped_override(
    ctx_id,
    self.emulation_state.emulation_geolocation_by_context,
    self.emulation_state.emulation_geolocation_by_user_context,
    self.emulation_state.emulation_geolocation_global,
  )
}

///|
fn BidiProtocol::resolve_effective_geolocation_error_code(
  self : BidiProtocol,
  ctx_id : String,
) -> Int? {
  if self.resolve_permission_state_for_context("geolocation", ctx_id) !=
    "granted" {
    return Some(1)
  }
  match self.resolve_effective_geolocation_override(ctx_id) {
    Some(Object(override_map)) =>
      match override_map.get("kind") {
        Some(String("error")) => Some(2)
        _ => None
      }
    _ => None
  }
}

///|
fn BidiProtocol::resolve_effective_geolocation_coordinates(
  self : BidiProtocol,
  ctx_id : String,
) -> Json? {
  if self.resolve_permission_state_for_context("geolocation", ctx_id) !=
    "granted" {
    return None
  }
  match self.resolve_effective_geolocation_override(ctx_id) {
    Some(Object(override_map)) =>
      match (override_map.get("kind"), override_map.get("coordinates")) {
        (Some(String("coordinates")), Some(Object(coords_map))) =>
          Some(make_object(coords_map))
        _ => None
      }
    None => Some(@browser_domain.default_geolocation_coordinates_json())
    _ => None
  }
}

///|
fn BidiProtocol::emit_geolocation_watch_update_for_scope(
  self : BidiProtocol,
  scope_key : String,
) -> Unit {
  let ctx_id = @browser_domain.geolocation_watch_scope_context(scope_key)
  let realm_id = match self.geolocation_watch_realm_by_scope.get(scope_key) {
    Some(realm_id) => realm_id
    None => return
  }
  let channel = match self.geolocation_watch_channel_by_scope.get(scope_key) {
    Some(channel) => channel
    None => return
  }
  match self.resolve_effective_geolocation_error_code(ctx_id) {
    Some(code) =>
      if self.geolocation_watch_notify_error_by_scope
        .get(scope_key)
        .unwrap_or(false) {
        self.emit_script_message(
          ctx_id,
          realm_id,
          channel,
          @browser_domain.build_geolocation_error_remote_value(code),
        )
      }
    None =>
      if self.geolocation_watch_notify_success_by_scope
        .get(scope_key)
        .unwrap_or(false) {
        let coordinates = self
          .resolve_effective_geolocation_coordinates(ctx_id)
          .unwrap_or(@browser_domain.default_geolocation_coordinates_json())
        self.emit_script_message(
          ctx_id,
          realm_id,
          channel,
          @browser_domain.build_geolocation_coordinates_remote_value(
            coordinates,
          ),
        )
      }
  }
}

///|
fn BidiProtocol::notify_geolocation_watchers(self : BidiProtocol) -> Unit {
  let watcher_keys : Array[String] = []
  for scope_key, _ in self.geolocation_watch_channel_by_scope {
    watcher_keys.push(scope_key)
  }
  for scope_key in watcher_keys {
    self.emit_geolocation_watch_update_for_scope(scope_key)
  }
}

///|
fn BidiProtocol::register_geolocation_watch(
  self : BidiProtocol,
  ctx_id : String,
  realm_id : String,
  channel : String,
  notify_success : Bool,
  notify_error : Bool,
) -> Int {
  let watch_id = self.next_geolocation_watch_id
  self.next_geolocation_watch_id = self.next_geolocation_watch_id + 1
  let scope_key = @browser_domain.geolocation_watch_scope_key(ctx_id, watch_id)
  self.geolocation_watch_channel_by_scope[scope_key] = channel
  self.geolocation_watch_realm_by_scope[scope_key] = realm_id
  self.geolocation_watch_notify_success_by_scope[scope_key] = notify_success
  self.geolocation_watch_notify_error_by_scope[scope_key] = notify_error
  self.emit_geolocation_watch_update_for_scope(scope_key)
  watch_id
}

///|
fn BidiProtocol::clear_geolocation_watch(
  self : BidiProtocol,
  ctx_id : String,
  watch_id : Int,
) -> Unit {
  let scope_key = @browser_domain.geolocation_watch_scope_key(ctx_id, watch_id)
  self.geolocation_watch_channel_by_scope.remove(scope_key)
  self.geolocation_watch_realm_by_scope.remove(scope_key)
  self.geolocation_watch_notify_success_by_scope.remove(scope_key)
  self.geolocation_watch_notify_error_by_scope.remove(scope_key)
}

///|
fn BidiProtocol::parse_geolocation_coordinates_param(
  self : BidiProtocol,
  request_id : Int,
  raw_value : Json,
) -> (Bool, Json?) {
  match raw_value {
    Null => (true, None)
    Object(coordinates_map) => {
      let latitude = match coordinates_map.get("latitude") {
        Some(value) =>
          match
            @browser_domain.normalize_geolocation_number_json(
              value, -90.0, 90.0,
            ) {
            Some(parsed) => parsed
            None => {
              self.send_error(
                request_id, "invalid argument", "coordinates.latitude is invalid",
              )
              return (false, None)
            }
          }
        None => {
          self.send_error(
            request_id, "invalid argument", "coordinates.latitude must be provided",
          )
          return (false, None)
        }
      }
      let longitude = match coordinates_map.get("longitude") {
        Some(value) =>
          match
            @browser_domain.normalize_geolocation_number_json(
              value, -180.0, 180.0,
            ) {
            Some(parsed) => parsed
            None => {
              self.send_error(
                request_id, "invalid argument", "coordinates.longitude is invalid",
              )
              return (false, None)
            }
          }
        None => {
          self.send_error(
            request_id, "invalid argument", "coordinates.longitude must be provided",
          )
          return (false, None)
        }
      }
      let accuracy = match coordinates_map.get("accuracy") {
        Some(value) =>
          match
            @browser_domain.normalize_geolocation_number_json(
              value, 0.0, 9007199254740991.0,
            ) {
            Some(parsed) => parsed
            None => {
              self.send_error(
                request_id, "invalid argument", "coordinates.accuracy is invalid",
              )
              return (false, None)
            }
          }
        None => 1.0
      }
      let altitude = match coordinates_map.get("altitude") {
        Some(value) =>
          match
            @browser_domain.normalize_geolocation_number_json(
              value, -9007199254740991.0, 9007199254740991.0,
            ) {
            Some(parsed) => Some(parsed)
            None => {
              self.send_error(
                request_id, "invalid argument", "coordinates.altitude is invalid",
              )
              return (false, None)
            }
          }
        None => None
      }
      let altitude_accuracy = match
        get_map_field_with_alias(
          coordinates_map, "altitudeAccuracy", "altitude_accuracy",
        ) {
        Some(value) =>
          match
            @browser_domain.normalize_geolocation_number_json(
              value, 0.0, 9007199254740991.0,
            ) {
            Some(parsed) => Some(parsed)
            None => {
              self.send_error(
                request_id, "invalid argument", "coordinates.altitudeAccuracy is invalid",
              )
              return (false, None)
            }
          }
        None => None
      }
      if altitude_accuracy is Some(_) && altitude is None {
        self.send_error(
          request_id, "invalid argument", "coordinates.altitudeAccuracy requires altitude",
        )
        return (false, None)
      }
      let heading = match coordinates_map.get("heading") {
        Some(value) =>
          match
            @browser_domain.normalize_geolocation_number_json(
              value, 0.0, 359.999999999,
            ) {
            Some(parsed) => Some(parsed)
            None => {
              self.send_error(
                request_id, "invalid argument", "coordinates.heading is invalid",
              )
              return (false, None)
            }
          }
        None => None
      }
      let speed = match coordinates_map.get("speed") {
        Some(value) =>
          match
            @browser_domain.normalize_geolocation_number_json(
              value, 0.0, 9007199254740991.0,
            ) {
            Some(parsed) => Some(parsed)
            None => {
              self.send_error(
                request_id, "invalid argument", "coordinates.speed is invalid",
              )
              return (false, None)
            }
          }
        None => None
      }

      let normalized : Map[String, Json] = {}
      normalized["latitude"] = Json::number(latitude)
      normalized["longitude"] = Json::number(longitude)
      normalized["accuracy"] = Json::number(accuracy)
      match altitude {
        Some(parsed) => normalized["altitude"] = Json::number(parsed)
        None => ()
      }
      match altitude_accuracy {
        Some(parsed) => normalized["altitudeAccuracy"] = Json::number(parsed)
        None => ()
      }
      match heading {
        Some(parsed) => normalized["heading"] = Json::number(parsed)
        None => ()
      }
      match speed {
        Some(parsed) => normalized["speed"] = Json::number(parsed)
        None => ()
      }
      (true, Some(make_object(normalized)))
    }
    _ => {
      self.send_error(
        request_id, "invalid argument", "coordinates must be an object or null",
      )
      (false, None)
    }
  }
}

///|
fn BidiProtocol::parse_geolocation_error_param(
  self : BidiProtocol,
  request_id : Int,
  raw_value : Json,
) -> (Bool, String?) {
  match raw_value {
    Null => (true, None)
    Object(error_map) =>
      match error_map.get("type") {
        Some(String("positionUnavailable")) =>
          (true, Some("positionUnavailable"))
        Some(String(_)) => {
          self.send_error(
            request_id, "invalid argument", "error.type is invalid",
          )
          (false, None)
        }
        _ => {
          self.send_error(
            request_id, "invalid argument", "error.type must be provided",
          )
          (false, None)
        }
      }
    _ => {
      self.send_error(
        request_id, "invalid argument", "error must be an object or null",
      )
      (false, None)
    }
  }
}

///|
fn BidiProtocol::resolve_emulation_set_geolocation_override(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let map = match params {
    Some(Object(map)) => map
    _ => {
      self.send_error(
        request_id, "invalid argument", "params must be an object",
      )
      return None
    }
  }

  let has_coordinates = map.contains("coordinates")
  let has_error = map.contains("error")
  if !has_coordinates && !has_error {
    self.send_error(
      request_id, "invalid argument", "coordinates or error must be provided",
    )
    return None
  }

  let coordinates_override : Json? = if has_coordinates {
    let raw_value = map.get("coordinates").unwrap_or(Json::null())
    let (ok, normalized_coordinates) = self.parse_geolocation_coordinates_param(
      request_id, raw_value,
    )
    if !ok {
      return None
    }
    match normalized_coordinates {
      Some(normalized_coordinates) =>
        Some(
          @browser_domain.build_geolocation_coordinates_override(
            normalized_coordinates,
          ),
        )
      None => None
    }
  } else {
    None
  }

  let error_override : Json? = if has_error {
    let raw_value = map.get("error").unwrap_or(Json::null())
    let (ok, error_type) = self.parse_geolocation_error_param(
      request_id, raw_value,
    )
    if !ok {
      return None
    }
    match error_type {
      Some(error_type) =>
        Some(@browser_domain.build_geolocation_error_override(error_type))
      None => None
    }
  } else {
    None
  }

  if coordinates_override is Some(_) && error_override is Some(_) {
    self.send_error(
      request_id, "invalid argument", "coordinates and error are mutually exclusive",
    )
    return None
  }

  let override_value = match coordinates_override {
    Some(value) => Some(value)
    None => error_override
  }

  let target_contexts : Array[String] = []
  match map.get("contexts") {
    Some(Array(contexts)) =>
      if contexts.length() == 0 {
        self.send_error(
          request_id, "invalid argument", "contexts must not be an empty array",
        )
        return None
      } else {
        for context in contexts {
          match context {
            String(ctx_id) =>
              if !self.manager.has_session(ctx_id) {
                self.send_error(
                  request_id,
                  "no such frame",
                  "Unknown context: " + ctx_id,
                )
                return None
              } else if self.context_parent.contains(ctx_id) {
                self.send_error(
                  request_id, "invalid argument", "contexts must reference top-level browsing contexts",
                )
                return None
              } else if !array_contains(target_contexts, ctx_id) {
                target_contexts.push(ctx_id)
              }
            _ => {
              self.send_error(
                request_id, "invalid argument", "contexts entries must be strings",
              )
              return None
            }
          }
        }
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "contexts must be an array",
      )
      return None
    }
    None => ()
  }

  let target_user_contexts : Array[String] = []
  match get_map_field_with_alias(map, "userContexts", "user_contexts") {
    Some(Array(user_contexts)) =>
      if user_contexts.length() == 0 {
        self.send_error(
          request_id, "invalid argument", "userContexts must not be an empty array",
        )
        return None
      } else {
        for raw_user_context in user_contexts {
          match raw_user_context {
            String(user_context_id) =>
              if user_context_id == "" ||
                !self.user_contexts.contains(user_context_id) {
                self.send_error(
                  request_id,
                  "no such user context",
                  "Unknown user context: " + user_context_id,
                )
                return None
              } else if !array_contains(target_user_contexts, user_context_id) {
                target_user_contexts.push(user_context_id)
              }
            _ => {
              self.send_error(
                request_id, "invalid argument", "userContexts entries must be strings",
              )
              return None
            }
          }
        }
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "userContexts must be an array",
      )
      return None
    }
    None => ()
  }

  if target_contexts.length() > 0 && target_user_contexts.length() > 0 {
    self.send_error(
      request_id, "invalid argument", "contexts and userContexts are mutually exclusive",
    )
    return None
  }

  if target_contexts.length() > 0 {
    for ctx_id in target_contexts {
      match override_value {
        Some(override_value) =>
          self.emulation_state.emulation_geolocation_by_context[ctx_id] = override_value
        None =>
          self.emulation_state.emulation_geolocation_by_context.remove(ctx_id)
      }
    }
    self.notify_geolocation_watchers()
    return Some(true)
  }

  if target_user_contexts.length() > 0 {
    for user_context_id in target_user_contexts {
      match override_value {
        Some(override_value) =>
          self.emulation_state.emulation_geolocation_by_user_context[user_context_id] = override_value
        None =>
          self.emulation_state.emulation_geolocation_by_user_context.remove(
            user_context_id,
          )
      }
    }
    self.notify_geolocation_watchers()
    return Some(true)
  }

  self.emulation_state.emulation_geolocation_global = override_value
  self.notify_geolocation_watchers()
  Some(true)
}

///|
fn BidiProtocol::try_handle_synthetic_geolocation_call(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  function_declaration : String,
  params : Json?,
  unwrap_result : Bool,
) -> Bool {
  if function_declaration.contains("navigator.geolocation.clearWatch(") {
    let watch_id = match @browser_domain.get_first_numeric_argument(params) {
      Some(value) => value.to_int()
      None => 0
    }
    self.clear_geolocation_watch(ctx_id, watch_id)
    self.send_script_undefined_response(request_id, realm_id, unwrap_result)
    return true
  }

  if function_declaration.contains("navigator.geolocation.watchPosition(") {
    let channel_name = match
      @browser_domain.extract_script_channel_name(params) {
      Some(channel_name) => channel_name
      None => return false
    }
    let notify_success = function_declaration.contains("result.coords.toJSON()")
    let notify_error = function_declaration.contains("error.code")
    if !notify_success && !notify_error {
      return false
    }
    let watch_id = self.register_geolocation_watch(
      ctx_id, realm_id, channel_name, notify_success, notify_error,
    )
    self.send_script_remote_value_response(
      request_id,
      realm_id,
      make_object({
        "type": Json::string("number"),
        "value": Json::number(watch_id.to_double()),
      }),
      unwrap_result,
    )
    return true
  }

  if !function_declaration.contains("navigator.geolocation.getCurrentPosition(") {
    return false
  }

  match self.resolve_effective_geolocation_error_code(ctx_id) {
    Some(code) =>
      self.send_script_remote_value_response(
        request_id,
        realm_id,
        @browser_domain.build_geolocation_error_remote_value(code),
        unwrap_result,
      )
    None => {
      let coordinates = self
        .resolve_effective_geolocation_coordinates(ctx_id)
        .unwrap_or(@browser_domain.default_geolocation_coordinates_json())
      self.send_script_remote_value_response(
        request_id,
        realm_id,
        @browser_domain.build_geolocation_coordinates_remote_value(coordinates),
        unwrap_result,
      )
    }
  }
  true
}