// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
/**
 * ProxyDeviceManager proxies DeviceManager and subscribes to all events that affect device manager state. State updates
 * are set on the provided context. Also any queries for state are proxied and stored in state as well. Only one device
 * manager should exist for a given CallClient so if CallClient.getDeviceManager is called multiple times, either a
 * cached ProxyDeviceManager should be returned or the existing ProxyDeviceManager should be destructed via destructor()
 * and a new ProxyDeviceManager created.
 */
class ProxyDeviceManager {
    constructor(deviceManager, context) {
        this.setDeviceManager = () => {
            // isSpeakerSelectionAvailable, selectedMicrophone, and selectedSpeaker are properties on DeviceManager. Since they
            // are not functions we can't proxy them so we'll update whenever we think they may need updating such as at
            // construction time or when certain events happen.
            this._context.setDeviceManagerIsSpeakerSelectionAvailable(this._deviceManager.isSpeakerSelectionAvailable);
            this._context.setDeviceManagerSelectedMicrophone(this._deviceManager.selectedMicrophone);
            this._context.setDeviceManagerSelectedSpeaker(this._deviceManager.selectedSpeaker);
        };
        this.subscribe = () => {
            this._deviceManager.on('videoDevicesUpdated', this.videoDevicesUpdated);
            this._deviceManager.on('audioDevicesUpdated', this.audioDevicesUpdated);
            this._deviceManager.on('selectedMicrophoneChanged', this.selectedMicrophoneChanged);
            this._deviceManager.on('selectedSpeakerChanged', this.selectedSpeakerChanged);
            // Subscribe to browser camera permissions changes
            if (navigator.permissions) {
                try {
                    navigator.permissions.query({
                        name: 'camera'
                    }).then((cameraPermissions) => {
                        cameraPermissions.addEventListener('change', this.permissionsApiStateChangeHandler);
                    });
                }
                catch (e) {
                    console.info('Could not subscribe to Permissions API Camera changed events, API is not supported by browser', e);
                }
                // Subscribe to browser microphone permissions changes
                try {
                    navigator.permissions.query({
                        name: 'microphone'
                    }).then((micPermissions) => {
                        micPermissions.addEventListener('change', this.permissionsApiStateChangeHandler);
                    });
                }
                catch (e) {
                    console.info('Could not subscribe to Permissions API Microphone changed events, API is not supported by browser', e);
                }
            }
        };
        /**
         * This is used to unsubscribe DeclarativeDeviceManager from the DeviceManager events.
         */
        this.unsubscribe = () => {
            this._deviceManager.off('videoDevicesUpdated', this.videoDevicesUpdated);
            this._deviceManager.off('audioDevicesUpdated', this.audioDevicesUpdated);
            this._deviceManager.off('selectedMicrophoneChanged', this.selectedMicrophoneChanged);
            this._deviceManager.off('selectedSpeakerChanged', this.selectedSpeakerChanged);
            if (navigator.permissions) {
                // Unsubscribe from browser camera permissions changes
                try {
                    navigator.permissions.query({
                        name: 'camera'
                    }).then((cameraPermissions) => {
                        cameraPermissions.removeEventListener('change', this.permissionsApiStateChangeHandler);
                    });
                }
                catch (e) {
                    console.info('Could not Unsubscribe to Permissions API Camera changed events, API is not supported by browser', e);
                }
                // Unsubscribe from browser microphone permissions changes
                try {
                    navigator.permissions.query({
                        name: 'microphone'
                    }).then((micPermissions) => {
                        micPermissions.removeEventListener('change', this.permissionsApiStateChangeHandler);
                    });
                }
                catch (e) {
                    console.info('Could not Unsubscribe to Permissions API Camera changed events, API is not supported by browser', e);
                }
            }
        };
        this.permissionsApiStateChangeHandler = () => __awaiter(this, void 0, void 0, function* () {
            yield this.updateDevicePermissionState();
        });
        /**
         * Used to set a camera inside the proxy device manager.
         *
         * @param videoDeviceInfo VideoDeviceInfo
         */
        this.selectCamera = (videoDeviceInfo) => {
            this._context.setDeviceManagerSelectedCamera(videoDeviceInfo);
        };
        this.videoDevicesUpdated = () => __awaiter(this, void 0, void 0, function* () {
            // Device Manager always has a camera with '' name if there are no real camera devices available.
            // We don't want to show that in the UI.
            const realCameras = (yield this._deviceManager.getCameras()).filter(c => !!c.name);
            this._context.setDeviceManagerCameras(dedupeById(realCameras));
        });
        this.audioDevicesUpdated = () => __awaiter(this, void 0, void 0, function* () {
            this._context.setDeviceManagerMicrophones(dedupeById(yield this._deviceManager.getMicrophones()));
            if (this._deviceManager.isSpeakerSelectionAvailable) {
                this._context.setDeviceManagerSpeakers(dedupeById(yield this._deviceManager.getSpeakers()));
            }
        });
        this.selectedMicrophoneChanged = () => {
            this._context.setDeviceManagerSelectedMicrophone(this._deviceManager.selectedMicrophone);
        };
        this.selectedSpeakerChanged = () => {
            this._context.setDeviceManagerSelectedSpeaker(this._deviceManager.selectedSpeaker);
        };
        this.updateDevicePermissionState = (sdkDeviceAccessState) => __awaiter(this, void 0, void 0, function* () {
            let hasCameraPermission = !!(sdkDeviceAccessState === null || sdkDeviceAccessState === void 0 ? void 0 : sdkDeviceAccessState.video);
            let hasMicPermission = !!(sdkDeviceAccessState === null || sdkDeviceAccessState === void 0 ? void 0 : sdkDeviceAccessState.audio);
            // Supplement the SDK values with values from the Permissions API to get a better understanding of the device
            // permission state. The SDK only uses the getUserMedia API to determine the device permission state. However,
            // this returns false if the camera is in use by another application. The Permissions API can provide more
            // information about the device permission state, but is not supported yet in Firefox or Android WebView.
            // Note: It also has the limitation where it cannot detect if the device is blocked by the Operating System
            // permissions.
            if (navigator.permissions) {
                try {
                    const [cameraPermissions, micPermissions] = yield Promise.all([navigator.permissions.query({
                            name: 'camera'
                        }), navigator.permissions.query({
                            name: 'microphone'
                        })]);
                    hasCameraPermission = cameraPermissions.state === 'granted';
                    hasMicPermission = micPermissions.state === 'granted';
                }
                catch (e) {
                    console.info('Permissions API is not supported by browser', e);
                }
            }
            this._context.setDeviceManagerDeviceAccess({
                video: hasCameraPermission,
                audio: hasMicPermission
            });
            this.setDeviceManager();
        });
        this._deviceManager = deviceManager;
        this._context = context;
        this.setDeviceManager();
        this.subscribe();
    }
    get(target, prop) {
        switch (prop) {
            case 'getCameras':
                {
                    return this._context.withAsyncErrorTeedToState(() => {
                        return target.getCameras().then((cameras) => {
                            // Device Manager always has a camera with '' name if there are no real camera devices available.
                            // We don't want to show that in the UI.
                            const realCameras = cameras.filter(c => !!c.name);
                            this._context.setDeviceManagerCameras(dedupeById(realCameras));
                            return realCameras;
                        });
                    }, 'DeviceManager.getCameras');
                }
            case 'getMicrophones':
                {
                    return this._context.withAsyncErrorTeedToState(() => {
                        return target.getMicrophones().then((microphones) => {
                            this._context.setDeviceManagerMicrophones(dedupeById(microphones));
                            return microphones;
                        });
                    }, 'DeviceManager.getMicrophones');
                }
            case 'getSpeakers':
                {
                    return this._context.withAsyncErrorTeedToState(() => {
                        return target.getSpeakers().then((speakers) => {
                            this._context.setDeviceManagerSpeakers(dedupeById(speakers));
                            return speakers;
                        });
                    }, 'DeviceManager.getSpeakers');
                }
            case 'selectMicrophone':
                {
                    return this._context.withAsyncErrorTeedToState((...args) => {
                        return target.selectMicrophone(...args).then(() => {
                            this._context.setDeviceManagerSelectedMicrophone(target.selectedMicrophone);
                        });
                    }, 'DeviceManager.selectMicrophone');
                }
            case 'selectSpeaker':
                {
                    return this._context.withAsyncErrorTeedToState((...args) => {
                        return target.selectSpeaker(...args).then(() => {
                            this._context.setDeviceManagerSelectedSpeaker(target.selectedSpeaker);
                        });
                    }, 'DeviceManager.selectSpeaker');
                }
            case 'askDevicePermission':
                {
                    return this._context.withAsyncErrorTeedToState((...args) => {
                        return target.askDevicePermission(...args).then((deviceAccess) => __awaiter(this, void 0, void 0, function* () {
                            yield this.updateDevicePermissionState(deviceAccess);
                            return deviceAccess;
                        }));
                    }, 'DeviceManager.askDevicePermission');
                }
            default:
                return Reflect.get(target, prop);
        }
    }
}
// TODO: Remove this when SDK no longer returns duplicate audio and video devices
/** Helper function to dedupe duplicate audio and video devices obtained from SDK */
const dedupeById = (devices) => {
    const ids = new Set();
    const uniqueDevices = [];
    devices.forEach((device) => {
        if (!ids.has(device.id)) {
            uniqueDevices.push(device);
            ids.add(device.id);
        }
    });
    return uniqueDevices;
};
/**
 * Creates a declarative DeviceManager by proxying DeviceManager with ProxyDeviceManager. The declarative DeviceManager
 * will put state updates in the given context.
 *
 * @param deviceManager - DeviceManager from SDK
 * @param context - CallContext from StatefulCallClient
 *
 * @private
 */
export const deviceManagerDeclaratify = (deviceManager, context, internalContext) => {
    const proxyDeviceManager = new ProxyDeviceManager(deviceManager, context);
    Object.defineProperty(deviceManager, 'unsubscribe', {
        configurable: false,
        value: () => proxyDeviceManager.unsubscribe()
    });
    Object.defineProperty(deviceManager, 'selectCamera', {
        configurable: false,
        value: (videoDeviceInfo) => proxyDeviceManager.selectCamera(videoDeviceInfo)
    });
    Object.defineProperty(deviceManager, 'getUnparentedVideoStreams', {
        configurable: false,
        value: () => internalContext.getUnparentedRenderInfos()
    });
    return new Proxy(deviceManager, proxyDeviceManager);
};
//# sourceMappingURL=DeviceManagerDeclarative.js.map