///|
/// Dispatch bluetooth domain.
fn BidiProtocol::dispatch_bluetooth(
  self : BidiProtocol,
  request : BidiRequest,
  action : String,
) -> Result[Unit, String] {
  match action {
    "simulateAdapter" =>
      match
        self.resolve_bluetooth_simulate_adapter(request.id, request.params) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "disableSimulation" =>
      match
        self.resolve_bluetooth_disable_simulation(request.id, request.params) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "simulatePreconnectedPeripheral" =>
      match
        self.resolve_bluetooth_simulate_preconnected_peripheral(
          request.id,
          request.params,
        ) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "handleRequestDevicePrompt" =>
      match
        self.resolve_bluetooth_handle_request_device_prompt(
          request.id,
          request.params,
        ) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "simulateGATTConnectionResponse" =>
      match
        self.resolve_bluetooth_simulate_gatt_connection_response(
          request.id,
          request.params,
        ) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "simulateGATTDisconnection" =>
      match
        self.resolve_bluetooth_simulate_gatt_disconnection(
          request.id,
          request.params,
        ) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "simulateService" =>
      match
        self.resolve_bluetooth_simulate_service(request.id, request.params) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "simulateCharacteristic" =>
      match
        self.resolve_bluetooth_simulate_characteristic(
          request.id,
          request.params,
        ) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "simulateCharacteristicResponse" =>
      match
        self.resolve_bluetooth_simulate_characteristic_response(
          request.id,
          request.params,
        ) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "simulateDescriptor" =>
      match
        self.resolve_bluetooth_simulate_descriptor(request.id, request.params) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    "simulateDescriptorResponse" =>
      match
        self.resolve_bluetooth_simulate_descriptor_response(
          request.id,
          request.params,
        ) {
        Some(_) => {
          self.send_success(request.id, Some(make_object({})))
          Ok(())
        }
        None => Ok(())
      }
    _ => {
      self.send_error(
        request.id,
        "unknown command",
        "Unknown method: " + request.method_name,
      )
      Ok(())
    }
  }
}

///|
fn BidiProtocol::resolve_bluetooth_simulate_adapter(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let ctx_id = match self.validate_input_context(request_id, params) {
    Some(ctx_id) => ctx_id
    None => return None
  }
  let state = match get_param_raw(params, "state") {
    Some(String(state)) =>
      if state == "absent" || state == "powered-off" || state == "powered-on" {
        state
      } else {
        self.send_error(request_id, "invalid argument", "state is invalid")
        return None
      }
    Some(_) => {
      self.send_error(request_id, "invalid argument", "state must be a string")
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing state")
      return None
    }
  }
  self.bluetooth_adapter_state_by_context[ctx_id] = state
  self.bluetooth_simulation_disabled_by_context.remove(ctx_id)
  Some(true)
}

///|
fn BidiProtocol::resolve_bluetooth_disable_simulation(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let ctx_id = match self.validate_input_context(request_id, params) {
    Some(ctx_id) => ctx_id
    None => return None
  }
  self.bluetooth_simulation_disabled_by_context[ctx_id] = true
  Some(true)
}

///|
fn BidiProtocol::resolve_bluetooth_simulate_preconnected_peripheral(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let ctx_id = match self.validate_input_context(request_id, params) {
    Some(ctx_id) => ctx_id
    None => return None
  }
  if self.bluetooth_simulation_disabled_by_context.get(ctx_id).unwrap_or(false) {
    self.send_error(
      request_id, "unknown error", "Bluetooth simulation is disabled for this context",
    )
    return None
  }
  let address = match get_param_raw(params, "address") {
    Some(String(address)) => address
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "address must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing address")
      return None
    }
  }
  let name = match get_param_raw(params, "name") {
    Some(String(name)) => name
    Some(_) => {
      self.send_error(request_id, "invalid argument", "name must be a string")
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing name")
      return None
    }
  }
  let manufacturer_data = match
    self.validate_bluetooth_manufacturer_data(request_id, params) {
    Some(data) => data
    None => return None
  }
  let known_service_uuids = match
    self.validate_bluetooth_known_service_uuids(request_id, params) {
    Some(uuids) => uuids
    None => return None
  }
  let device = make_object({
    "address": Json::string(address),
    "name": Json::string(name),
    "manufacturerData": Json::array(manufacturer_data),
    "knownServiceUuids": Json::array(known_service_uuids),
  })
  let devices = self.bluetooth_preconnected_devices_by_context
    .get(ctx_id)
    .unwrap_or([])
  self.bluetooth_preconnected_devices_by_context[ctx_id] = @browser_domain.upsert_bluetooth_device(
    devices, device, address,
  )
  Some(true)
}

///|
fn BidiProtocol::resolve_bluetooth_handle_request_device_prompt(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let ctx_id = match self.validate_input_context(request_id, params) {
    Some(ctx_id) => ctx_id
    None => return None
  }
  let prompt_id = match get_param_raw(params, "prompt") {
    Some(String(prompt_id)) => prompt_id
    Some(_) => {
      self.send_error(request_id, "invalid argument", "prompt must be a string")
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing prompt")
      return None
    }
  }
  let accept = match get_param_raw(params, "accept") {
    Some(True) => true
    Some(False) => false
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "accept must be a boolean",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing accept")
      return None
    }
  }
  match self.bluetooth_pending_prompt_id.get(ctx_id) {
    Some(current_prompt_id) if current_prompt_id == prompt_id => ()
    _ => {
      self.send_error(
        request_id, "invalid argument", "Unknown bluetooth prompt",
      )
      return None
    }
  }
  let selected_address = match get_param_raw(params, "device") {
    Some(String(device_id)) => device_id
    Some(_) => {
      self.send_error(request_id, "invalid argument", "device must be a string")
      return None
    }
    None =>
      self.bluetooth_pending_prompt_device_address.get(ctx_id).unwrap_or("")
  }
  let selected_name = self.bluetooth_pending_prompt_device_name
    .get(ctx_id)
    .unwrap_or("")
  let pending_request_id = self.bluetooth_pending_prompt_request.get(ctx_id)
  let pending_realm = self.bluetooth_pending_prompt_realm.get(ctx_id)
  let unwrap_result = self.bluetooth_pending_prompt_unwrap_result
    .get(ctx_id)
    .unwrap_or(false)
  self.bluetooth_pending_prompt_request.remove(ctx_id)
  self.bluetooth_pending_prompt_realm.remove(ctx_id)
  self.bluetooth_pending_prompt_unwrap_result.remove(ctx_id)
  self.bluetooth_pending_prompt_id.remove(ctx_id)
  self.bluetooth_pending_prompt_device_address.remove(ctx_id)
  self.bluetooth_pending_prompt_device_name.remove(ctx_id)

  match (pending_request_id, pending_realm) {
    (Some(pending_request_id), Some(realm_id)) =>
      if accept {
        let device = self
          .find_bluetooth_device_descriptor(ctx_id, selected_address)
          .unwrap_or(
            make_object({
              "address": Json::string(selected_address),
              "name": Json::string(selected_name),
              "manufacturerData": Json::array([]),
              "knownServiceUuids": Json::array([]),
            }),
          )
        let granted_devices = self.bluetooth_granted_devices_by_context
          .get(ctx_id)
          .unwrap_or([])
        self.bluetooth_granted_devices_by_context[ctx_id] = @browser_domain.upsert_bluetooth_device(
          granted_devices, device, selected_address,
        )
        self.send_script_remote_value_response(
          pending_request_id,
          realm_id,
          @browser_domain.make_bluetooth_requested_device_remote_value(
            selected_address, selected_name,
          ),
          unwrap_result,
        )
      } else {
        self.send_error(
          pending_request_id, "unknown error", "Bluetooth request device canceled",
        )
      }
    _ => ()
  }
  Some(true)
}

