"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebUSB = exports.getWebUsb = void 0;
const usb = require("../usb");
const events_1 = require("events");
const webusb_device_1 = require("./webusb-device");
/**
 * Convenience method to get the WebUSB interface available
 */
const getWebUsb = () => {
    if (navigator && navigator.usb) {
        return navigator.usb;
    }
    return new WebUSB();
};
exports.getWebUsb = getWebUsb;
class NamedError extends Error {
    constructor(message, name) {
        super(message);
        this.name = name;
    }
}
class WebUSB {
    constructor(options = {}) {
        this.options = options;
        this.emitter = new events_1.EventEmitter();
        this.knownDevices = new Map();
        this.authorisedDevices = new Set();
        const deviceConnectCallback = async (device) => {
            const webDevice = await this.getWebDevice(device);
            // When connected, emit an event if it is an allowed device
            if (webDevice && this.isAuthorisedDevice(webDevice)) {
                const event = {
                    type: 'connect',
                    device: webDevice
                };
                this.emitter.emit('connect', event);
            }
        };
        const deviceDisconnectCallback = async (device) => {
            // When disconnected, emit an event if the device was a known allowed device
            if (this.knownDevices.has(device)) {
                const webDevice = this.knownDevices.get(device);
                if (webDevice && this.isAuthorisedDevice(webDevice)) {
                    const event = {
                        type: 'disconnect',
                        device: webDevice
                    };
                    this.emitter.emit('disconnect', event);
                }
            }
        };
        this.emitter.on('newListener', event => {
            const listenerCount = this.emitter.listenerCount(event);
            if (listenerCount !== 0) {
                return;
            }
            if (event === 'connect') {
                usb.addListener('attach', deviceConnectCallback);
            }
            else if (event === 'disconnect') {
                usb.addListener('detach', deviceDisconnectCallback);
            }
        });
        this.emitter.on('removeListener', event => {
            const listenerCount = this.emitter.listenerCount(event);
            if (listenerCount !== 0) {
                return;
            }
            if (event === 'connect') {
                usb.removeListener('attach', deviceConnectCallback);
            }
            else if (event === 'disconnect') {
                usb.removeListener('detach', deviceDisconnectCallback);
            }
        });
    }
    set onconnect(fn) {
        if (this._onconnect) {
            this.removeEventListener('connect', this._onconnect);
            this._onconnect = undefined;
        }
        if (fn) {
            this._onconnect = fn;
            this.addEventListener('connect', this._onconnect);
        }
    }
    set ondisconnect(fn) {
        if (this._ondisconnect) {
            this.removeEventListener('disconnect', this._ondisconnect);
            this._ondisconnect = undefined;
        }
        if (fn) {
            this._ondisconnect = fn;
            this.addEventListener('disconnect', this._ondisconnect);
        }
    }
    addEventListener(type, listener) {
        this.emitter.addListener(type, listener);
    }
    removeEventListener(type, callback) {
        this.emitter.removeListener(type, callback);
    }
    dispatchEvent(_event) {
        // Don't dispatch from here
        return false;
    }
    /**
     * Requests a single Web USB device
     * @param options The options to use when scanning
     * @returns Promise containing the selected device
     */
    async requestDevice(options) {
        // Must have options
        if (!options) {
            throw new TypeError('requestDevice error: 1 argument required, but only 0 present');
        }
        // Options must be an object
        if (options.constructor !== {}.constructor) {
            throw new TypeError('requestDevice error: parameter 1 (options) is not an object');
        }
        // Must have a filter
        if (!options.filters) {
            throw new TypeError('requestDevice error: required member filters is undefined');
        }
        // Filter must be an array
        if (options.filters.constructor !== [].constructor) {
            throw new TypeError('requestDevice error: the provided value cannot be converted to a sequence');
        }
        // Check filters
        options.filters.forEach(filter => {
            // Protocol & Subclass
            if (filter.protocolCode && !filter.subclassCode) {
                throw new TypeError('requestDevice error: subclass code is required');
            }
            // Subclass & Class
            if (filter.subclassCode && !filter.classCode) {
                throw new TypeError('requestDevice error: class code is required');
            }
        });
        let devices = await this.loadDevices(options.filters);
        devices = devices.filter(device => this.filterDevice(device, options.filters));
        if (devices.length === 0) {
            throw new NamedError('Failed to execute \'requestDevice\' on \'USB\': No device selected.', 'NotFoundError');
        }
        try {
            // If no devicesFound function, select the first device found
            const device = this.options.devicesFound ? await this.options.devicesFound(devices) : devices[0];
            if (!device) {
                throw new NamedError('Failed to execute \'requestDevice\' on \'USB\': No device selected.', 'NotFoundError');
            }
            this.authorisedDevices.add({
                vendorId: device.vendorId,
                productId: device.productId,
                classCode: device.deviceClass,
                subclassCode: device.deviceSubclass,
                protocolCode: device.deviceProtocol,
                serialNumber: device.serialNumber || undefined
            });
            return device;
        }
        catch (error) {
            throw new NamedError('Failed to execute \'requestDevice\' on \'USB\': No device selected.', 'NotFoundError');
        }
    }
    /**
     * Gets all allowed Web USB devices which are connected
     * @returns Promise containing an array of devices
     */
    async getDevices() {
        const preFilters = this.options.allowAllDevices ? undefined : this.options.allowedDevices;
        // Refresh devices and filter for allowed ones
        const devices = await this.loadDevices(preFilters);
        return devices.filter(device => this.isAuthorisedDevice(device));
    }
    async loadDevices(preFilters) {
        let devices = usb.getDeviceList();
        // Pre-filter devices
        devices = this.quickFilter(devices, preFilters);
        const refreshedKnownDevices = new Map();
        for (const device of devices) {
            const webDevice = await this.getWebDevice(device);
            if (webDevice) {
                refreshedKnownDevices.set(device, webDevice);
            }
        }
        // Refresh knownDevices to remove old devices from the map
        this.knownDevices = refreshedKnownDevices;
        return [...this.knownDevices.values()];
    }
    // Get a WebUSBDevice corresponding to underlying device.
    // Returns undefined the device was not found and could not be created.
    async getWebDevice(device) {
        if (!this.knownDevices.has(device)) {
            if (this.options.deviceTimeout) {
                device.timeout = this.options.deviceTimeout;
            }
            try {
                const webDevice = await webusb_device_1.WebUSBDevice.createInstance(device, this.options.autoDetachKernelDriver);
                this.knownDevices.set(device, webDevice);
            }
            catch {
                // Ignore creation issues as this may be a system device
            }
        }
        return this.knownDevices.get(device);
    }
    // Undertake quick filter on devices before creating WebUSB devices if possible
    quickFilter(devices, preFilters) {
        if (!preFilters || !preFilters.length) {
            return devices;
        }
        // Just pre-filter on vid/pid
        return devices.filter(device => preFilters.some(filter => {
            // Vendor
            if (filter.vendorId && filter.vendorId !== device.deviceDescriptor.idVendor)
                return false;
            // Product
            if (filter.productId && filter.productId !== device.deviceDescriptor.idProduct)
                return false;
            // Ignore Class, Subclass and Protocol as these need to check interfaces, too
            // Ignore serial number for node-usb as it requires device connection
            return true;
        }));
    }
    // Filter WebUSB devices
    filterDevice(device, filters) {
        if (!filters || !filters.length) {
            return true;
        }
        return filters.some(filter => {
            // Vendor
            if (filter.vendorId && filter.vendorId !== device.vendorId)
                return false;
            // Product
            if (filter.productId && filter.productId !== device.productId)
                return false;
            // Class
            if (filter.classCode) {
                if (!device.configuration) {
                    return false;
                }
                // Interface Descriptors
                const match = device.configuration.interfaces.some(iface => {
                    // Class
                    if (filter.classCode && filter.classCode !== iface.alternate.interfaceClass)
                        return false;
                    // Subclass
                    if (filter.subclassCode && filter.subclassCode !== iface.alternate.interfaceSubclass)
                        return false;
                    // Protocol
                    if (filter.protocolCode && filter.protocolCode !== iface.alternate.interfaceProtocol)
                        return false;
                    return true;
                });
                if (match) {
                    return true;
                }
            }
            // Class
            if (filter.classCode && filter.classCode !== device.deviceClass)
                return false;
            // Subclass
            if (filter.subclassCode && filter.subclassCode !== device.deviceSubclass)
                return false;
            // Protocol
            if (filter.protocolCode && filter.protocolCode !== device.deviceProtocol)
                return false;
            // Serial
            if (filter.serialNumber && filter.serialNumber !== device.serialNumber)
                return false;
            return true;
        });
    }
    // Check whether a device is authorised
    isAuthorisedDevice(device) {
        // All devices are authorised
        if (this.options.allowAllDevices) {
            return true;
        }
        // Check any allowed device filters
        if (this.options.allowedDevices && this.filterDevice(device, this.options.allowedDevices)) {
            return true;
        }
        // Check authorised devices
        return [...this.authorisedDevices.values()].some(authorised => authorised.vendorId === device.vendorId
            && authorised.productId === device.productId
            && authorised.classCode === device.deviceClass
            && authorised.subclassCode === device.deviceSubclass
            && authorised.protocolCode === device.deviceProtocol
            && authorised.serialNumber === device.serialNumber);
    }
}
exports.WebUSB = WebUSB;
//# sourceMappingURL=index.js.map