// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { Features } from '@azure/communication-calling';
import { toFlatCommunicationIdentifier } from "../../acs-ui-common/src";
import { CaptionsFeatureSubscriber } from './CaptionsSubscriber';
import { convertSdkLocalStreamToDeclarativeLocalStream, convertSdkParticipantToDeclarativeParticipant } from './Converter';
import { LocalVideoStreamVideoEffectsSubscriber } from './LocalVideoStreamVideoEffectsSubscriber';
import { ParticipantSubscriber } from './ParticipantSubscriber';
import { RecordingSubscriber } from './RecordingSubscriber';
import { PPTLiveSubscriber } from './PPTLiveSubscriber';
import { disposeView } from './StreamUtils';
import { TranscriptionSubscriber } from './TranscriptionSubscriber';
import { UserFacingDiagnosticsSubscriber } from './UserFacingDiagnosticsSubscriber';
import { RaiseHandSubscriber } from './RaiseHandSubscriber';
import { OptimalVideoCountSubscriber } from './OptimalVideoCountSubscriber';
import { CapabilitiesSubscriber } from './CapabilitiesSubscriber';
import { ReactionSubscriber } from './ReactionSubscriber';
import { SpotlightSubscriber } from './SpotlightSubscriber';
import { BreakoutRoomsSubscriber } from './BreakoutRoomsSubscriber';
import { TogetherModeSubscriber } from './TogetherModeSubscriber';
import { MediaAccessSubscriber } from './MediaAccessSubscriber';
import { _isTeamsMeeting } from './TypeGuards';
import { RealTimeTextSubscriber } from './RealTimeTextSubscriber';
/**
 * Keeps track of the listeners assigned to a particular call because when we get an event from SDK, it doesn't tell us
 * which call it is for. If we keep track of this then we know which call in the state that needs an update and also
 * which property of that call. Also we can use this when unregistering to a call.
 */