///|
fn BidiProtocol::resolve_bluetooth_simulate_gatt_connection_response(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let ctx_id = match self.validate_input_context(request_id, params) {
    Some(ctx_id) => ctx_id
    None => return None
  }
  let address = match get_param_raw(params, "address") {
    Some(String(address)) => address
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "address must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing address")
      return None
    }
  }
  let code = match get_param_raw(params, "code") {
    Some(Number(code, ..)) if code.floor() == code => code.to_int()
    Some(_) => {
      self.send_error(request_id, "invalid argument", "code must be an integer")
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing code")
      return None
    }
  }
  let connected = code == 0
  self.bluetooth_gatt_connected_by_scope[@browser_domain.bluetooth_scope_key(
    ctx_id, address,
  )] = connected
  let pending_request_id = self.bluetooth_pending_gatt_request.get(ctx_id)
  let pending_realm = self.bluetooth_pending_gatt_realm.get(ctx_id)
  let unwrap_result = self.bluetooth_pending_gatt_unwrap_result
    .get(ctx_id)
    .unwrap_or(false)
  let pending_address = self.bluetooth_pending_gatt_address
    .get(ctx_id)
    .unwrap_or("")
  let response_kind = self.bluetooth_pending_gatt_response_kind
    .get(ctx_id)
    .unwrap_or("undefined")
  self.bluetooth_pending_gatt_request.remove(ctx_id)
  self.bluetooth_pending_gatt_realm.remove(ctx_id)
  self.bluetooth_pending_gatt_unwrap_result.remove(ctx_id)
  self.bluetooth_pending_gatt_address.remove(ctx_id)
  self.bluetooth_pending_gatt_response_kind.remove(ctx_id)
  match (pending_request_id, pending_realm) {
    (Some(pending_request_id), Some(realm_id)) if pending_address == address =>
      if response_kind == "connected" {
        self.send_script_remote_value_response(
          pending_request_id,
          realm_id,
          make_object({
            "type": Json::string("boolean"),
            "value": Json::boolean(connected),
          }),
          unwrap_result,
        )
      } else {
        self.send_script_undefined_response(
          pending_request_id, realm_id, unwrap_result,
        )
      }
    _ => ()
  }
  Some(true)
}

///|
fn BidiProtocol::resolve_bluetooth_simulate_gatt_disconnection(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let ctx_id = match self.validate_input_context(request_id, params) {
    Some(ctx_id) => ctx_id
    None => return None
  }
  let address = match get_param_raw(params, "address") {
    Some(String(address)) => address
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "address must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing address")
      return None
    }
  }
  self.bluetooth_gatt_connected_by_scope[@browser_domain.bluetooth_scope_key(
    ctx_id, address,
  )] = false
  Some(true)
}

///|
fn BidiProtocol::resolve_bluetooth_simulate_service(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let ctx_id = match self.validate_input_context(request_id, params) {
    Some(ctx_id) => ctx_id
    None => return None
  }
  let address = match get_param_raw(params, "address") {
    Some(String(address)) => address
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "address must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing address")
      return None
    }
  }
  if !self.has_bluetooth_device_descriptor(ctx_id, address) {
    self.send_error(
      request_id, "invalid argument", "Unknown bluetooth device address",
    )
    return None
  }
  let uuid = match get_param_raw(params, "uuid") {
    Some(String(uuid)) => uuid
    Some(_) => {
      self.send_error(request_id, "invalid argument", "uuid must be a string")
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing uuid")
      return None
    }
  }
  let action_type = match get_param_raw(params, "type") {
    Some(String(action_type)) =>
      if action_type == "add" || action_type == "remove" {
        action_type
      } else {
        self.send_error(request_id, "invalid argument", "type is invalid")
        return None
      }
    Some(_) => {
      self.send_error(request_id, "invalid argument", "type must be a string")
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing type")
      return None
    }
  }
  let scope_key = @browser_domain.bluetooth_scope_key(ctx_id, address)
  let services = self.bluetooth_services_by_scope.get(scope_key).unwrap_or([])
  if action_type == "add" {
    if array_contains(services, uuid) {
      self.send_error(request_id, "invalid argument", "Service already exists")
      return None
    }
    let next_services : Array[String] = []
    for service_uuid in services {
      next_services.push(service_uuid)
    }
    next_services.push(uuid)
    self.bluetooth_services_by_scope[scope_key] = next_services
    return Some(true)
  }
  if !array_contains(services, uuid) {
    self.send_error(request_id, "invalid argument", "Service does not exist")
    return None
  }
  let next_services : Array[String] = []
  for service_uuid in services {
    if service_uuid != uuid {
      next_services.push(service_uuid)
    }
  }
  self.bluetooth_services_by_scope[scope_key] = next_services
  Some(true)
}

