// 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());
    });
};
import { CallSubscriber } from './CallSubscriber';
import { convertSdkCallToDeclarativeCall, convertSdkIncomingCallToDeclarativeIncomingCall } from './Converter';
import { IncomingCallSubscriber } from './IncomingCallSubscriber';
import { disposeAllViews, disposeAllViewsFromCall } from './StreamUtils';
import { _isTeamsIncomingCall } from './TypeGuards';
import { incomingCallDeclaratify } from './IncomingCallDeclarative';
import { teamsIncomingCallDeclaratify } from './TeamsIncomingCallDeclarative';
/**
 * ProxyCallAgent proxies CallAgent and saves any returned state in the given context. It will subscribe to all state
 * updates in the CallAgent and in the contained Calls and RemoteParticipants. When dispose is called it will
 * unsubscribe from all state updates.
 */
export class ProxyCallAgentCommon {
    constructor(context, internalContext) {
        // Unsubscribe is called when CallAgent is disposed. This should mean no more updating of existing call but we don't
        // remove any existing state.
        this.unregisterSubscriber = () => {
            for (const [_, callSubscriber] of this._callSubscribers.entries()) {
                callSubscriber.unsubscribe();
            }
            this._callSubscribers.clear();
            for (const [_, incomingCallSubscriber] of this._incomingCallSubscribers.entries()) {
                incomingCallSubscriber.unsubscribe();
            }
            this._incomingCallSubscribers.clear();
            this._incomingCalls.clear();
            for (const [_, declarativeCall] of this._declarativeCalls.entries()) {
                declarativeCall.unsubscribe();
            }
            this._declarativeCalls.clear();
        };
        this.callsUpdated = (event) => {
            const addedStatefulCall = [];
            for (const call of event.added) {
                const statefulCall = this.addCall(call);
                addedStatefulCall.push(statefulCall);
            }
            const removedStatefulCall = [];
            for (const call of event.removed) {
                disposeAllViewsFromCall(this._context, this._internalContext, call.id);
                const callSubscriber = this._callSubscribers.get(call);
                if (callSubscriber) {
                    callSubscriber.unsubscribe();
                    this._callSubscribers.delete(call);
                }
                this._context.setCallEnded(call.id, call.callEndReason);
                const declarativeCall = this._declarativeCalls.get(call);
                if (declarativeCall) {
                    declarativeCall.unsubscribe();
                    removedStatefulCall.push(declarativeCall);
                    this._declarativeCalls.delete(call);
                }
                else {
                    removedStatefulCall.push(this.callDeclaratify(call, this._context));
                }
            }
            for (const externalCallsUpdatedListener of this._externalCallsUpdatedListeners) {
                externalCallsUpdatedListener({
                    added: addedStatefulCall,
                    removed: removedStatefulCall
                });
            }
        };
        this.setIncomingCallEnded = (incomingCallId, callEndReason) => {
            const incomingCallSubscriber = this._incomingCallSubscribers.get(incomingCallId);
            if (incomingCallSubscriber) {
                incomingCallSubscriber.unsubscribe();
                this._incomingCallSubscribers.delete(incomingCallId);
            }
            this._incomingCalls.delete(incomingCallId);
            this._context.setIncomingCallEnded(incomingCallId, callEndReason);
        };
        this.incomingCall = ({ incomingCall }) => {
            // Make sure to not subscribe to the incoming call if we are already subscribed to it.
            if (!this._incomingCallSubscribers.has(incomingCall.id)) {
                this._incomingCallSubscribers.set(incomingCall.id, new IncomingCallSubscriber(incomingCall, this.setIncomingCallEnded));
            }
            if (_isTeamsIncomingCall(incomingCall)) {
                this._incomingCalls.set(incomingCall.id, teamsIncomingCallDeclaratify(incomingCall, this._context));
            }
            else {
                this._incomingCalls.set(incomingCall.id, incomingCallDeclaratify(incomingCall, this._context));
            }
            this._context.setIncomingCall(convertSdkIncomingCallToDeclarativeIncomingCall(incomingCall));
        };
        this.addCall = (call) => {
            var _a;
            (_a = this._callSubscribers.get(call)) === null || _a === void 0 ? void 0 : _a.unsubscribe();
            // For API extentions we need to have the call in the state when we are subscribing as we may want to update the
            // state during the subscription process in the subscriber so we add the call to state before subscribing.
            this._context.setCall(convertSdkCallToDeclarativeCall(call));
            this._callSubscribers.set(call, new CallSubscriber(call, this._context, this._internalContext));
            return this.getOrCreateDeclarativeCall(call);
        };
        this.getOrCreateDeclarativeCall = (call) => {
            const declarativeCall = this._declarativeCalls.get(call);
            if (declarativeCall) {
                return declarativeCall;
            }
            const newDeclarativeCall = this.callDeclaratify(call, this._context);
            this._declarativeCalls.set(call, newDeclarativeCall);
            return newDeclarativeCall;
        };
        this._context = context;
        this._internalContext = internalContext;
        this._callSubscribers = new Map();
        this._incomingCallSubscribers = new Map();
        this._incomingCalls = new Map();
        this._declarativeCalls = new Map();
        this._externalCallsUpdatedListeners = new Set();
    }
    // We can't directly override get function because it is proxied,
    // Add a getCommon function and call it in child class
    getCommon(target, prop) {
        switch (prop) {
            case 'startCall':
                {
                    return this._context.withErrorTeedToState((...args) => {
                        const call = this.startCall(target, args);
                        this.addCall(call);
                        return this.getOrCreateDeclarativeCall(call);
                    }, 'CallAgent.startCall');
                }
            case 'join':
                {
                    return this._context.withErrorTeedToState((...args) => {
                        const call = this.joinCall(target, args);
                        this.addCall(call);
                        return this.getOrCreateDeclarativeCall(call);
                    }, 'CallAgent.join');
                }
            case 'calls':
                {
                    return Array.from(this._declarativeCalls.values());
                }
            case 'on':
                {
                    return (...args) => {
                        // typescript is not smart enough to handle multiple overloads and pull the correct type here so force casting args
                        const event = args[0];
                        const isCallsUpdated = event === 'callsUpdated';
                        if (isCallsUpdated) {
                            const listener = args[1];
                            this._externalCallsUpdatedListeners.add(listener);
                        }
                        else {
                            this.agentSubscribe(target, args);
                        }
                    };
                }
            case 'off':
                {
                    return (...args) => {
                        // typescript is not smart enough to handle multiple overloads and pull the correct type here so force casting args
                        const event = args[0];
                        const isCallsUpdated = event === 'callsUpdated';
                        if (isCallsUpdated) {
                            const listener = args[1];
                            this._externalCallsUpdatedListeners.delete(listener);
                        }
                        else {
                            this.agentUnsubscribe(target, args);
                        }
                    };
                }
            case 'dispose':
                {
                    // Wrapping CallAgent.dispose in a callback type (): Promise<void> to accomodate the change of CallAgent.dispose
                    // in calling beta version 1.8.0-beta.1 from callback type (): Promise<void> to (): void
                    const callAgentDisposeAsyncCallbackWrapper = () => __awaiter(this, void 0, void 0, function* () {
                        yield target.dispose();
                        return Promise.resolve();
                    });
                    return () => {
                        return callAgentDisposeAsyncCallbackWrapper().then(() => {
                            this.unsubscribe();
                        });
                    };
                }
            /**
             * This attribute is a special case and doesn't exist on the CallAgent interface.
             * We need this to be able to return a declarative incoming call object using the call agent.
             * In a standard headless SDK usage, the right way to get an incoming call is to use the `incomingCall` event.
             * However, using the declarative layer, the ideal usage would be to:
             * 1. subscribe to the `onStateChange` event
             * 2. Get the incoming call from the new state and it's ID
             * 3. Use `callAgent.incomingCalls` and filter an incoming call ID to get a declarative incoming call object
             */
            case 'incomingCalls':
                {
                    return Array.from(this._incomingCalls.values());
                }
            default:
                return Reflect.get(target, prop);
        }
    }
}
/**
 * @private
 */
export const clearCallRelatedState = (context, internalContext) => {
    // Make sure there are no existing call data if creating a new CallAgentDeclarative (if creating a new
    // CallAgentDeclarative after disposing of the hold one will mean context have old call state). TODO: should we stop
    // rendering when the previous callAgent is disposed?
    disposeAllViews(context, internalContext);
    context.clearCallRelatedState();
    internalContext.clearCallRelatedState();
};
//# sourceMappingURL=CallAgentDeclarativeCommon.js.map