"use strict";
/**
 * Copyright 2024 Google LLC.
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.BluetoothProcessor = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
/** Represents a base Bluetooth GATT item. */
class BluetoothGattItem {
    id;
    uuid;
    constructor(id, uuid) {
        this.id = id;
        this.uuid = uuid;
    }
}
/** Represents a Bluetooth descriptor. */
class BluetoothDescriptor extends BluetoothGattItem {
    characteristic;
    constructor(id, uuid, characteristic) {
        super(id, uuid);
        this.characteristic = characteristic;
    }
}
/** Represents a Bluetooth characteristic. */
class BluetoothCharacteristic extends BluetoothGattItem {
    descriptors = new Map();
    service;
    constructor(id, uuid, service) {
        super(id, uuid);
        this.service = service;
    }
}
/** Represents a Bluetooth service. */
class BluetoothService extends BluetoothGattItem {
    characteristics = new Map();
    device;
    constructor(id, uuid, device) {
        super(id, uuid);
        this.device = device;
    }
}
/** Represents a Bluetooth device. */
class BluetoothDevice {
    address;
    services = new Map();
    constructor(address) {
        this.address = address;
    }
}
class BluetoothProcessor {
    #eventManager;
    #browsingContextStorage;
    #bluetoothDevices = new Map();
    // A map from a characteristic id from CDP to its BluetoothCharacteristic object.
    #bluetoothCharacteristics = new Map();
    // A map from a descriptor id from CDP to its BluetoothDescriptor object.
    #bluetoothDescriptors = new Map();
    constructor(eventManager, browsingContextStorage) {
        this.#eventManager = eventManager;
        this.#browsingContextStorage = browsingContextStorage;
    }
    #getDevice(address) {
        const device = this.#bluetoothDevices.get(address);
        if (!device) {
            throw new protocol_js_1.InvalidArgumentException(`Bluetooth device with address ${address} does not exist`);
        }
        return device;
    }
    #getService(device, serviceUuid) {
        const service = device.services.get(serviceUuid);
        if (!service) {
            throw new protocol_js_1.InvalidArgumentException(`Service with UUID ${serviceUuid} on device ${device.address} does not exist`);
        }
        return service;
    }
    #getCharacteristic(service, characteristicUuid) {
        const characteristic = service.characteristics.get(characteristicUuid);
        if (!characteristic) {
            throw new protocol_js_1.InvalidArgumentException(`Characteristic with UUID ${characteristicUuid} does not exist for service ${service.uuid} on device ${service.device.address}`);
        }
        return characteristic;
    }
    #getDescriptor(characteristic, descriptorUuid) {
        const descriptor = characteristic.descriptors.get(descriptorUuid);
        if (!descriptor) {
            throw new protocol_js_1.InvalidArgumentException(`Descriptor with UUID ${descriptorUuid} does not exist for characteristic ${characteristic.uuid} on service ${characteristic.service.uuid} on device ${characteristic.service.device.address}`);
        }
        return descriptor;
    }
    async simulateAdapter(params) {
        if (params.state === undefined) {
            // The bluetooth.simulateAdapter Command
            // Step 4.2. If params["state"] does not exist, return error with error code invalid argument.
            // https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command
            throw new protocol_js_1.InvalidArgumentException(`Parameter "state" is required for creating a Bluetooth adapter`);
        }
        const context = this.#browsingContextStorage.getContext(params.context);
        // Bluetooth spec requires overriding the existing adapter (step 6). From the CDP
        // perspective, we need to disable the emulation first.
        // https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command
        await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.disable');
        this.#bluetoothDevices.clear();
        this.#bluetoothCharacteristics.clear();
        this.#bluetoothDescriptors.clear();
        await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.enable', {
            state: params.state,
            leSupported: params.leSupported ?? true,
        });
        return {};
    }
    async disableSimulation(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.disable');
        this.#bluetoothDevices.clear();
        this.#bluetoothCharacteristics.clear();
        this.#bluetoothDescriptors.clear();
        return {};
    }
    async simulatePreconnectedPeripheral(params) {
        if (this.#bluetoothDevices.has(params.address)) {
            throw new protocol_js_1.InvalidArgumentException(`Bluetooth device with address ${params.address} already exists`);
        }
        const context = this.#browsingContextStorage.getContext(params.context);
        await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulatePreconnectedPeripheral', {
            address: params.address,
            name: params.name,
            knownServiceUuids: params.knownServiceUuids,
            manufacturerData: params.manufacturerData,
        });
        this.#bluetoothDevices.set(params.address, new BluetoothDevice(params.address));
        return {};
    }
    async simulateAdvertisement(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateAdvertisement', {
            entry: params.scanEntry,
        });
        return {};
    }
    async simulateCharacteristic(params) {
        const device = this.#getDevice(params.address);
        const service = this.#getService(device, params.serviceUuid);
        const context = this.#browsingContextStorage.getContext(params.context);
        switch (params.type) {
            case 'add': {
                if (params.characteristicProperties === undefined) {
                    throw new protocol_js_1.InvalidArgumentException(`Parameter "characteristicProperties" is required for adding a Bluetooth characteristic`);
                }
                if (service.characteristics.has(params.characteristicUuid)) {
                    throw new protocol_js_1.InvalidArgumentException(`Characteristic with UUID ${params.characteristicUuid} already exists`);
                }
                const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addCharacteristic', {
                    serviceId: service.id,
                    characteristicUuid: params.characteristicUuid,
                    properties: params.characteristicProperties,
                });
                const characteristic = new BluetoothCharacteristic(response.characteristicId, params.characteristicUuid, service);
                service.characteristics.set(params.characteristicUuid, characteristic);
                this.#bluetoothCharacteristics.set(characteristic.id, characteristic);
                return {};
            }
            case 'remove': {
                if (params.characteristicProperties !== undefined) {
                    throw new protocol_js_1.InvalidArgumentException(`Parameter "characteristicProperties" should not be provided for removing a Bluetooth characteristic`);
                }
                const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
                await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeCharacteristic', {
                    characteristicId: characteristic.id,
                });
                service.characteristics.delete(params.characteristicUuid);
                this.#bluetoothCharacteristics.delete(characteristic.id);
                return {};
            }
            default:
                throw new protocol_js_1.InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`);
        }
    }
    async simulateCharacteristicResponse(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        const device = this.#getDevice(params.address);
        const service = this.#getService(device, params.serviceUuid);
        const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
        await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateCharacteristicOperationResponse', {
            characteristicId: characteristic.id,
            type: params.type,
            code: params.code,
            ...(params.data && {
                data: btoa(String.fromCharCode(...params.data)),
            }),
        });
        return {};
    }
    async simulateDescriptor(params) {
        const device = this.#getDevice(params.address);
        const service = this.#getService(device, params.serviceUuid);
        const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
        const context = this.#browsingContextStorage.getContext(params.context);
        switch (params.type) {
            case 'add': {
                if (characteristic.descriptors.has(params.descriptorUuid)) {
                    throw new protocol_js_1.InvalidArgumentException(`Descriptor with UUID ${params.descriptorUuid} already exists`);
                }
                const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addDescriptor', {
                    characteristicId: characteristic.id,
                    descriptorUuid: params.descriptorUuid,
                });
                const descriptor = new BluetoothDescriptor(response.descriptorId, params.descriptorUuid, characteristic);
                characteristic.descriptors.set(params.descriptorUuid, descriptor);
                this.#bluetoothDescriptors.set(descriptor.id, descriptor);
                return {};
            }
            case 'remove': {
                const descriptor = this.#getDescriptor(characteristic, params.descriptorUuid);
                await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeDescriptor', {
                    descriptorId: descriptor.id,
                });
                characteristic.descriptors.delete(params.descriptorUuid);
                this.#bluetoothDescriptors.delete(descriptor.id);
                return {};
            }
            default:
                throw new protocol_js_1.InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`);
        }
    }
    async simulateDescriptorResponse(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        const device = this.#getDevice(params.address);
        const service = this.#getService(device, params.serviceUuid);
        const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
        const descriptor = this.#getDescriptor(characteristic, params.descriptorUuid);
        await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateDescriptorOperationResponse', {
            descriptorId: descriptor.id,
            type: params.type,
            code: params.code,
            ...(params.data && {
                data: btoa(String.fromCharCode(...params.data)),
            }),
        });
        return {};
    }
    async simulateGattConnectionResponse(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTOperationResponse', {
            address: params.address,
            type: 'connection',
            code: params.code,
        });
        return {};
    }
    async simulateGattDisconnection(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTDisconnection', {
            address: params.address,
        });
        return {};
    }
    async simulateService(params) {
        const device = this.#getDevice(params.address);
        const context = this.#browsingContextStorage.getContext(params.context);
        switch (params.type) {
            case 'add': {
                if (device.services.has(params.uuid)) {
                    throw new protocol_js_1.InvalidArgumentException(`Service with UUID ${params.uuid} already exists`);
                }
                const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addService', {
                    address: params.address,
                    serviceUuid: params.uuid,
                });
                device.services.set(params.uuid, new BluetoothService(response.serviceId, params.uuid, device));
                return {};
            }
            case 'remove': {
                const service = this.#getService(device, params.uuid);
                await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeService', {
                    serviceId: service.id,
                });
                device.services.delete(params.uuid);
                return {};
            }
            default:
                throw new protocol_js_1.InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`);
        }
    }
    onCdpTargetCreated(cdpTarget) {
        cdpTarget.cdpClient.on('DeviceAccess.deviceRequestPrompted', (event) => {
            this.#eventManager.registerEvent({
                type: 'event',
                method: 'bluetooth.requestDevicePromptUpdated',
                params: {
                    context: cdpTarget.id,
                    prompt: event.id,
                    devices: event.devices,
                },
            }, cdpTarget.id);
        });
        cdpTarget.browserCdpClient.on('BluetoothEmulation.gattOperationReceived', async (event) => {
            switch (event.type) {
                case 'connection':
                    this.#eventManager.registerEvent({
                        type: 'event',
                        method: 'bluetooth.gattConnectionAttempted',
                        params: {
                            context: cdpTarget.id,
                            address: event.address,
                        },
                    }, cdpTarget.id);
                    return;
                case 'discovery':
                    // Chromium Web Bluetooth simulation generates this GATT discovery event when
                    // a page attempts to get services for a given Bluetooth device for the first time.
                    // This 'get services' operation is put on hold until a GATT discovery response
                    // is sent to the simulation.
                    // Note: Web Bluetooth automation (see https://webbluetoothcg.github.io/web-bluetooth/#automated-testing)
                    // does not support simulating a GATT discovery response. This is because simulated services, characteristics,
                    // or descriptors are immediately visible to the simulation, meaning it doesn't have a distinct
                    // DISCOVERY state. Therefore, this code simulates a successful GATT discovery
                    // response upon receiving this event.
                    await cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTOperationResponse', {
                        address: event.address,
                        type: 'discovery',
                        code: 0x0,
                    });
            }
        });
        cdpTarget.browserCdpClient.on('BluetoothEmulation.characteristicOperationReceived', (event) => {
            if (!this.#bluetoothCharacteristics.has(event.characteristicId)) {
                return;
            }
            let type;
            if (event.type === 'write') {
                // write-default-deprecated comes from
                // https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothremotegattcharacteristic-writevalue,
                // which is deprecated so not supported.
                if (event.writeType === 'write-default-deprecated') {
                    return;
                }
                type = event.writeType;
            }
            else {
                type = event.type;
            }
            const characteristic = this.#bluetoothCharacteristics.get(event.characteristicId);
            this.#eventManager.registerEvent({
                type: 'event',
                method: 'bluetooth.characteristicEventGenerated',
                params: {
                    context: cdpTarget.id,
                    address: characteristic.service.device.address,
                    serviceUuid: characteristic.service.uuid,
                    characteristicUuid: characteristic.uuid,
                    type,
                    ...(event.data && {
                        data: Array.from(atob(event.data), (c) => c.charCodeAt(0)),
                    }),
                },
            }, cdpTarget.id);
        });
        cdpTarget.browserCdpClient.on('BluetoothEmulation.descriptorOperationReceived', (event) => {
            if (!this.#bluetoothDescriptors.has(event.descriptorId)) {
                return;
            }
            const descriptor = this.#bluetoothDescriptors.get(event.descriptorId);
            this.#eventManager.registerEvent({
                type: 'event',
                method: 'bluetooth.descriptorEventGenerated',
                params: {
                    context: cdpTarget.id,
                    address: descriptor.characteristic.service.device.address,
                    serviceUuid: descriptor.characteristic.service.uuid,
                    characteristicUuid: descriptor.characteristic.uuid,
                    descriptorUuid: descriptor.uuid,
                    type: event.type,
                    ...(event.data && {
                        data: Array.from(atob(event.data), (c) => c.charCodeAt(0)),
                    }),
                },
            }, cdpTarget.id);
        });
    }
    async handleRequestDevicePrompt(params) {
        const context = this.#browsingContextStorage.getContext(params.context);
        if (params.accept) {
            await context.cdpTarget.cdpClient.sendCommand('DeviceAccess.selectPrompt', {
                id: params.prompt,
                deviceId: params.device,
            });
        }
        else {
            await context.cdpTarget.cdpClient.sendCommand('DeviceAccess.cancelPrompt', {
                id: params.prompt,
            });
        }
        return {};
    }
}
exports.BluetoothProcessor = BluetoothProcessor;
//# sourceMappingURL=BluetoothProcessor.js.map