///|
fn BidiProtocol::resolve_bluetooth_simulate_characteristic(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let ctx_id = match self.validate_input_context(request_id, params) {
    Some(ctx_id) => ctx_id
    None => return None
  }
  let address = match get_param_raw(params, "address") {
    Some(String(address)) => address
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "address must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing address")
      return None
    }
  }
  if !self.has_bluetooth_device_descriptor(ctx_id, address) {
    self.send_error(
      request_id, "invalid argument", "Unknown bluetooth device address",
    )
    return None
  }
  let service_uuid = match
    get_param_raw_with_alias(params, "serviceUuid", "service_uuid") {
    Some(String(service_uuid)) => service_uuid
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "serviceUuid must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing serviceUuid")
      return None
    }
  }
  let service_scope_key = @browser_domain.bluetooth_scope_key(ctx_id, address)
  let services = self.bluetooth_services_by_scope
    .get(service_scope_key)
    .unwrap_or([])
  if !array_contains(services, service_uuid) {
    self.send_error(request_id, "invalid argument", "Unknown service uuid")
    return None
  }
  let characteristic_uuid = match
    get_param_raw_with_alias(
      params, "characteristicUuid", "characteristic_uuid",
    ) {
    Some(String(characteristic_uuid)) => characteristic_uuid
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "characteristicUuid must be a string",
      )
      return None
    }
    None => {
      self.send_error(
        request_id, "invalid argument", "Missing characteristicUuid",
      )
      return None
    }
  }
  let action_type = match get_param_raw(params, "type") {
    Some(String(action_type)) =>
      if action_type == "add" || action_type == "remove" {
        action_type
      } else {
        self.send_error(request_id, "invalid argument", "type is invalid")
        return None
      }
    Some(_) => {
      self.send_error(request_id, "invalid argument", "type must be a string")
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing type")
      return None
    }
  }
  let characteristic_scope_key = @browser_domain.bluetooth_characteristic_scope_key(
    ctx_id, address, service_uuid, characteristic_uuid,
  )
  let existing = self.bluetooth_characteristics_by_scope.get(
    characteristic_scope_key,
  )
  if action_type == "add" {
    let properties = match
      self.validate_bluetooth_characteristic_properties(request_id, params) {
      Some(properties) => properties
      None => return None
    }
    if existing != None {
      self.send_error(
        request_id, "invalid argument", "Characteristic already exists",
      )
      return None
    }
    self.bluetooth_characteristics_by_scope[characteristic_scope_key] = properties
    return Some(true)
  }
  match
    get_param_raw_with_alias(
      params, "characteristicProperties", "characteristic_properties",
    ) {
    Some(Null) | None => ()
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "characteristicProperties must be absent for remove",
      )
      return None
    }
  }
  if existing == None {
    self.send_error(
      request_id, "invalid argument", "Characteristic does not exist",
    )
    return None
  }
  self.bluetooth_characteristics_by_scope.remove(characteristic_scope_key)
  Some(true)
}

///|
fn BidiProtocol::resolve_bluetooth_simulate_characteristic_response(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let ctx_id = match self.validate_input_context(request_id, params) {
    Some(ctx_id) => ctx_id
    None => return None
  }
  let address = match get_param_raw(params, "address") {
    Some(String(address)) => address
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "address must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing address")
      return None
    }
  }
  if !self.has_bluetooth_device_descriptor(ctx_id, address) {
    self.send_error(
      request_id, "invalid argument", "Unknown bluetooth device address",
    )
    return None
  }
  let service_uuid = match
    get_param_raw_with_alias(params, "serviceUuid", "service_uuid") {
    Some(String(service_uuid)) => service_uuid
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "serviceUuid must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing serviceUuid")
      return None
    }
  }
  let services = self.bluetooth_services_by_scope
    .get(@browser_domain.bluetooth_scope_key(ctx_id, address))
    .unwrap_or([])
  if !array_contains(services, service_uuid) {
    self.send_error(request_id, "invalid argument", "Unknown service uuid")
    return None
  }
  let characteristic_uuid = match
    get_param_raw_with_alias(
      params, "characteristicUuid", "characteristic_uuid",
    ) {
    Some(String(characteristic_uuid)) => characteristic_uuid
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "characteristicUuid must be a string",
      )
      return None
    }
    None => {
      self.send_error(
        request_id, "invalid argument", "Missing characteristicUuid",
      )
      return None
    }
  }
  let characteristic_scope_key = @browser_domain.bluetooth_characteristic_scope_key(
    ctx_id, address, service_uuid, characteristic_uuid,
  )
  if self.bluetooth_characteristics_by_scope.get(characteristic_scope_key) ==
    None {
    self.send_error(
      request_id, "invalid argument", "Unknown characteristic uuid",
    )
    return None
  }
  let response_type = match get_param_raw(params, "type") {
    Some(String(response_type)) =>
      if response_type == "write" ||
        response_type == "read" ||
        response_type == "subscribe-to-notifications" ||
        response_type == "unsubscribe-from-notifications" {
        response_type
      } else {
        self.send_error(request_id, "invalid argument", "type is invalid")
        return None
      }
    Some(_) => {
      self.send_error(request_id, "invalid argument", "type must be a string")
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing type")
      return None
    }
  }
  let code = match get_param_raw(params, "code") {
    Some(Number(code, ..)) if code.floor() == code => code.to_int()
    Some(_) => {
      self.send_error(request_id, "invalid argument", "code must be an integer")
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing code")
      return None
    }
  }
  let data = match
    self.validate_bluetooth_response_data(
      request_id,
      params,
      response_type == "read",
    ) {
    Some(data) => data
    None => return None
  }
  let pending_request_id = self.bluetooth_pending_characteristic_request.get(
    ctx_id,
  )
  let pending_realm = self.bluetooth_pending_characteristic_realm.get(ctx_id)
  let unwrap_result = self.bluetooth_pending_characteristic_unwrap_result
    .get(ctx_id)
    .unwrap_or(false)
  let pending_address = self.bluetooth_pending_characteristic_address
    .get(ctx_id)
    .unwrap_or("")
  let pending_service_uuid = self.bluetooth_pending_characteristic_service_uuid
    .get(ctx_id)
    .unwrap_or("")
  let pending_characteristic_uuid = self.bluetooth_pending_characteristic_uuid
    .get(ctx_id)
    .unwrap_or("")
  let pending_type = self.bluetooth_pending_characteristic_type
    .get(ctx_id)
    .unwrap_or("")
  self.clear_pending_bluetooth_characteristic(ctx_id)
  match (pending_request_id, pending_realm) {
    (Some(pending_request_id), Some(realm_id)) =>
      if pending_address != address ||
        pending_service_uuid != service_uuid ||
        pending_characteristic_uuid != characteristic_uuid ||
        pending_type != response_type {
        self.send_error(
          request_id, "invalid argument", "No pending characteristic response",
        )
        None
      } else {
        if code == 0 {
          if response_type == "read" {
            self.send_script_remote_value_response(
              pending_request_id,
              realm_id,
              @browser_domain.make_bluetooth_byte_remote_value(data),
              unwrap_result,
            )
          } else {
            self.send_script_undefined_response(
              pending_request_id, realm_id, unwrap_result,
            )
          }
        } else {
          self.send_error(
            pending_request_id, "unknown error", "Bluetooth characteristic request failed",
          )
        }
        Some(true)
      }
    _ => {
      self.send_error(
        request_id, "invalid argument", "No pending characteristic response",
      )
      None
    }
  }
}

