// 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 { LocalVideoStream, VideoStreamRenderer } from '@azure/communication-calling';
import { convertSdkLocalStreamToDeclarativeLocalStream, convertSdkRemoteStreamToDeclarativeRemoteStream, convertFromSDKToDeclarativeVideoStreamRendererView } from './Converter';
import { toFlatCommunicationIdentifier } from "../../acs-ui-common/src";
import { EventNames } from './Logger';
import { _logStreamEvent } from './StreamUtilsLogging';
function createViewVideo(context, internalContext, callId, stream, participantId, options) {
    return __awaiter(this, void 0, void 0, function* () {
        // we can only have 3 types of createView
        let streamEventType;
        // we will reuse these for local as well but we need to make sure the remote stream is passed in like before.
        if (participantId) {
            streamEventType = 'createViewRemote';
        }
        else if (callId) {
            streamEventType = 'createViewLocal';
        }
        else {
            // TODO update for when unparented view.
            throw new Error('unparented createView not implemented yet here');
            streamEventType = 'createViewUnparented';
        }
        const streamType = stream === null || stream === void 0 ? void 0 : stream.mediaStreamType;
        const localStreamKey = stream.mediaStreamType;
        const remoteStreamId = stream.id;
        // we want to check to see if there is a participantId this will tell us whether its a local stream or a remote one.
        const participantKey = streamEventType === 'createViewRemote' && participantId ? typeof participantId === 'string' ? participantId : toFlatCommunicationIdentifier(participantId) : undefined;
        const streamLogInfo = {
            callId,
            participantKey,
            streamId: remoteStreamId !== null && remoteStreamId !== void 0 ? remoteStreamId : localStreamKey,
            streamType,
            streamEventType
        };
        // make different logging announcement based on whether or not we are starting a local or remote
        _logStreamEvent(EventNames.CREATING_VIEW, streamLogInfo);
        // if we have a participant Id and a stream get the remote info, else get the local render info from state.
        const renderInfo = streamEventType === 'createViewRemote' && participantKey ? internalContext.getRemoteRenderInfoForParticipant(callId, participantKey, remoteStreamId) : internalContext.getLocalRenderInfo(callId, localStreamKey);
        if (!renderInfo) {
            _logStreamEvent(EventNames.STREAM_NOT_FOUND, streamLogInfo);
            return;
        }
        if (renderInfo.status === 'Rendered') {
            _logStreamEvent(EventNames.STREAM_ALREADY_RENDERED, streamLogInfo);
            return;
        }
        if (renderInfo.status === 'Rendering') {
            // Do not log to console here as this is a very common situation due to UI rerenders while
            // the video rendering is in progress.
            _logStreamEvent(EventNames.STREAM_RENDERING, streamLogInfo);
            return;
        }
        // "Stopping" only happens if the stream was in "rendering" but `disposeView` was called.
        // Now that `createView` has been re-called, we can flip the state back to "rendering".
        if (renderInfo.status === 'Stopping') {
            if (streamEventType === 'createViewRemote' && participantKey) {
                _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo);
                internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, renderInfo.stream, 'Rendering', renderInfo.renderer);
            }
            else if (streamEventType === 'createViewLocal') {
                _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo);
                internalContext.setLocalRenderInfo(callId, localStreamKey, renderInfo.stream, 'Rendering', renderInfo.renderer);
            }
            return;
        }
        const renderer = new VideoStreamRenderer(renderInfo.stream);
        if (streamEventType === 'createViewRemote' && participantKey) {
            internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, renderInfo.stream, 'Rendering', undefined);
        }
        else {
            internalContext.setLocalRenderInfo(callId, localStreamKey, renderInfo.stream, 'Rendering', renderer);
        }
        let view;
        try {
            view = yield renderer.createView(options);
        }
        catch (e) {
            if (streamEventType === 'createViewRemote' && participantKey) {
                _logStreamEvent(EventNames.CREATE_STREAM_FAIL, streamLogInfo);
                internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, renderInfo.stream, 'NotRendered', undefined);
            }
            else if (streamEventType === 'createViewLocal') {
                _logStreamEvent(EventNames.CREATE_STREAM_FAIL, streamLogInfo, e);
                internalContext.setLocalRenderInfo(callId, localStreamKey, renderInfo.stream, 'NotRendered', undefined);
            }
            throw e;
        }
        // Since render could take some time, we need to check if the stream is still valid and if we received a signal to
        // stop rendering.
        const refreshedRenderInfo = streamEventType === 'createViewRemote' && participantKey ? internalContext.getRemoteRenderInfoForParticipant(callId, participantKey, remoteStreamId) : internalContext.getLocalRenderInfo(callId, localStreamKey);
        if (!refreshedRenderInfo) {
            // RenderInfo was removed. This should not happen unless stream was removed from the call so dispose the renderer
            // and clean up the state.
            _logStreamEvent(EventNames.RENDER_INFO_NOT_FOUND, streamLogInfo);
            renderer.dispose();
            if (streamEventType === 'createViewRemote' && participantKey) {
                context.setRemoteVideoStreamRendererView(callId, participantKey, remoteStreamId, undefined);
            }
            else {
                context.setLocalVideoStreamRendererView(callId, localStreamKey, undefined);
            }
            return;
        }
        if (refreshedRenderInfo.status === 'Stopping') {
            // Stop render was called on this stream after we had started rendering. We will dispose this view and do not
            // put the view into the state.
            _logStreamEvent(EventNames.CREATED_STREAM_STOPPING, streamLogInfo);
            renderer.dispose();
            if (streamEventType === 'createViewRemote' && participantKey) {
                internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, refreshedRenderInfo.stream, 'NotRendered', undefined);
                context.setRemoteVideoStreamRendererView(callId, participantKey, remoteStreamId, undefined);
            }
            else if (streamEventType === 'createViewLocal') {
                internalContext.setLocalRenderInfo(callId, localStreamKey, refreshedRenderInfo.stream, 'NotRendered', undefined);
                context.setLocalVideoStreamRendererView(callId, localStreamKey, undefined);
            }
            return;
        }
        // Else the stream still exists and status is not telling us to stop rendering. Complete the render process by
        // updating the state.
        if (streamEventType === 'createViewRemote' && participantKey) {
            internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, refreshedRenderInfo.stream, 'Rendered', renderer);
            context.setRemoteVideoStreamRendererView(callId, participantKey, remoteStreamId, convertFromSDKToDeclarativeVideoStreamRendererView(view));
            _logStreamEvent(EventNames.VIEW_RENDER_SUCCEED, streamLogInfo);
        }
        else if (streamEventType === 'createViewLocal') {
            internalContext.setLocalRenderInfo(callId, localStreamKey, refreshedRenderInfo.stream, 'Rendered', renderer);
            context.setLocalVideoStreamRendererView(callId, localStreamKey, convertFromSDKToDeclarativeVideoStreamRendererView(view));
            _logStreamEvent(EventNames.VIEW_RENDER_SUCCEED, streamLogInfo);
        }
        return {
            renderer,
            view
        };
    });
}
function createViewUnparentedVideo(context, internalContext, stream, options) {
    return __awaiter(this, void 0, void 0, function* () {
        const renderInfo = internalContext.getUnparentedRenderInfo(stream);
        if (renderInfo && renderInfo.status === 'Rendered') {
            console.warn('Unparented LocalVideoStream is already rendered');
            return;
        }
        if (renderInfo && renderInfo.status === 'Rendering') {
            // Do not log to console here as this is a very common situation due to UI rerenders while
            // the video rendering is in progress.
            return;
        }
        if (renderInfo && renderInfo.status === 'Stopping') {
            console.warn('Unparented LocalVideoStream is in the middle of stopping');
            return;
        }
        const localVideoStream = new LocalVideoStream(stream.source);
        const renderer = new VideoStreamRenderer(localVideoStream);
        internalContext.setUnparentedRenderInfo(stream, localVideoStream, 'Rendering', undefined);
        let view;
        try {
            view = yield renderer.createView(options);
        }
        catch (e) {
            // Special case for unparented views. Since they are not tied to anything and created by us based on the calls to
            // this function we'll delete it to clean up the data since keeping it around doesn't help us and if developer wants
            // to create a new view they can check that the view is not rendered and call this function again.
            internalContext.deleteUnparentedRenderInfo(stream);
            throw e;
        }
        // Since render could take some time, we need to check if the stream is still valid and if we received a signal to
        // stop rendering.
        const refreshedRenderInfo = internalContext.getUnparentedRenderInfo(stream);
        if (!refreshedRenderInfo) {
            // Unparented stream's RenderInfo was deleted. Currently this shouldn't happen but if it does we'll just dispose the
            // renderer and clean up state. If developer wanted the stream they could call this function again and that should
            // generate new working state via this function.
            renderer.dispose();
            context.deleteDeviceManagerUnparentedView(stream);
            return;
        }
        if (refreshedRenderInfo.status === 'Stopping') {
            // Stop render was called on this stream after we had started rendering. We will dispose this view and do not
            // put the view into the state. Special case for unparented views, delete them from state when stopped to free up
            // the memory since we were the ones generating this and not tied to any Call state.
            internalContext.deleteUnparentedRenderInfo(stream);
            context.deleteDeviceManagerUnparentedView(stream);
            return;
        }
        // Else the stream still exists and status is not telling us to stop rendering. Complete the render process by
        // updating the state.
        internalContext.setUnparentedRenderInfo(stream, localVideoStream, 'Rendered', renderer);
        internalContext.subscribeToUnparentedViewVideoEffects(localVideoStream, context);
        context.setDeviceManagerUnparentedView(stream, convertFromSDKToDeclarativeVideoStreamRendererView(view));
        return {
            renderer,
            view
        };
    });
}
function disposeViewVideo(context, internalContext, callId, stream, participantId) {
    // we can only have 3 types of createView
    let streamEventType;
    // we will reuse these for local as well but we need to make sure the remote stream is passed in like before.
    if (participantId) {
        streamEventType = 'disposeViewRemote';
    }
    else if (callId) {
        streamEventType = 'disposeViewLocal';
    }
    else {
        // TODO update for when unparented view.
        streamEventType = 'disposeViewUnparented';
    }
    const streamType = stream.mediaStreamType;
    const localStreamKey = stream.mediaStreamType;
    const remoteStreamId = stream.id;
    // we want to check to see if there is a participantId this will tell us whether its a local stream or a remote one.
    const participantKey = streamEventType === 'disposeViewRemote' && participantId ? typeof participantId === 'string' ? participantId : toFlatCommunicationIdentifier(participantId) : undefined;
    const streamLogInfo = {
        callId,
        participantKey,
        streamId: remoteStreamId !== null && remoteStreamId !== void 0 ? remoteStreamId : localStreamKey,
        streamType
    };
    _logStreamEvent(EventNames.START_DISPOSE_STREAM, streamLogInfo);
    if (streamEventType === 'disposeViewRemote' && participantKey) {
        context.setRemoteVideoStreamRendererView(callId, participantKey, remoteStreamId, undefined);
    }
    const renderInfo = streamEventType === 'disposeViewRemote' && participantKey ? internalContext.getRemoteRenderInfoForParticipant(callId, participantKey, remoteStreamId) : internalContext.getLocalRenderInfo(callId, localStreamKey);
    if (!renderInfo) {
        _logStreamEvent(EventNames.DISPOSE_INFO_NOT_FOUND, streamLogInfo);
        return;
    }
    // Nothing to dispose of or clean up -- we can safely exit early here.
    if (renderInfo.status === 'NotRendered') {
        _logStreamEvent(EventNames.STREAM_ALREADY_DISPOSED, streamLogInfo);
        return;
    }
    // Status is already marked as "stopping" so we can exit early here. This is because stopping only occurs
    // when the stream is being created in createView but hasn't been completed being created yet. The createView
    // method will see the "stopping" status and perform the cleanup
    if (renderInfo.status === 'Stopping') {
        _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo);
        return;
    }
    // If the stream is in the middle of being rendered (i.e. has state "Rendering"), we need the status as
    // "stopping" without performing any cleanup. This will tell the `createView` method that it should stop
    // rendering and clean up the state once the view has finished being created.
    if (renderInfo.status === 'Rendering') {
        _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo);
        if (streamEventType === 'disposeViewRemote' && participantKey) {
            internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, renderInfo.stream, 'Stopping', undefined);
        }
        else {
            internalContext.setLocalRenderInfo(callId, localStreamKey, renderInfo.stream, 'Stopping', renderInfo.renderer);
        }
        return;
    }
    if (renderInfo.renderer) {
        _logStreamEvent(EventNames.DISPOSING_RENDERER, streamLogInfo);
        renderInfo.renderer.dispose();
        // Else the state must be in the "Rendered" state, so we can dispose the renderer and clean up the state.
        if (streamEventType === 'disposeViewRemote' && participantKey) {
            internalContext.setRemoteRenderInfo(callId, participantKey, remoteStreamId, renderInfo.stream, 'NotRendered', undefined);
        }
        else if (streamEventType === 'disposeViewLocal') {
            internalContext.setLocalRenderInfo(callId, localStreamKey, renderInfo.stream, 'NotRendered', undefined);
            context.setLocalVideoStreamRendererView(callId, localStreamKey, undefined);
        }
    }
    else {
        _logStreamEvent(EventNames.RENDERER_NOT_FOUND, streamLogInfo);
    }
}
function disposeViewUnparentedVideo(context, internalContext, stream) {
    const streamType = stream.mediaStreamType;
    const streamLogInfo = {
        streamType,
        streamEventType: 'disposeViewUnparented'
    };
    _logStreamEvent(EventNames.START_DISPOSE_STREAM, streamLogInfo);
    context.deleteDeviceManagerUnparentedView(stream);
    const renderInfo = internalContext.getUnparentedRenderInfo(stream);
    if (!renderInfo) {
        _logStreamEvent(EventNames.DISPOSE_INFO_NOT_FOUND, streamLogInfo);
        return;
    }
    if (renderInfo.status === 'Rendering') {
        _logStreamEvent(EventNames.STREAM_STOPPING, streamLogInfo);
        internalContext.setUnparentedRenderInfo(stream, renderInfo.stream, 'Stopping', undefined);
    }
    else {
        internalContext.deleteUnparentedRenderInfo(stream);
    }
    if (renderInfo.renderer) {
        _logStreamEvent(EventNames.DISPOSING_RENDERER, streamLogInfo);
        renderInfo.renderer.dispose();
    }
    else {
        _logStreamEvent(EventNames.RENDERER_NOT_FOUND, streamLogInfo);
    }
}
/**
 * @private
 */