export class CallSubscriber {
    constructor(call, context, internalContext) {
        this._safeSubscribeInitCaptionSubscriber = () => {
            this._safeSubscribe(this.initCaptionSubscriber);
        };
        this._safeSubscribeInitTeamsMeetingConference = () => {
            this._safeSubscribe(this.initTeamsMeetingConference);
        };
        this._safeSubscribeInitRealTimeText = () => {
            this._safeSubscribe(this.initRealTimeText);
        };
        this.subscribe = () => {
            this._call.on('stateChanged', this.stateChanged);
            this._call.on('stateChanged', this._safeSubscribeInitCaptionSubscriber);
            this._call.on('stateChanged', this._safeSubscribeInitTeamsMeetingConference);
            this._call.on('idChanged', this.idChanged);
            this._call.on('isScreenSharingOnChanged', this.isScreenSharingOnChanged);
            this._call.on('remoteParticipantsUpdated', this.remoteParticipantsUpdated);
            this._call.on('localVideoStreamsUpdated', this.localVideoStreamsUpdated);
            this._call.on('isMutedChanged', this.isMuteChanged);
            this._call.on('roleChanged', this.callRoleChangedHandler);
            this._call.feature(Features.DominantSpeakers).on('dominantSpeakersChanged', this.dominantSpeakersChanged);
            this._call.on('mutedByOthers', this.mutedByOthersHandler);
            this._safeSubscribeInitRealTimeText();
            for (const localVideoStream of this._call.localVideoStreams) {
                this._internalContext.setLocalRenderInfo(this._callIdRef.callId, localVideoStream.mediaStreamType, localVideoStream, 'NotRendered', undefined);
            }
            if (this._call.remoteParticipants.length > 0) {
                this._call.remoteParticipants.forEach((participant) => {
                    this.addParticipantListener(participant);
                });
                this._context.setCallRemoteParticipants(this._callIdRef.callId, this._call.remoteParticipants.map(convertSdkParticipantToDeclarativeParticipant), []);
            }
        };
        this.unsubscribe = () => {
            var _a, _b, _c;
            this._call.off('stateChanged', this.stateChanged);
            this._call.off('stateChanged', this._safeSubscribeInitCaptionSubscriber);
            this._call.off('stateChanged', this._safeSubscribeInitTeamsMeetingConference);
            this._call.off('idChanged', this.idChanged);
            this._call.off('isScreenSharingOnChanged', this.isScreenSharingOnChanged);
            this._call.off('remoteParticipantsUpdated', this.remoteParticipantsUpdated);
            this._call.off('localVideoStreamsUpdated', this.localVideoStreamsUpdated);
            this._call.off('isMutedChanged', this.isMuteChanged);
            this._call.off('roleChanged', this.callRoleChangedHandler);
            this._call.off('mutedByOthers', this.mutedByOthersHandler);
            this._participantSubscribers.forEach((participantSubscriber) => {
                participantSubscriber.unsubscribe();
            });
            this._participantSubscribers.clear();
            // If we are unsubscribing that means we no longer want to display any video for this call (callEnded or callAgent
            // disposed) and we should not be updating it any more. So if video is rendering we stop rendering.
            for (const localVideoStream of this._call.localVideoStreams) {
                const mediaStreamType = localVideoStream.mediaStreamType;
                disposeView(this._context, this._internalContext, this._callIdRef.callId, undefined, convertSdkLocalStreamToDeclarativeLocalStream(localVideoStream));
                this._internalContext.deleteLocalRenderInfo(this._callIdRef.callId, mediaStreamType);
            }
            this._diagnosticsSubscriber.unsubscribe();
            this._recordingSubscriber.unsubscribe();
            this._transcriptionSubscriber.unsubscribe();
            this._optimalVideoCountSubscriber.unsubscribe();
            this._pptLiveSubscriber.unsubscribe();
            (_a = this._CaptionsFeatureSubscriber) === null || _a === void 0 ? void 0 : _a.unsubscribe();
            if (this._realTimeTextSubscriber) {
                this._realTimeTextSubscriber.unsubscribe();
            }
            (_b = this._raiseHandSubscriber) === null || _b === void 0 ? void 0 : _b.unsubscribe();
            this._capabilitiesSubscriber.unsubscribe();
            (_c = this._reactionSubscriber) === null || _c === void 0 ? void 0 : _c.unsubscribe();
            this._spotlightSubscriber.unsubscribe();
            this._breakoutRoomsSubscriber.unsubscribe();
            this._togetherModeSubscriber.unsubscribe();
            this._mediaAccessSubscriber.unsubscribe();
        };
        this.stateChanged = () => {
            this._context.setCallState(this._callIdRef.callId, this._call.state);
        };
        this.initCaptionSubscriber = () => {
            // subscribe to captions here so that we don't call captions when call is not initialized
            if (this._call.state === 'Connected' && !this._CaptionsFeatureSubscriber) {
                this._CaptionsFeatureSubscriber = new CaptionsFeatureSubscriber(this._callIdRef, this._context, this._call.feature(Features.Captions));
                this._call.off('stateChanged', this._safeSubscribeInitCaptionSubscriber);
            }
        };
        this.initTeamsMeetingConference = () => {
            if (this._call.state === 'Connected' && _isTeamsMeeting(this._call)) {
                this._call.feature(Features.TeamsMeetingAudioConferencing).getTeamsMeetingAudioConferencingDetails().then(teamsMeetingConferenceDetails => {
                    this._context.setTeamsMeetingConference(this._callIdRef.callId, teamsMeetingConferenceDetails);
                });
                this._call.off('stateChanged', this._safeSubscribeInitTeamsMeetingConference);
            }
        };
        this.initRealTimeText = () => {
            if (this._context.getState().userId.kind !== 'microsoftTeamsUser') {
                this._realTimeTextSubscriber = new RealTimeTextSubscriber(this._callIdRef, this._context, this._call.feature(Features.RealTimeText));
            }
        };
        this.idChanged = () => {
            this._internalContext.setCallId(this._call.id, this._callIdRef.callId);
            this._context.setCallId(this._call.id, this._callIdRef.callId);
            this._callIdRef.callId = this._call.id;
        };
        this.isScreenSharingOnChanged = () => {
            this._context.setCallIsScreenSharingOn(this._callIdRef.callId, this._call.isScreenSharingOn);
        };
        this.isMuteChanged = () => {
            this._context.setCallIsMicrophoneMuted(this._callIdRef.callId, this._call.isMuted);
        };
        this.callRoleChangedHandler = () => {
            this._context.setRole(this._callIdRef.callId, this._call.role);
        };
        // TODO: Tee to notification state once available
        this.mutedByOthersHandler = () => {
            this._context.teeErrorToState({
                name: 'mutedByOthers',
                message: 'Muted by another participant'
            }, 'Call.mutedByOthers');
        };
        this.remoteParticipantsUpdated = (event) => {
            event.added.forEach((participant) => {
                this.addParticipantListener(participant);
            });
            event.removed.forEach((participant) => {
                this.removeParticipantListener(participant);
            });
            // Remove any added participants from remoteParticipantsEnded if they are there and add any removed participants to
            // remoteParticipantsEnded.
            this._context.setCallRemoteParticipantsEnded(this._callIdRef.callId, event.removed.map(convertSdkParticipantToDeclarativeParticipant), event.added.map((participant) => {
                return toFlatCommunicationIdentifier(participant.identifier);
            }));
            // Add added participants to remoteParticipants and remove removed participants from remoteParticipants.
            this._context.setCallRemoteParticipants(this._callIdRef.callId, event.added.map(convertSdkParticipantToDeclarativeParticipant), event.removed.map((participant) => {
                return toFlatCommunicationIdentifier(participant.identifier);
            }));
        };
        this.localVideoStreamsUpdated = (event) => {
            var _a, _b;
            for (const localVideoStream of event.added) {
                const mediaStreamType = localVideoStream.mediaStreamType;
                // IMPORTANT: The internalContext should be set before context. This is done to ensure that the internal context
                // has the required data when component re-renders due to external state changes.
                this._internalContext.setLocalRenderInfo(this._callIdRef.callId, mediaStreamType, localVideoStream, 'NotRendered', undefined);
                // Subscribe to video effect changes
                (_a = this._localVideoStreamVideoEffectsSubscribers.get(mediaStreamType)) === null || _a === void 0 ? void 0 : _a.unsubscribe();
                this._localVideoStreamVideoEffectsSubscribers.set(mediaStreamType, new LocalVideoStreamVideoEffectsSubscriber({
                    parent: this._callIdRef,
                    context: this._context,
                    localVideoStream: localVideoStream,
                    localVideoStreamEffectsAPI: localVideoStream.feature(Features.VideoEffects)
                }));
            }
            for (const localVideoStream of event.removed) {
                const mediaStreamType = localVideoStream.mediaStreamType;
                (_b = this._localVideoStreamVideoEffectsSubscribers.get(mediaStreamType)) === null || _b === void 0 ? void 0 : _b.unsubscribe();
                disposeView(this._context, this._internalContext, this._callIdRef.callId, undefined, convertSdkLocalStreamToDeclarativeLocalStream(localVideoStream));
                this._internalContext.deleteLocalRenderInfo(this._callIdRef.callId, mediaStreamType);
            }
            this._context.setCallLocalVideoStream(this._callIdRef.callId, event.added.map(convertSdkLocalStreamToDeclarativeLocalStream), event.removed.map(convertSdkLocalStreamToDeclarativeLocalStream));
        };
        this.dominantSpeakersChanged = () => {
            const dominantSpeakers = this._call.feature(Features.DominantSpeakers).dominantSpeakers;
            this._context.setCallDominantSpeakers(this._callIdRef.callId, dominantSpeakers);
        };
        this._call = call;
        this._callIdRef = {
            callId: call.id
        };
        this._context = context;
        this._internalContext = internalContext;
        this._diagnosticsSubscriber = new UserFacingDiagnosticsSubscriber(this._callIdRef, this._context, this._call.feature(Features.UserFacingDiagnostics));
        this._participantSubscribers = new Map();
        this._recordingSubscriber = new RecordingSubscriber(this._callIdRef, this._context, this._call.feature(Features.Recording));
        this._pptLiveSubscriber = new PPTLiveSubscriber(this._callIdRef, this._context, this._call);
        this._transcriptionSubscriber = new TranscriptionSubscriber(this._callIdRef, this._context, this._call.feature(Features.Transcription));
        this._raiseHandSubscriber = new RaiseHandSubscriber(this._callIdRef, this._context, this._call.feature(Features.RaiseHand));
        this._reactionSubscriber = new ReactionSubscriber(this._callIdRef, this._context, this._call.feature(Features.Reaction));
        this._optimalVideoCountSubscriber = new OptimalVideoCountSubscriber({
            callIdRef: this._callIdRef,
            context: this._context,
            localOptimalVideoCountFeature: this._call.feature(Features.OptimalVideoCount)
        });
        this._localVideoStreamVideoEffectsSubscribers = new Map();
        this._capabilitiesSubscriber = new CapabilitiesSubscriber(this._callIdRef, this._context, this._call.feature(Features.Capabilities));
        this._spotlightSubscriber = new SpotlightSubscriber(this._callIdRef, this._context, this._call.feature(Features.Spotlight));
        // Clear assigned breakout room closed notification for this call.
        this._context.deleteLatestNotification('assignedBreakoutRoomClosed');
        this._breakoutRoomsSubscriber = new BreakoutRoomsSubscriber(this._callIdRef, this._context, this._call.feature(Features.BreakoutRooms));
        this._togetherModeSubscriber = new TogetherModeSubscriber(this._callIdRef, this._context, this._internalContext, this._call.feature(Features.TogetherMode));
        this._mediaAccessSubscriber = new MediaAccessSubscriber(this._callIdRef, this._context, this._call.feature(Features.MediaAccess));
        this.subscribe();
    }
    // This is a helper function to safely call subscriber functions. This is needed in order to prevent events
    // with the same event type from failing to fire due to a subscriber throwing an error upon subscription.
    // Wrap your listeners with this helper function if your listener can fail due to initialization or fail
    // during a function call. This will prevent other events using the same event type from failing to fire.
    _safeSubscribe(subscriber) {
        try {
            subscriber();
        }
        catch (e) {
            this._context.teeErrorToState(e, 'Call.on');
        }
    }
    addParticipantListener(participant) {
        var _a;
        const participantKey = toFlatCommunicationIdentifier(participant.identifier);
        (_a = this._participantSubscribers.get(participantKey)) === null || _a === void 0 ? void 0 : _a.unsubscribe();
        this._participantSubscribers.set(participantKey, new ParticipantSubscriber(this._callIdRef, participant, this._context, this._internalContext));
    }
    removeParticipantListener(participant) {
        const participantKey = toFlatCommunicationIdentifier(participant.identifier);
        const participantSubscriber = this._participantSubscribers.get(participantKey);
        if (participantSubscriber) {
            participantSubscriber.unsubscribe();
            this._participantSubscribers.delete(participantKey);
        }
    }
}
//# sourceMappingURL=CallSubscriber.js.map