///|
fn BidiProtocol::resolve_bluetooth_simulate_descriptor(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let ctx_id = match self.validate_input_context(request_id, params) {
    Some(ctx_id) => ctx_id
    None => return None
  }
  let address = match get_param_raw(params, "address") {
    Some(String(address)) => address
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "address must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing address")
      return None
    }
  }
  if !self.has_bluetooth_device_descriptor(ctx_id, address) {
    self.send_error(
      request_id, "invalid argument", "Unknown bluetooth device address",
    )
    return None
  }
  let service_uuid = match
    get_param_raw_with_alias(params, "serviceUuid", "service_uuid") {
    Some(String(service_uuid)) => service_uuid
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "serviceUuid must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing serviceUuid")
      return None
    }
  }
  let services = self.bluetooth_services_by_scope
    .get(@browser_domain.bluetooth_scope_key(ctx_id, address))
    .unwrap_or([])
  if !array_contains(services, service_uuid) {
    self.send_error(request_id, "invalid argument", "Unknown service uuid")
    return None
  }
  let characteristic_uuid = match
    get_param_raw_with_alias(
      params, "characteristicUuid", "characteristic_uuid",
    ) {
    Some(String(characteristic_uuid)) => characteristic_uuid
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "characteristicUuid must be a string",
      )
      return None
    }
    None => {
      self.send_error(
        request_id, "invalid argument", "Missing characteristicUuid",
      )
      return None
    }
  }
  let characteristic_scope_key = @browser_domain.bluetooth_characteristic_scope_key(
    ctx_id, address, service_uuid, characteristic_uuid,
  )
  if self.bluetooth_characteristics_by_scope.get(characteristic_scope_key) ==
    None {
    self.send_error(
      request_id, "invalid argument", "Unknown characteristic uuid",
    )
    return None
  }
  let descriptor_uuid = match
    get_param_raw_with_alias(params, "descriptorUuid", "descriptor_uuid") {
    Some(String(descriptor_uuid)) => descriptor_uuid
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "descriptorUuid must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing descriptorUuid")
      return None
    }
  }
  let action_type = match get_param_raw(params, "type") {
    Some(String(action_type)) =>
      if action_type == "add" || action_type == "remove" {
        action_type
      } else {
        self.send_error(request_id, "invalid argument", "type is invalid")
        return None
      }
    Some(_) => {
      self.send_error(request_id, "invalid argument", "type must be a string")
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing type")
      return None
    }
  }
  let descriptor_scope_key = @browser_domain.bluetooth_descriptor_scope_key(
    ctx_id, address, service_uuid, characteristic_uuid, descriptor_uuid,
  )
  let existing = self.bluetooth_descriptors_by_scope.get(descriptor_scope_key)
  if action_type == "add" {
    if existing != None {
      self.send_error(
        request_id, "invalid argument", "Descriptor already exists",
      )
      return None
    }
    self.bluetooth_descriptors_by_scope[descriptor_scope_key] = true
    return Some(true)
  }
  if existing == None {
    self.send_error(request_id, "invalid argument", "Descriptor does not exist")
    return None
  }
  self.bluetooth_descriptors_by_scope.remove(descriptor_scope_key)
  Some(true)
}

///|
fn BidiProtocol::resolve_bluetooth_simulate_descriptor_response(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Bool? {
  let ctx_id = match self.validate_input_context(request_id, params) {
    Some(ctx_id) => ctx_id
    None => return None
  }
  let address = match get_param_raw(params, "address") {
    Some(String(address)) => address
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "address must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing address")
      return None
    }
  }
  if !self.has_bluetooth_device_descriptor(ctx_id, address) {
    self.send_error(
      request_id, "invalid argument", "Unknown bluetooth device address",
    )
    return None
  }
  let service_uuid = match
    get_param_raw_with_alias(params, "serviceUuid", "service_uuid") {
    Some(String(service_uuid)) => service_uuid
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "serviceUuid must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing serviceUuid")
      return None
    }
  }
  let services = self.bluetooth_services_by_scope
    .get(@browser_domain.bluetooth_scope_key(ctx_id, address))
    .unwrap_or([])
  if !array_contains(services, service_uuid) {
    self.send_error(request_id, "invalid argument", "Unknown service uuid")
    return None
  }
  let characteristic_uuid = match
    get_param_raw_with_alias(
      params, "characteristicUuid", "characteristic_uuid",
    ) {
    Some(String(characteristic_uuid)) => characteristic_uuid
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "characteristicUuid must be a string",
      )
      return None
    }
    None => {
      self.send_error(
        request_id, "invalid argument", "Missing characteristicUuid",
      )
      return None
    }
  }
  let descriptor_uuid = match
    get_param_raw_with_alias(params, "descriptorUuid", "descriptor_uuid") {
    Some(String(descriptor_uuid)) => descriptor_uuid
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "descriptorUuid must be a string",
      )
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing descriptorUuid")
      return None
    }
  }
  let descriptor_scope_key = @browser_domain.bluetooth_descriptor_scope_key(
    ctx_id, address, service_uuid, characteristic_uuid, descriptor_uuid,
  )
  if self.bluetooth_descriptors_by_scope.get(descriptor_scope_key) == None {
    self.send_error(request_id, "invalid argument", "Unknown descriptor uuid")
    return None
  }
  let response_type = match get_param_raw(params, "type") {
    Some(String(response_type)) =>
      if response_type == "write" || response_type == "read" {
        response_type
      } else {
        self.send_error(request_id, "invalid argument", "type is invalid")
        return None
      }
    Some(_) => {
      self.send_error(request_id, "invalid argument", "type must be a string")
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing type")
      return None
    }
  }
  let code = match get_param_raw(params, "code") {
    Some(Number(code, ..)) if code.floor() == code => code.to_int()
    Some(_) => {
      self.send_error(request_id, "invalid argument", "code must be an integer")
      return None
    }
    None => {
      self.send_error(request_id, "invalid argument", "Missing code")
      return None
    }
  }
  let data = match
    self.validate_bluetooth_response_data(
      request_id,
      params,
      response_type == "read",
    ) {
    Some(data) => data
    None => return None
  }
  let pending_request_id = self.bluetooth_pending_descriptor_request.get(ctx_id)
  let pending_realm = self.bluetooth_pending_descriptor_realm.get(ctx_id)
  let unwrap_result = self.bluetooth_pending_descriptor_unwrap_result
    .get(ctx_id)
    .unwrap_or(false)
  let pending_address = self.bluetooth_pending_descriptor_address
    .get(ctx_id)
    .unwrap_or("")
  let pending_service_uuid = self.bluetooth_pending_descriptor_service_uuid
    .get(ctx_id)
    .unwrap_or("")
  let pending_characteristic_uuid = self.bluetooth_pending_descriptor_characteristic_uuid
    .get(ctx_id)
    .unwrap_or("")
  let pending_descriptor_uuid = self.bluetooth_pending_descriptor_uuid
    .get(ctx_id)
    .unwrap_or("")
  let pending_type = self.bluetooth_pending_descriptor_type
    .get(ctx_id)
    .unwrap_or("")
  self.clear_pending_bluetooth_descriptor(ctx_id)
  match (pending_request_id, pending_realm) {
    (Some(pending_request_id), Some(realm_id)) =>
      if pending_address != address ||
        pending_service_uuid != service_uuid ||
        pending_characteristic_uuid != characteristic_uuid ||
        pending_descriptor_uuid != descriptor_uuid ||
        pending_type != response_type {
        self.send_error(
          request_id, "invalid argument", "No pending descriptor response",
        )
        None
      } else {
        if code == 0 {
          if response_type == "read" {
            self.send_script_remote_value_response(
              pending_request_id,
              realm_id,
              @browser_domain.make_bluetooth_byte_remote_value(data),
              unwrap_result,
            )
          } else {
            self.send_script_undefined_response(
              pending_request_id, realm_id, unwrap_result,
            )
          }
        } else {
          self.send_error(
            pending_request_id, "unknown error", "Bluetooth descriptor request failed",
          )
        }
        Some(true)
      }
    _ => {
      self.send_error(
        request_id, "invalid argument", "No pending descriptor response",
      )
      None
    }
  }
}