export function createView(context, internalContext, callId, participantId, stream, options) {
    const streamType = stream.mediaStreamType;
    if (callId) {
        return createViewVideo(context, internalContext, callId, stream, participantId, options);
    }
    else if (!('id' in stream) && !callId) {
        // Render LocalVideoStream that is not part of a Call
        // Because it is not part of the call we don't tee errors to state naturally (e.g. via a Call Client function such as startVideo).
        // We do not have a startLocalPreviewVideo function, so as a workaround we ensure any errors are propagated here.
        return context.withAsyncErrorTeedToState(() => __awaiter(this, void 0, void 0, function* () { return yield createViewUnparentedVideo(context, internalContext, stream, options); }), 'Call.startVideo')();
    }
    else {
        _logStreamEvent(EventNames.CREATE_STREAM_INVALID_PARAMS, {
            streamType
        });
        return Promise.resolve(undefined);
    }
}
/**
 * @private
 */
export function disposeView(context, internalContext, callId, participantId, stream) {
    const streamType = stream.mediaStreamType;
    if (callId) {
        disposeViewVideo(context, internalContext, callId, stream, participantId);
    }
    else if (!('id' in stream) && !callId) {
        // Stop rendering LocalVideoStream that is not part of a Call
        // Because it is not part of the call we don't tee errors to state naturally (e.g. via a Call Client function such as startVideo).
        // We do not have a stopLocalPreviewVideo function, so as a workaround we ensure any errors are propagated here.
        context.withErrorTeedToState(() => disposeViewUnparentedVideo(context, internalContext, stream), 'Call.stopVideo')();
    }
    else {
        _logStreamEvent(EventNames.DISPOSE_STREAM_INVALID_PARAMS, {
            streamType
        });
        return;
    }
}
/**
 * @private
 * Only stops videos that are tied to a Call.
 */
