"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SessionManager = void 0;
const softphone_session_handler_1 = __importDefault(require("./softphone-session-handler"));
const enums_1 = require("../types/enums");
const utils_1 = require("../utils");
const screen_share_session_handler_1 = __importDefault(require("./screen-share-session-handler"));
const video_session_handler_1 = __importDefault(require("./video-session-handler"));
const screen_recording_session_handler_1 = __importDefault(require("./screen-recording-session-handler"));
const live_monitoring_session_handler_1 = __importDefault(require("./live-monitoring-session-handler"));
const sessionHandlersToConfigure = [
    softphone_session_handler_1.default,
    video_session_handler_1.default,
    screen_share_session_handler_1.default,
    screen_recording_session_handler_1.default,
    live_monitoring_session_handler_1.default
];
class SessionManager {
    constructor(sdk) {
        this.sdk = sdk;
        /* use conversationId to index to account for persistent connections */
        this.pendingSessions = [];
        this.sessionHandlers = sessionHandlersToConfigure.map((ClassDef) => new ClassDef(this.sdk, this));
    }
    log(level, message, details) {
        this.sdk.logger[level](message, details);
    }
    get webrtcSessions() {
        return this.sdk._streamingConnection.webrtcSessions;
    }
    async addAllowedSessionType(sessionType) {
        const handler = this.getSessionHandler({ sessionType });
        return handler.enableHandler();
    }
    async removeAllowedSessionType(sessionType) {
        const handler = this.getSessionHandler({ sessionType });
        return handler.disableHandler();
    }
    handleConversationUpdate(update) {
        const sessions = this.getAllSessions();
        /* let each enabled handler process updates */
        this.sessionHandlers
            .filter(handler => !handler.disabled)
            .forEach(handler => handler.handleConversationUpdate(update, sessions.filter(s => s.sessionType === handler.sessionType)));
    }
    handleConversationUpdateRaw(update) {
        /* let each enabled handler process updates */
        this.sessionHandlers
            .filter(handler => !handler.disabled)
            .forEach(handler => handler.handleConversationUpdateRaw(update));
    }
    getPendingSession(params) {
        const session = this.pendingSessions
            .find(s => {
            // if sessionId is provided use that exclusively
            if (params.sessionId) {
                return s.id === params.sessionId;
            }
            let matchesSessionType = true;
            if (params.sessionType) {
                matchesSessionType = s.sessionType === params.sessionType;
            }
            return matchesSessionType && s.conversationId === params.conversationId;
        });
        return session;
    }
    removePendingSession(params) {
        const pendingSession = this.getPendingSession(params);
        if (!pendingSession) {
            this.sdk.logger.warn('failed to find pendingSession to remove', params);
            return;
        }
        this.pendingSessions = this.pendingSessions.filter(s => s !== pendingSession);
    }
    getSession(params) {
        let sessionTypesToSearch;
        if (params.sessionType) {
            sessionTypesToSearch = [params.sessionType];
        }
        else {
            const sessionTypes = { ...enums_1.SessionTypes };
            if (!params.searchScreenRecordingSessions) {
                delete sessionTypes[enums_1.SessionTypes.screenRecording];
            }
            sessionTypesToSearch = Object.values(sessionTypes);
        }
        let session = this.getAllSessions()
            .find((s) => sessionTypesToSearch.includes(s.sessionType) && s.conversationId === params.conversationId);
        // search fake/shared softphone sessions
        if (!session && sessionTypesToSearch.includes(enums_1.SessionTypes.softphone)) {
            const softphoneHandler = this.getSessionHandler({ sessionType: enums_1.SessionTypes.softphone });
            session = Object.values(softphoneHandler.conversations).find((c) => c.conversationId === params.conversationId)?.session;
        }
        if (!session) {
            throw utils_1.createAndEmitSdkError.call(this.sdk, enums_1.SdkErrorTypes.session, 'Unable to find session', params);
        }
        return session;
    }
    getSessionBySessionId(sessionId) {
        return this.getAllSessions().find(s => s.id === sessionId);
    }
    getAllActiveSessions() {
        return this.webrtcSessions.getAllSessions()
            .filter((session) => session.state === 'active');
    }
    getAllActiveConversations() {
        return [].concat(...this.sessionHandlers.map(handler => handler.getActiveConversations()));
    }
    getAllSessions() {
        return this.webrtcSessions.getAllSessions();
    }
    getSessionHandler(params) {
        let handler;
        if (params.sessionType || params.sessionInfo?.sessionType) {
            handler = this.sessionHandlers.find((handler) => handler.sessionType == (params.sessionType || params.sessionInfo?.sessionType));
        }
        else {
            const fromJid = (params.sessionInfo && params.sessionInfo.fromJid) || (params.jingleSession && params.jingleSession.peerID);
            if (!fromJid) {
                throw utils_1.createAndEmitSdkError.call(this.sdk, enums_1.SdkErrorTypes.generic, 'getSessionHandler was called without any identifying information', params);
            }
            handler = this.sessionHandlers.find((handler) => handler.shouldHandleSessionByJid(fromJid));
        }
        if (!handler) {
            throw utils_1.createAndEmitSdkError.call(this.sdk, enums_1.SdkErrorTypes.session, 'Failed to find session handler for session', params);
        }
        return handler;
    }
    async startSession(startSessionParams) {
        if (!this.sdk.connected) {
            throw utils_1.createAndEmitSdkError.call(this.sdk, enums_1.SdkErrorTypes.session, 'A session cannot be started as streaming client is not yet connected', { sessionType: startSessionParams.sessionType });
        }
        const handler = this.getSessionHandler({ sessionType: startSessionParams.sessionType });
        if (handler.disabled) {
            throw utils_1.createAndEmitSdkError.call(this.sdk, enums_1.SdkErrorTypes.generic, 'Cannot start a session with a disabled session handler', { startSessionParams, allowedSessionTypes: this.sdk._config.allowedSessionTypes });
        }
        return handler.startSession(startSessionParams);
    }
    /**
     * Update the outgoing media for a session.
     *
     * @param options for updating outgoing media
     */
    async updateOutgoingMedia(options) {
        const session = options.session || this.getSession({ conversationId: options.conversationId });
        const handler = this.getSessionHandler({ jingleSession: session });
        return handler.updateOutgoingMedia(session, options);
    }
    async updateOutgoingMediaForAllSessions(options) {
        const opts = options || {
            videoDeviceId: this.sdk._config.defaults.videoDeviceId,
            audioDeviceId: this.sdk._config.defaults.audioDeviceId
        };
        const sessions = this.getAllActiveSessions();
        this.log('info', 'Updating outgoing deviceId(s) for all active sessions', {
            sessionInfos: sessions.map(s => ({ sessionId: s.id, conversationId: s.conversationId, sessionType: s.sessionType })),
            videoDeviceId: opts.videoDeviceId,
            audioDeviceId: opts.audioDeviceId
        });
        const promises = sessions.map(session => {
            return this.updateOutgoingMedia({
                session,
                ...opts
            });
        });
        await Promise.all(promises);
    }
    updateAudioVolume(volume) {
        const sessions = this.getAllActiveSessions();
        this.log('info', 'Updating volume for all active sessions', {
            sessionInfos: sessions.map(s => ({ sessionId: s.id, conversationId: s.conversationId })),
            volume
        });
        sessions.forEach((session) => {
            const handler = this.getSessionHandler({ jingleSession: session });
            handler.updateAudioVolume(session, volume);
        });
    }
    async updateOutputDeviceForAllSessions(outputDeviceId) {
        const sessions = this.getAllActiveSessions().filter(s => s.sessionType !== enums_1.SessionTypes.acdScreenShare);
        const _outputDeviceId = this.sdk.media.getValidDeviceId('audiooutput', outputDeviceId, ...sessions) || '';
        const ids = sessions.map(s => ({ sessionId: s.id, conversationId: s.conversationId, sessionType: s.sessionType }));
        if (typeof outputDeviceId === 'string' && _outputDeviceId !== outputDeviceId) {
            this.log('warn', 'Output deviceId not found. Not updating output media', { sessions: ids, outputDeviceId });
            return;
        }
        this.log('info', 'Updating output deviceId for all active sessions', {
            sessions: ids,
            outputDeviceId: _outputDeviceId
        });
        const promises = sessions.map(session => {
            const handler = this.getSessionHandler({ jingleSession: session });
            return handler.updateOutputDevice(session, _outputDeviceId);
        });
        await Promise.all(promises);
    }
    async addOrReplaceTrackOnSession(track, session) {
        const handler = this.getSessionHandler({ jingleSession: session });
        await handler.addReplaceTrackToSession(session, track);
    }
    /**
     * Event handler for pending webrtc-sessions.
     * @param sessionInfo pending webrtc-session info
     */
    async onPropose(sessionInfo) {
        const handler = this.getSessionHandler({ sessionInfo });
        if (handler.disabled) {
            return;
        }
        (0, utils_1.logPendingSession)(this.sdk.logger, 'onPendingSession', sessionInfo);
        const { conversationId, sessionType } = sessionInfo;
        const existingSession = this.getPendingSession({ conversationId, sessionType });
        if (existingSession) {
            if (existingSession.sessionId !== sessionInfo.sessionId) {
                this.log('info', `found an existingSession matching propose's conversationId, updating existingSession.sessionId to match`, { existingSessionId: existingSession.sessionId, proposeSessionId: sessionInfo.sessionId, conversationId: sessionInfo.conversationId });
                existingSession.sessionId = sessionInfo.sessionId;
                existingSession.id = sessionInfo.id;
                if (existingSession.accepted) {
                    this.log('info', `updated existingSession was already accepted, "proceeding" again`, { sessionId: sessionInfo.sessionId, conversationId: sessionInfo.conversationId });
                    return handler.proceedWithSession(existingSession);
                }
            }
            (0, utils_1.logPendingSession)(this.sdk.logger, 'duplicate session invitation, ignoring', sessionInfo);
            return;
        }
        const pendingSession = {
            id: sessionInfo.sessionId,
            sessionId: sessionInfo.sessionId,
            autoAnswer: sessionInfo.autoAnswer,
            conversationId: sessionInfo.conversationId,
            sessionType: handler.sessionType,
            originalRoomJid: sessionInfo.originalRoomJid,
            fromUserId: sessionInfo.fromUserId,
            toJid: sessionInfo.toJid,
            fromJid: sessionInfo.fromJid,
            privAnswerMode: sessionInfo.privAnswerMode,
            meetingId: sessionInfo.meetingId
        };
        this.pendingSessions.push(pendingSession);
        await handler.handlePropose(pendingSession);
    }
    /**
     * If we get this event from streaming-client, that means the session was
     *  canceled. We no longer care if it was a LA of 1 session or not
     *  because streaming-client will only emit events for actual sessions
     *  and not our psuedo-events we emit for LA of 1 conversation
     *  events.
     * @param sessionId session id canceled by streaming client
     */
    onCancelPendingSession(sessionId, conversationId) {
        const pendingSession = this.getPendingSession({ sessionId, conversationId });
        if (!pendingSession) {
            return;
        }
        this.sdk.emit('cancelPendingSession', { sessionId, conversationId: pendingSession.conversationId });
        this.removePendingSession(pendingSession);
    }
    /**
     * If we get this event from streaming-client, that means the session was
     *  canceled. We no longer care if it was a LA of 1 session or not
     *  because streaming-client will only emit events for actual sessions
     *  and not our psuedo-events we emit for LA of 1 conversation
     *  events.
     * @param sessionId session id canceled by streaming client
     */
    onHandledPendingSession(sessionId, conversationId) {
        const pendingSession = this.getPendingSession({ sessionId, conversationId });
        if (!pendingSession) {
            return;
        }
        this.sdk.emit('handledPendingSession', { sessionId, conversationId: pendingSession.conversationId });
        this.removePendingSession(pendingSession);
    }
    async proceedWithSession(params) {
        const pendingSession = this.getPendingSession(params);
        if (!pendingSession) {
            throw utils_1.createAndEmitSdkError.call(this.sdk, enums_1.SdkErrorTypes.session, 'Could not find a pendingSession matching accept params', { params });
        }
        const sessionHandler = this.getSessionHandler({ sessionType: pendingSession.sessionType });
        await sessionHandler.proceedWithSession(pendingSession);
    }
    async rejectPendingSession(params) {
        const pendingSession = this.getPendingSession(params);
        if (!pendingSession) {
            throw utils_1.createAndEmitSdkError.call(this.sdk, enums_1.SdkErrorTypes.session, 'Could not find a pendingSession', { params });
        }
        const sessionHandler = this.getSessionHandler({ sessionType: pendingSession.sessionType });
        await sessionHandler.rejectPendingSession(pendingSession);
    }
    async onSessionInit(session) {
        const sessionHandler = this.getSessionHandler({ jingleSession: session });
        if (sessionHandler.disabled) {
            return;
        }
        session.sessionType = sessionHandler.sessionType;
        return sessionHandler.handleSessionInit(session);
    }
    async acceptSession(params) {
        if (!params || !params.conversationId) {
            throw utils_1.createAndEmitSdkError.call(this.sdk, enums_1.SdkErrorTypes.invalid_options, 'A conversationId is required for acceptSession');
        }
        const session = this.getSession({ conversationId: params.conversationId, sessionType: params.sessionType, searchScreenRecordingSessions: true });
        if (session._alreadyAccepted) {
            this.log('info', 'acceptSession called for session that was already accepted, not accepting again.', { sessionId: session.id, conversationId: session.conversationId });
            return;
        }
        session._alreadyAccepted = true;
        const sessionHandler = this.getSessionHandler({ jingleSession: session });
        return sessionHandler.acceptSession(session, params);
    }
    async endSession(params) {
        const conversationId = params.conversationId;
        if (!conversationId) {
            throw utils_1.createAndEmitSdkError.call(this.sdk, enums_1.SdkErrorTypes.session, 'Unable to end session: must provide a conversationId.');
        }
        const session = this.getSession(params);
        const sessionHandler = this.getSessionHandler({ jingleSession: session });
        return sessionHandler.endSession(conversationId, session, params.reason);
    }
    async forceTerminateSession(sessionId, reason) {
        const session = this.getAllSessions().find((s) => s.id === sessionId);
        if (!session) {
            throw utils_1.createAndEmitSdkError.call(this.sdk, enums_1.SdkErrorTypes.generic, 'Failed to find session by sessionId', { sessionId });
        }
        const handler = this.getSessionHandler({ sessionType: session.sessionType });
        return handler.forceEndSession(session, reason);
    }
    async setVideoMute(params) {
        const session = this.getSession({ conversationId: params.conversationId });
        const handler = this.getSessionHandler({ sessionType: session.sessionType });
        await handler.setVideoMute(session, params);
    }
    async setAudioMute(params) {
        const session = this.getSession({ conversationId: params.conversationId });
        const handler = this.getSessionHandler({ sessionType: session.sessionType });
        await handler.setAudioMute(session, params);
    }
    async setConversationHeld(params) {
        const session = this.getSession({ conversationId: params.conversationId });
        const handler = this.getSessionHandler({ sessionType: session.sessionType });
        await handler.setConversationHeld(session, params);
    }
    async validateOutgoingMediaTracks() {
        const sessions = this.getAllActiveSessions();
        const { videoDevices, audioDevices, outputDevices, hasOutputDeviceSupport } = this.sdk.media.getState();
        const updates = new Map();
        const promises = [];
        let updateOutputDeviceForAllSessions = false;
        /* find all sessions that ned to be updated */
        for (const session of sessions) {
            const trackIdsToIgnore = [];
            /* if we have a video session with a screenShareStream */
            if (session._screenShareStream) {
                trackIdsToIgnore.push(...session._screenShareStream.getTracks().map((track) => track.id));
            }
            session.peerConnection.getSenders()
                .filter((sender) => sender.track && !trackIdsToIgnore.includes(sender.track.id))
                .map(s => s.track)
                .forEach(track => {
                /* senders won't be using output devices so we don't need to worry about those */
                const deviceExists = !!(track.kind === 'video' ? videoDevices : audioDevices).find(d => d.label === track.label && d.kind.slice(0, 5) === track.kind);
                if (deviceExists) {
                    this.log('debug', 'sessions outgoing track still has available device', { conversationId: session.conversationId, sessionId: session.id, sessionType: session.sessionType, kind: track.kind, deviceLabel: track.label });
                    return;
                }
                const currVal = updates.get(session.id) || {};
                currVal[track.kind] = true;
                updates.set(session.id, currVal);
                this.log('info', 'session lost media device and will attempt to switch devices', { conversationId: session.conversationId, sessionId: session.id, sessionType: session.sessionType, kind: track.kind, deviceLabel: track.label });
            });
            /* check output device */
            if (hasOutputDeviceSupport && session._outputAudioElement) {
                const deviceExists = outputDevices.find(d => d.deviceId === session._outputAudioElement.sinkId);
                if (!deviceExists) {
                    updateOutputDeviceForAllSessions = true;
                    this.log('info', 'session lost output device and will attempt to switch device', { conversationId: session.conversationId, sessionId: session.id, kind: 'output', sessionType: session.sessionType });
                }
            }
        }
        /* if there are not sessions to updated, log and we are done */
        if (!updates.size && !updateOutputDeviceForAllSessions) {
            this.log('debug', 'no active sessions have outgoing tracks that need to have the device updated', { sessionIds: sessions.map(s => s.id) });
            return;
        }
        /* update the sessions */
        for (const [sessionId, mediaToUpdate] of updates) {
            const jingleSession = this.getSessionBySessionId(sessionId);
            const opts = { session: jingleSession };
            const handler = this.getSessionHandler({ jingleSession });
            /* if our video needs to be updated */
            if (mediaToUpdate.video) {
                /* if we have devices, let the sdk figure out which to switch to */
                if (videoDevices.length) {
                    opts.videoDeviceId = true;
                }
                else {
                    this.log('warn', 'no available video devices to switch to. setting video to mute for session', { conversationId: jingleSession.conversationId, sessionId, kind: 'video', sessionType: jingleSession.sessionType });
                    promises.push(handler.setVideoMute(jingleSession, { mute: true, conversationId: jingleSession.conversationId }));
                }
            }
            /* if our audio needs to be updated */
            if (mediaToUpdate.audio) {
                if (audioDevices.length) {
                    opts.audioDeviceId = true;
                }
                else {
                    this.log('warn', 'no available audio devices to switch to. setting audio to mute for session', { conversationId: jingleSession.conversationId, sessionId, kind: 'audio', sessionType: jingleSession.sessionType });
                    promises.push(handler.setAudioMute(jingleSession, { mute: true, conversationId: jingleSession.conversationId }));
                    const senders = handler.getSendersByTrackType(jingleSession, 'audio')
                        .filter((sender) => sender.track);
                    senders.forEach((sender) => {
                        sender.track.stop();
                        promises.push(handler.removeMediaFromSession(jingleSession, sender));
                        jingleSession._outboundStream.removeTrack(sender.track);
                    });
                }
            }
            /* update outgoing media */
            if (opts.videoDeviceId || opts.audioDeviceId) {
                promises.push(this.sdk.updateOutgoingMedia(opts));
            }
        }
        /* if the output device needs to change, update all sessions */
        if (updateOutputDeviceForAllSessions) {
            promises.push(this.updateOutputDeviceForAllSessions(true));
        }
        return Promise.all(promises);
    }
}
exports.SessionManager = SessionManager;
//# sourceMappingURL=session-manager.js.map