///|
fn BidiProtocol::try_handle_synthetic_bluetooth_availability_eval(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  expression : String,
  unwrap_result : Bool,
) -> Bool {
  if expression.trim().to_owned() != "navigator.bluetooth.getAvailability()" {
    return false
  }
  let adapter_state = self.bluetooth_adapter_state_by_context
    .get(ctx_id)
    .unwrap_or("absent")
  let simulation_disabled = self.bluetooth_simulation_disabled_by_context
    .get(ctx_id)
    .unwrap_or(false)
  let available = !simulation_disabled &&
    (adapter_state == "powered-off" || adapter_state == "powered-on")
  self.send_script_remote_value_response(
    request_id,
    realm_id,
    make_object({
      "type": Json::string("boolean"),
      "value": Json::boolean(available),
    }),
    unwrap_result,
  )
  true
}

///|
fn BidiProtocol::try_handle_synthetic_bluetooth_request_device_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.bluetooth.requestDevice(") {
    return false
  }
  let devices = self.bluetooth_preconnected_devices_by_context
    .get(ctx_id)
    .unwrap_or([])
  if devices.length() == 0 {
    self.send_error(
      request_id, "unknown error", "No simulated bluetooth device is available",
    )
    return true
  }
  let expected_name = @browser_domain.extract_first_bluetooth_string_argument(
    params,
  )
  let selected = @browser_domain.find_bluetooth_device_for_name(
    devices, expected_name,
  )
  let address = @browser_domain.bluetooth_device_field_string(
    selected, "address",
  ).unwrap_or("")
  let name = @browser_domain.bluetooth_device_field_string(selected, "name").unwrap_or(
    "",
  )
  if address == "" {
    self.send_error(
      request_id, "unknown error", "Simulated bluetooth device address is missing",
    )
    return true
  }
  let prompt_id = "bluetooth-prompt-" +
    self.next_bluetooth_prompt_id.to_string()
  self.next_bluetooth_prompt_id += 1
  self.bluetooth_pending_prompt_request[ctx_id] = request_id
  self.bluetooth_pending_prompt_realm[ctx_id] = realm_id
  self.bluetooth_pending_prompt_unwrap_result[ctx_id] = unwrap_result
  self.bluetooth_pending_prompt_id[ctx_id] = prompt_id
  self.bluetooth_pending_prompt_device_address[ctx_id] = address
  self.bluetooth_pending_prompt_device_name[ctx_id] = name
  self.emit_bluetooth_request_device_prompt_updated(ctx_id, prompt_id, address)
  true
}