export function disposeAllViewsFromCall(context, internalContext, callId) {
    const remoteStreams = internalContext.getRemoteRenderInfoForCall(callId);
    if (remoteStreams) {
        for (const [participantKey, participantStreams] of remoteStreams.entries()) {
            for (const [_, remoteStreamAndRenderer] of participantStreams.entries()) {
                // We don't want to accept SDK stream as parameter but we also don't cache the declarative stream so we have to
                // convert the SDK stream to declarative stream which is not pretty so this could use some further refactoring.
                disposeView(context, internalContext, callId, participantKey, convertSdkRemoteStreamToDeclarativeRemoteStream(remoteStreamAndRenderer.stream));
            }
        }
    }
    const localStreams = internalContext.getLocalRenderInfosForCall(callId);
    if (localStreams) {
        for (const localStreamAndRenderer of localStreams.values()) {
            if (localStreamAndRenderer && localStreamAndRenderer.renderer) {
                // We don't want to accept SDK stream as parameter but we also don't cache the declarative stream so we have to
                // convert the SDK stream to declarative stream which is not pretty so this could use some further refactoring.
                disposeView(context, internalContext, callId, undefined, convertSdkLocalStreamToDeclarativeLocalStream(localStreamAndRenderer.stream));
            }
        }
    }
    const callFeatureStreams = internalContext.getCallFeatureRenderInfosForCall(callId);
    if (callFeatureStreams) {
        for (const featureStreams of callFeatureStreams.values()) {
            for (const streamAndRenderer of featureStreams.values()) {
                disposeView(context, internalContext, callId, undefined, convertSdkRemoteStreamToDeclarativeRemoteStream(streamAndRenderer.stream));
            }
        }
    }
}
/**
 * @private
 */
export function disposeAllViews(context, internalContext) {
    const callIds = internalContext.getCallIds();
    for (const callId of callIds) {
        disposeAllViewsFromCall(context, internalContext, callId);
    }
}
//# sourceMappingURL=StreamUtils.js.map