///|
fn BidiProtocol::try_handle_synthetic_bluetooth_gatt_call(
  self : BidiProtocol,
  request_id : Int,
  ctx_id : String,
  realm_id : String,
  function_declaration : String,
  unwrap_result : Bool,
) -> Bool {
  if !function_declaration.contains("navigator.bluetooth.getDevices()") {
    return false
  }
  let granted_devices = self.bluetooth_granted_devices_by_context
    .get(ctx_id)
    .unwrap_or([])
  if granted_devices.length() == 0 {
    self.send_error(
      request_id, "unknown error", "No granted bluetooth device is available",
    )
    return true
  }
  let address = @browser_domain.bluetooth_device_field_string(
    granted_devices[0],
    "address",
  ).unwrap_or("")
  if address == "" {
    self.send_error(
      request_id, "unknown error", "Granted bluetooth device address is missing",
    )
    return true
  }
  let trimmed = function_declaration.trim().to_owned()
  if trimmed.contains("await device.gatt.connect();") {
    let response_kind = if trimmed.contains("return device.gatt.connected;") {
      "connected"
    } else {
      "undefined"
    }
    self.bluetooth_pending_gatt_request[ctx_id] = request_id
    self.bluetooth_pending_gatt_realm[ctx_id] = realm_id
    self.bluetooth_pending_gatt_unwrap_result[ctx_id] = unwrap_result
    self.bluetooth_pending_gatt_address[ctx_id] = address
    self.bluetooth_pending_gatt_response_kind[ctx_id] = response_kind
    self.emit_bluetooth_gatt_connection_attempted(ctx_id, address)
    return true
  }
  if trimmed.contains("return device.gatt.connected;") {
    let connected = self.bluetooth_gatt_connected_by_scope
      .get(@browser_domain.bluetooth_scope_key(ctx_id, address))
      .unwrap_or(false)
    self.send_script_remote_value_response(
      request_id,
      realm_id,
      make_object({
        "type": Json::string("boolean"),
        "value": Json::boolean(connected),
      }),
      unwrap_result,
    )
    return true
  }
  if trimmed.contains("device.gatt.getPrimaryServices()") &&
    trimmed.contains("return services.map(s => s.uuid)") {
    let services = self.bluetooth_services_by_scope
      .get(@browser_domain.bluetooth_scope_key(ctx_id, address))
      .unwrap_or([])
    let items : Array[Json] = []
    for service_uuid in services {
      items.push(
        make_object({
          "type": Json::string("string"),
          "value": Json::string(service_uuid),
        }),
      )
    }
    self.send_script_remote_value_response(
      request_id,
      realm_id,
      make_object({ "type": Json::string("array"), "value": Json::array(items) }),
      unwrap_result,
    )
    return true
  }
  if trimmed.contains("device.gatt.getPrimaryService(") &&
    trimmed.contains("getCharacteristics()") &&
    trimmed.contains("return characteristics.map(c => c.uuid)") {
    let service_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getPrimaryService",
    )
    let prefix = @browser_domain.bluetooth_characteristic_prefix_key(
      ctx_id, address, service_uuid,
    )
    let items : Array[Json] = []
    for key, properties in self.bluetooth_characteristics_by_scope {
      ignore(properties)
      if key.has_prefix(prefix) {
        let characteristic_uuid = key.unsafe_substring(
          start=prefix.length(),
          end=key.length(),
        )
        if characteristic_uuid != "" {
          items.push(
            make_object({
              "type": Json::string("string"),
              "value": Json::string(characteristic_uuid),
            }),
          )
        }
      }
    }
    self.send_script_remote_value_response(
      request_id,
      realm_id,
      make_object({ "type": Json::string("array"), "value": Json::array(items) }),
      unwrap_result,
    )
    return true
  }
  if trimmed.contains("device.gatt.getPrimaryService(") &&
    trimmed.contains("characteristic.getDescriptors()") &&
    trimmed.contains("return descriptors.map(d => d.uuid)") {
    let service_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getPrimaryService",
    )
    let characteristic_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getCharacteristic",
    )
    let prefix = @browser_domain.bluetooth_descriptor_prefix_key(
      ctx_id, address, service_uuid, characteristic_uuid,
    )
    let items : Array[Json] = []
    for key, exists in self.bluetooth_descriptors_by_scope {
      if exists && key.has_prefix(prefix) {
        let descriptor_uuid = key.unsafe_substring(
          start=prefix.length(),
          end=key.length(),
        )
        if descriptor_uuid != "" {
          items.push(
            make_object({
              "type": Json::string("string"),
              "value": Json::string(descriptor_uuid),
            }),
          )
        }
      }
    }
    self.send_script_remote_value_response(
      request_id,
      realm_id,
      make_object({ "type": Json::string("array"), "value": Json::array(items) }),
      unwrap_result,
    )
    return true
  }
  if trimmed.contains("device.gatt.getPrimaryService(") &&
    trimmed.contains("getCharacteristic(") &&
    trimmed.contains("writeValueWithoutResponse(") {
    let service_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getPrimaryService",
    )
    let characteristic_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getCharacteristic",
    )
    let data = @browser_domain.extract_bluetooth_uint8_array_data(trimmed)
    self.bluetooth_pending_characteristic_request[ctx_id] = request_id
    self.bluetooth_pending_characteristic_realm[ctx_id] = realm_id
    self.bluetooth_pending_characteristic_unwrap_result[ctx_id] = unwrap_result
    self.bluetooth_pending_characteristic_address[ctx_id] = address
    self.bluetooth_pending_characteristic_service_uuid[ctx_id] = service_uuid
    self.bluetooth_pending_characteristic_uuid[ctx_id] = characteristic_uuid
    self.bluetooth_pending_characteristic_type[ctx_id] = "write"
    self.emit_bluetooth_characteristic_event_generated(
      ctx_id,
      address,
      service_uuid,
      characteristic_uuid,
      "write-without-response",
      Some(Json::array(data)),
    )
    return true
  }
  if trimmed.contains("device.gatt.getPrimaryService(") &&
    trimmed.contains("getCharacteristic(") &&
    trimmed.contains("writeValueWithResponse(") {
    let service_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getPrimaryService",
    )
    let characteristic_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getCharacteristic",
    )
    let data = @browser_domain.extract_bluetooth_uint8_array_data(trimmed)
    self.bluetooth_pending_characteristic_request[ctx_id] = request_id
    self.bluetooth_pending_characteristic_realm[ctx_id] = realm_id
    self.bluetooth_pending_characteristic_unwrap_result[ctx_id] = unwrap_result
    self.bluetooth_pending_characteristic_address[ctx_id] = address
    self.bluetooth_pending_characteristic_service_uuid[ctx_id] = service_uuid
    self.bluetooth_pending_characteristic_uuid[ctx_id] = characteristic_uuid
    self.bluetooth_pending_characteristic_type[ctx_id] = "write"
    self.emit_bluetooth_characteristic_event_generated(
      ctx_id,
      address,
      service_uuid,
      characteristic_uuid,
      "write-with-response",
      Some(Json::array(data)),
    )
    return true
  }
  if trimmed.contains("device.gatt.getPrimaryService(") &&
    trimmed.contains("getCharacteristic(") &&
    trimmed.contains("const value = await characteristic.readValue();") {
    let service_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getPrimaryService",
    )
    let characteristic_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getCharacteristic",
    )
    self.bluetooth_pending_characteristic_request[ctx_id] = request_id
    self.bluetooth_pending_characteristic_realm[ctx_id] = realm_id
    self.bluetooth_pending_characteristic_unwrap_result[ctx_id] = unwrap_result
    self.bluetooth_pending_characteristic_address[ctx_id] = address
    self.bluetooth_pending_characteristic_service_uuid[ctx_id] = service_uuid
    self.bluetooth_pending_characteristic_uuid[ctx_id] = characteristic_uuid
    self.bluetooth_pending_characteristic_type[ctx_id] = "read"
    self.emit_bluetooth_characteristic_event_generated(
      ctx_id,
      address,
      service_uuid,
      characteristic_uuid,
      "read",
      None,
    )
    return true
  }
  if trimmed.contains("device.gatt.getPrimaryService(") &&
    trimmed.contains("getCharacteristic(") &&
    trimmed.contains("startNotifications();") {
    let service_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getPrimaryService",
    )
    let characteristic_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getCharacteristic",
    )
    self.bluetooth_pending_characteristic_request[ctx_id] = request_id
    self.bluetooth_pending_characteristic_realm[ctx_id] = realm_id
    self.bluetooth_pending_characteristic_unwrap_result[ctx_id] = unwrap_result
    self.bluetooth_pending_characteristic_address[ctx_id] = address
    self.bluetooth_pending_characteristic_service_uuid[ctx_id] = service_uuid
    self.bluetooth_pending_characteristic_uuid[ctx_id] = characteristic_uuid
    self.bluetooth_pending_characteristic_type[ctx_id] = "subscribe-to-notifications"
    self.emit_bluetooth_characteristic_event_generated(
      ctx_id,
      address,
      service_uuid,
      characteristic_uuid,
      "subscribe-to-notifications",
      None,
    )
    return true
  }
  if trimmed.contains("device.gatt.getPrimaryService(") &&
    trimmed.contains("getCharacteristic(") &&
    trimmed.contains("stopNotifications();") {
    let service_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getPrimaryService",
    )
    let characteristic_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getCharacteristic",
    )
    self.bluetooth_pending_characteristic_request[ctx_id] = request_id
    self.bluetooth_pending_characteristic_realm[ctx_id] = realm_id
    self.bluetooth_pending_characteristic_unwrap_result[ctx_id] = unwrap_result
    self.bluetooth_pending_characteristic_address[ctx_id] = address
    self.bluetooth_pending_characteristic_service_uuid[ctx_id] = service_uuid
    self.bluetooth_pending_characteristic_uuid[ctx_id] = characteristic_uuid
    self.bluetooth_pending_characteristic_type[ctx_id] = "unsubscribe-from-notifications"
    self.emit_bluetooth_characteristic_event_generated(
      ctx_id,
      address,
      service_uuid,
      characteristic_uuid,
      "unsubscribe-from-notifications",
      None,
    )
    return true
  }
  if trimmed.contains("device.gatt.getPrimaryService(") &&
    trimmed.contains("getDescriptor(") &&
    trimmed.contains("descriptor.writeValue(") {
    let service_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getPrimaryService",
    )
    let characteristic_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getCharacteristic",
    )
    let descriptor_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getDescriptor",
    )
    let data = @browser_domain.extract_bluetooth_uint8_array_data(trimmed)
    self.bluetooth_pending_descriptor_request[ctx_id] = request_id
    self.bluetooth_pending_descriptor_realm[ctx_id] = realm_id
    self.bluetooth_pending_descriptor_unwrap_result[ctx_id] = unwrap_result
    self.bluetooth_pending_descriptor_address[ctx_id] = address
    self.bluetooth_pending_descriptor_service_uuid[ctx_id] = service_uuid
    self.bluetooth_pending_descriptor_characteristic_uuid[ctx_id] = characteristic_uuid
    self.bluetooth_pending_descriptor_uuid[ctx_id] = descriptor_uuid
    self.bluetooth_pending_descriptor_type[ctx_id] = "write"
    self.emit_bluetooth_descriptor_event_generated(
      ctx_id,
      address,
      service_uuid,
      characteristic_uuid,
      descriptor_uuid,
      "write",
      Some(Json::array(data)),
    )
    return true
  }
  if trimmed.contains("device.gatt.getPrimaryService(") &&
    trimmed.contains("getDescriptor(") &&
    trimmed.contains("const value = await descriptor.readValue();") {
    let service_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getPrimaryService",
    )
    let characteristic_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getCharacteristic",
    )
    let descriptor_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getDescriptor",
    )
    self.bluetooth_pending_descriptor_request[ctx_id] = request_id
    self.bluetooth_pending_descriptor_realm[ctx_id] = realm_id
    self.bluetooth_pending_descriptor_unwrap_result[ctx_id] = unwrap_result
    self.bluetooth_pending_descriptor_address[ctx_id] = address
    self.bluetooth_pending_descriptor_service_uuid[ctx_id] = service_uuid
    self.bluetooth_pending_descriptor_characteristic_uuid[ctx_id] = characteristic_uuid
    self.bluetooth_pending_descriptor_uuid[ctx_id] = descriptor_uuid
    self.bluetooth_pending_descriptor_type[ctx_id] = "read"
    self.emit_bluetooth_descriptor_event_generated(
      ctx_id,
      address,
      service_uuid,
      characteristic_uuid,
      descriptor_uuid,
      "read",
      None,
    )
    return true
  }
  if trimmed.contains("device.gatt.getPrimaryService(") &&
    trimmed.contains("getCharacteristic(") &&
    trimmed.contains("return toPropertyStrings(characteristic.properties);") {
    let service_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getPrimaryService",
    )
    let characteristic_uuid = @browser_domain.extract_bluetooth_call_string_argument(
      trimmed, "getCharacteristic",
    )
    let scope_key = @browser_domain.bluetooth_characteristic_scope_key(
      ctx_id, address, service_uuid, characteristic_uuid,
    )
    let property_items : Array[Json] = []
    match self.bluetooth_characteristics_by_scope.get(scope_key) {
      Some(Object(map)) =>
        for key, value in map {
          match value {
            True =>
              property_items.push(
                make_object({
                  "type": Json::string("string"),
                  "value": Json::string(key),
                }),
              )
            _ => ()
          }
        }
      _ => ()
    }
    self.send_script_remote_value_response(
      request_id,
      realm_id,
      make_object({
        "type": Json::string("array"),
        "value": Json::array(property_items),
      }),
      unwrap_result,
    )
    return true
  }
  false
}

///|
fn BidiProtocol::emit_bluetooth_request_device_prompt_updated(
  self : BidiProtocol,
  ctx_id : String,
  prompt_id : String,
  address : String,
) -> Unit {
  let event_name = "bluetooth.requestDevicePromptUpdated"
  if !self.is_subscribed_for_context(event_name, ctx_id) {
    return
  }
  self.outbox.push(
    Event({
      event_method: event_name,
      params: make_object({
        "context": Json::string(ctx_id),
        "prompt": Json::string(prompt_id),
        "devices": Json::array([
          make_object({ "id": Json::string(address), "name": Json::string("") }),
        ]),
      }),
    }),
  )
}

///|
fn BidiProtocol::emit_bluetooth_gatt_connection_attempted(
  self : BidiProtocol,
  ctx_id : String,
  address : String,
) -> Unit {
  let event_name = "bluetooth.gattConnectionAttempted"
  if !self.is_subscribed_for_context(event_name, ctx_id) {
    return
  }
  self.outbox.push(
    Event({
      event_method: event_name,
      params: make_object({
        "context": Json::string(ctx_id),
        "address": Json::string(address),
      }),
    }),
  )
}

///|
fn BidiProtocol::emit_bluetooth_characteristic_event_generated(
  self : BidiProtocol,
  ctx_id : String,
  address : String,
  service_uuid : String,
  characteristic_uuid : String,
  event_type : String,
  data : Json?,
) -> Unit {
  let event_name = "bluetooth.characteristicEventGenerated"
  if !self.is_subscribed_for_context(event_name, ctx_id) {
    return
  }
  let params : Map[String, Json] = {
    "context": Json::string(ctx_id),
    "address": Json::string(address),
    "serviceUuid": Json::string(service_uuid),
    "characteristicUuid": Json::string(characteristic_uuid),
    "type": Json::string(event_type),
  }
  match data {
    Some(data) => params["data"] = data
    None => ()
  }
  self.outbox.push(
    Event({ event_method: event_name, params: make_object(params) }),
  )
}

///|
fn BidiProtocol::emit_bluetooth_descriptor_event_generated(
  self : BidiProtocol,
  ctx_id : String,
  address : String,
  service_uuid : String,
  characteristic_uuid : String,
  descriptor_uuid : String,
  event_type : String,
  data : Json?,
) -> Unit {
  let event_name = "bluetooth.descriptorEventGenerated"
  if !self.is_subscribed_for_context(event_name, ctx_id) {
    return
  }
  let params : Map[String, Json] = {
    "context": Json::string(ctx_id),
    "address": Json::string(address),
    "serviceUuid": Json::string(service_uuid),
    "characteristicUuid": Json::string(characteristic_uuid),
    "descriptorUuid": Json::string(descriptor_uuid),
    "type": Json::string(event_type),
  }
  match data {
    Some(data) => params["data"] = data
    None => ()
  }
  self.outbox.push(
    Event({ event_method: event_name, params: make_object(params) }),
  )
}

///|
fn BidiProtocol::validate_bluetooth_manufacturer_data(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Array[Json]? {
  match
    get_param_raw_with_alias(params, "manufacturerData", "manufacturer_data") {
    Some(Array(entries)) => {
      let out : Array[Json] = []
      for entry in entries {
        match entry {
          Object(map) =>
            match map.get("key") {
              Some(Number(key, ..)) =>
                if key >= 0 && key.floor() == key {
                  match map.get("data") {
                    Some(String(data)) =>
                      out.push(
                        make_object({
                          "key": Json::number(key),
                          "data": Json::string(data),
                        }),
                      )
                    Some(_) => {
                      self.send_error(
                        request_id, "invalid argument", "manufacturerData.data must be a string",
                      )
                      return None
                    }
                    None => {
                      self.send_error(
                        request_id, "invalid argument", "manufacturerData entry is invalid",
                      )
                      return None
                    }
                  }
                } else {
                  self.send_error(
                    request_id, "invalid argument", "manufacturerData.key must be a non-negative integer",
                  )
                  return None
                }
              Some(_) => {
                self.send_error(
                  request_id, "invalid argument", "manufacturerData.key must be a non-negative integer",
                )
                return None
              }
              None => {
                self.send_error(
                  request_id, "invalid argument", "manufacturerData entry is invalid",
                )
                return None
              }
            }
          _ => {
            self.send_error(
              request_id, "invalid argument", "manufacturerData must contain objects",
            )
            return None
          }
        }
      }
      Some(out)
    }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "manufacturerData must be an array",
      )
      None
    }
    None => {
      self.send_error(
        request_id, "invalid argument", "Missing manufacturerData",
      )
      None
    }
  }
}

///|
fn BidiProtocol::validate_bluetooth_known_service_uuids(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Array[Json]? {
  match
    get_param_raw_with_alias(params, "knownServiceUuids", "known_service_uuids") {
    Some(Array(entries)) => {
      let out : Array[Json] = []
      for entry in entries {
        match entry {
          String(uuid) => out.push(Json::string(uuid))
          _ => {
            self.send_error(
              request_id, "invalid argument", "knownServiceUuids entries must be strings",
            )
            return None
          }
        }
      }
      Some(out)
    }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "knownServiceUuids must be an array",
      )
      None
    }
    None => {
      self.send_error(
        request_id, "invalid argument", "Missing knownServiceUuids",
      )
      None
    }
  }
}

///|
fn BidiProtocol::find_bluetooth_device_descriptor(
  self : BidiProtocol,
  ctx_id : String,
  address : String,
) -> Json? {
  match self.bluetooth_preconnected_devices_by_context.get(ctx_id) {
    Some(devices) =>
      for device in devices {
        match @browser_domain.bluetooth_device_field_string(device, "address") {
          Some(device_address) if device_address == address =>
            return Some(device)
          _ => ()
        }
      }
    None => ()
  }
  None
}

///|
fn BidiProtocol::has_bluetooth_device_descriptor(
  self : BidiProtocol,
  ctx_id : String,
  address : String,
) -> Bool {
  self.find_bluetooth_device_descriptor(ctx_id, address) != None
}

///|
fn BidiProtocol::validate_bluetooth_characteristic_properties(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
) -> Json? {
  match
    get_param_raw_with_alias(
      params, "characteristicProperties", "characteristic_properties",
    ) {
    Some(Object(map)) => {
      let out : Map[String, Json] = {}
      for key, value in map {
        match value {
          True => out[key] = Json::boolean(true)
          False => out[key] = Json::boolean(false)
          _ => {
            self.send_error(
              request_id, "invalid argument", "characteristicProperties values must be booleans",
            )
            return None
          }
        }
      }
      Some(make_object(out))
    }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "characteristicProperties must be an object",
      )
      None
    }
    None => {
      self.send_error(
        request_id, "invalid argument", "characteristicProperties must be present for add",
      )
      None
    }
  }
}

///|
fn BidiProtocol::validate_bluetooth_response_data(
  self : BidiProtocol,
  request_id : Int,
  params : Json?,
  required : Bool,
) -> Array[Json]? {
  match get_param_raw(params, "data") {
    Some(Array(entries)) =>
      if !required {
        self.send_error(
          request_id, "invalid argument", "data must be absent for this response type",
        )
        None
      } else {
        let out : Array[Json] = []
        for entry in entries {
          match entry {
            Number(n, ..) if n >= 0.0 && n <= 255.0 && n.floor() == n =>
              out.push(Json::number(n))
            _ => {
              self.send_error(
                request_id, "invalid argument", "data must be an array of bytes",
              )
              return None
            }
          }
        }
        Some(out)
      }
    Some(Null) | None =>
      if required {
        self.send_error(
          request_id, "invalid argument", "data must be present for read",
        )
        None
      } else {
        Some([])
      }
    Some(_) => {
      self.send_error(
        request_id, "invalid argument", "data must be an array of bytes",
      )
      None
    }
  }
}

///|
fn BidiProtocol::clear_pending_bluetooth_characteristic(
  self : BidiProtocol,
  ctx_id : String,
) -> Unit {
  self.bluetooth_pending_characteristic_request.remove(ctx_id)
  self.bluetooth_pending_characteristic_realm.remove(ctx_id)
  self.bluetooth_pending_characteristic_unwrap_result.remove(ctx_id)
  self.bluetooth_pending_characteristic_address.remove(ctx_id)
  self.bluetooth_pending_characteristic_service_uuid.remove(ctx_id)
  self.bluetooth_pending_characteristic_uuid.remove(ctx_id)
  self.bluetooth_pending_characteristic_type.remove(ctx_id)
}

///|
fn BidiProtocol::clear_pending_bluetooth_descriptor(
  self : BidiProtocol,
  ctx_id : String,
) -> Unit {
  self.bluetooth_pending_descriptor_request.remove(ctx_id)
  self.bluetooth_pending_descriptor_realm.remove(ctx_id)
  self.bluetooth_pending_descriptor_unwrap_result.remove(ctx_id)
  self.bluetooth_pending_descriptor_address.remove(ctx_id)
  self.bluetooth_pending_descriptor_service_uuid.remove(ctx_id)
  self.bluetooth_pending_descriptor_characteristic_uuid.remove(ctx_id)
  self.bluetooth_pending_descriptor_uuid.remove(ctx_id)
  self.bluetooth_pending_descriptor_type.remove(ctx_id)
}