// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { END_CALL_PAGES } from '../adapter/CallAdapter';
import { _isInCall, _isPreviewOn, _isInLobbyOrConnecting } from "../../../../../calling-component-bindings/src";
import { isPhoneNumberIdentifier } from '@azure/communication-common';
const ACCESS_DENIED_TEAMS_MEETING_SUB_CODE = 5854;
const REMOTE_PSTN_USER_HUNG_UP = 560000;
const REMOVED_FROM_CALL_SUB_CODES = [5000, 5300, REMOTE_PSTN_USER_HUNG_UP];
const CALL_REJECTED_CODE = 603;
const INVALID_MEETING_IDENTIFIER = 5751;
/** @private */
export const ROOM_NOT_FOUND_SUB_CODE = 5732;
/** @private */
export const ROOM_NOT_VALID_SUB_CODE = 5829;
/** @private */
export const NOT_INVITED_TO_ROOM_SUB_CODE = 5828;
/** @private */
export const INVITE_TO_ROOM_REMOVED_SUB_CODE = 5317;
/** @private */
export const CALL_TIMEOUT_SUB_CODE = 10004;
/** @private */
export const CALL_TIMEOUT_CODE = 487;
/** @private */
export const BOT_TIMEOUT_CODE = 486;
/** @private */
export const BOT_TIMEOUT_SUB_CODE = 10321;
/**
 * @private
 */
export const isCameraOn = (state) => {
    if (state.call) {
        const stream = state.call.localVideoStreams.find(stream => stream.mediaStreamType === 'Video');
        return !!stream;
    }
    else {
        if (state.devices.selectedCamera) {
            const previewOn = _isPreviewOn(state.devices);
            return previewOn;
        }
    }
    return false;
};
/**
 * Reduce the set of call controls visible on mobile.
 * For example do not show screenshare button.
 *
 * @private
 */
export const reduceCallControlsForMobile = (callControlOptions) => {
    if (callControlOptions === false) {
        return false;
    }
    // Ensure call controls a valid object.
    const reduceCallControlOptions = callControlOptions === true ? {} : callControlOptions || {};
    // Set to compressed mode when composite is optimized for mobile
    reduceCallControlOptions.displayType = 'compact';
    // Do not show screen share button when composite is optimized for mobile unless the developer
    // has explicitly opted in.
    if (reduceCallControlOptions.screenShareButton !== true) {
        reduceCallControlOptions.screenShareButton = false;
    }
    return reduceCallControlOptions;
};
var CallEndReasons;
(function (CallEndReasons) {
    CallEndReasons[CallEndReasons["LEFT_CALL"] = 0] = "LEFT_CALL";
    CallEndReasons[CallEndReasons["ACCESS_DENIED"] = 1] = "ACCESS_DENIED";
    CallEndReasons[CallEndReasons["REMOVED_FROM_CALL"] = 2] = "REMOVED_FROM_CALL";
    CallEndReasons[CallEndReasons["BAD_REQUEST"] = 3] = "BAD_REQUEST";
})(CallEndReasons || (CallEndReasons = {}));
const getCallEndReason = (call) => {
    var _a, _b, _c, _d;
    const remoteParticipantsEndedArray = Array.from(Object.values(call.remoteParticipantsEnded));
    /**
     * Handle the special case in a PSTN call where removing the last user kicks the caller out of the call.
     * The code and subcode is the same as when a user is removed from a teams interop call.
     * Hence, we look at the last remote participant removed to determine if the last participant removed was a phone number.
     * If yes, the caller was kicked out of the call, but we need to show them that they left the call.
     * Note: This check will only work for 1:1 PSTN Calls. The subcode is different for 1:N PSTN calls, and we do not need to handle that case.
     */
    if (remoteParticipantsEndedArray[0] && isPhoneNumberIdentifier(remoteParticipantsEndedArray[0].identifier) && ((_a = call.callEndReason) === null || _a === void 0 ? void 0 : _a.subCode) !== REMOTE_PSTN_USER_HUNG_UP) {
        return CallEndReasons.LEFT_CALL;
    }
    if (((_b = call.callEndReason) === null || _b === void 0 ? void 0 : _b.subCode) && call.callEndReason.subCode === ACCESS_DENIED_TEAMS_MEETING_SUB_CODE) {
        return CallEndReasons.ACCESS_DENIED;
    }
    if (((_c = call.callEndReason) === null || _c === void 0 ? void 0 : _c.subCode) && REMOVED_FROM_CALL_SUB_CODES.includes(call.callEndReason.subCode)) {
        return CallEndReasons.REMOVED_FROM_CALL;
    }
    // If the call end reason code is 400, the call is ended due to a bad request. Keep this line at the bottom right before returning normal left call to catch the scenarios not including the ones above.
    if (((_d = call.callEndReason) === null || _d === void 0 ? void 0 : _d.code) === 400) {
        return CallEndReasons.BAD_REQUEST;
    }
    if (call.callEndReason) {
        // No error codes match, assume the user simply left the call regularly
        return CallEndReasons.LEFT_CALL;
    }
    throw new Error('No matching call end reason');
};
/**
 * Helper function for determine strings and icons for end call page
 * @private
 */
export const getEndedCallPageProps = (locale, endedCall) => {
    var _a, _b, _c, _d, _e, _f, _g;
    let title = locale.strings.call.leftCallTitle;
    let moreDetails = locale.strings.call.leftCallMoreDetails;
    let disableStartCallButton = false;
    let iconName = 'NoticePageLeftCall';
    switch ((_a = endedCall === null || endedCall === void 0 ? void 0 : endedCall.callEndReason) === null || _a === void 0 ? void 0 : _a.subCode) {
        case ROOM_NOT_FOUND_SUB_CODE:
            if (locale.strings.call.roomNotFoundTitle) {
                title = locale.strings.call.roomNotFoundTitle;
                moreDetails = locale.strings.call.roomNotFoundDetails;
                disableStartCallButton = true;
                iconName = 'NoticePageRoomNotFound';
            }
            break;
        case ROOM_NOT_VALID_SUB_CODE:
            if (locale.strings.call.roomNotValidTitle) {
                title = locale.strings.call.roomNotValidTitle;
                moreDetails = locale.strings.call.roomNotValidDetails;
                disableStartCallButton = true;
                iconName = 'NoticePageRoomNotValid';
            }
            break;
        case NOT_INVITED_TO_ROOM_SUB_CODE:
            if (locale.strings.call.notInvitedToRoomTitle) {
                title = locale.strings.call.notInvitedToRoomTitle;
                moreDetails = locale.strings.call.notInvitedToRoomDetails;
                disableStartCallButton = true;
                iconName = 'NoticePageNotInvitedToRoom';
            }
            break;
        case INVITE_TO_ROOM_REMOVED_SUB_CODE:
            if (locale.strings.call.inviteToRoomRemovedTitle) {
                title = locale.strings.call.inviteToRoomRemovedTitle;
                moreDetails = locale.strings.call.inviteToRoomRemovedDetails;
                disableStartCallButton = true;
                iconName = 'NoticePageInviteToRoomRemoved';
            }
            break;
        case CALL_TIMEOUT_SUB_CODE:
            if (((_b = endedCall === null || endedCall === void 0 ? void 0 : endedCall.callEndReason) === null || _b === void 0 ? void 0 : _b.code) === CALL_TIMEOUT_CODE && locale.strings.call.callTimeoutTitle) {
                title = locale.strings.call.callTimeoutTitle;
                moreDetails = locale.strings.call.callTimeoutDetails;
                disableStartCallButton = true;
                iconName = 'NoticePageCallTimeout';
            }
            break;
        case BOT_TIMEOUT_SUB_CODE:
            if (((_c = endedCall === null || endedCall === void 0 ? void 0 : endedCall.callEndReason) === null || _c === void 0 ? void 0 : _c.code) === BOT_TIMEOUT_CODE && locale.strings.call.callTimeoutBotTitle) {
                title = locale.strings.call.callTimeoutBotTitle;
                moreDetails = locale.strings.call.callTimeoutBotDetails;
                disableStartCallButton = true;
                iconName = 'NoticePageCallTimeout';
            }
            break;
    }
    switch ((_d = endedCall === null || endedCall === void 0 ? void 0 : endedCall.callEndReason) === null || _d === void 0 ? void 0 : _d.code) {
        case CALL_REJECTED_CODE:
            if (locale.strings.call.callRejectedTitle) {
                title = locale.strings.call.callRejectedTitle;
                moreDetails = locale.strings.call.callRejectedMoreDetails;
                disableStartCallButton = true;
                iconName = 'NoticePageCallRejected';
            }
            break;
    }
    switch ((_e = endedCall === null || endedCall === void 0 ? void 0 : endedCall.callEndReason) === null || _e === void 0 ? void 0 : _e.subCode) {
        case 10037:
            if (locale.strings.call.participantCouldNotBeReachedTitle) {
                title = locale.strings.call.participantCouldNotBeReachedTitle;
                moreDetails = locale.strings.call.participantCouldNotBeReachedMoreDetails;
                disableStartCallButton = true;
            }
            break;
        case 10124:
            if (locale.strings.call.permissionToReachTargetParticipantNotAllowedTitle) {
                title = locale.strings.call.permissionToReachTargetParticipantNotAllowedTitle;
                moreDetails = locale.strings.call.permissionToReachTargetParticipantNotAllowedMoreDetails;
                disableStartCallButton = true;
            }
            break;
        case 10119:
            if (locale.strings.call.unableToResolveTenantTitle) {
                title = locale.strings.call.unableToResolveTenantTitle;
                moreDetails = locale.strings.call.unableToResolveTenantMoreDetails;
                disableStartCallButton = true;
            }
            break;
        case 10044:
            if (locale.strings.call.participantIdIsMalformedTitle) {
                title = locale.strings.call.participantIdIsMalformedTitle;
                moreDetails = locale.strings.call.participantIdIsMalformedMoreDetails;
                disableStartCallButton = true;
            }
            break;
    }
    switch ((_f = endedCall === null || endedCall === void 0 ? void 0 : endedCall.callEndReason) === null || _f === void 0 ? void 0 : _f.subCode) {
        case INVALID_MEETING_IDENTIFIER:
            if (locale.strings.call.callRejectedTitle) {
                title = locale.strings.call.callRejectedTitle;
                moreDetails = locale.strings.call.invalidMeetingIdentifier;
                disableStartCallButton = true;
            }
            break;
    }
    // keep this at the bottom to catch the scenarios not including the ones above.
    switch ((_g = endedCall === null || endedCall === void 0 ? void 0 : endedCall.callEndReason) === null || _g === void 0 ? void 0 : _g.code) {
        case 400:
            if (locale.strings.call.callRejectedTitle) {
                title = locale.strings.call.callRejectedTitle;
                disableStartCallButton = true;
            }
            break;
    }
    return {
        title,
        moreDetails,
        disableStartCallButton,
        iconName
    };
};
/**
 * Get the current call composite page based on the current call composite state
 *
 * @param Call - The current call state
 * @param previousCall - The state of the most recent previous call that has ended.
 *
 * @remarks - The previousCall state is needed to determine if the call has ended.
 * When the call ends a new call object is created, and so we must lookback at the
 * previous call state to understand how the call has ended. If there is no previous
 * call we know that this is a fresh call and can display the configuration page.
 *
 * @private
 */
export const getCallCompositePage = (call, previousCall, transferCall, isReturningFromBreakoutRoom, unsupportedBrowserInfo) => {
    if (transferCall !== undefined) {
        return 'transferring';
    }
    if (isReturningFromBreakoutRoom) {
        return 'returningFromBreakoutRoom';
    }
    if (call) {
        // Must check for ongoing call *before* looking at any previous calls.
        // If the composite completes one call and joins another, the previous calls
        // will be populated, but not relevant for determining the page.
        // `_isInLobbyOrConnecting` needs to be checked first because `_isInCall` also returns true when call is in lobby.
        if (_isInLobbyOrConnecting(call === null || call === void 0 ? void 0 : call.state)) {
            return 'lobby';
            // `LocalHold` needs to be checked before `isInCall` since it is also a state that's considered in call.
        }
        else if ((call === null || call === void 0 ? void 0 : call.state) === 'LocalHold') {
            return 'hold';
        }
        else if ((call === null || call === void 0 ? void 0 : call.state) === 'Disconnecting') {
            return 'leaving';
        }
        else if (_isInCall(call === null || call === void 0 ? void 0 : call.state)) {
            return 'call';
        }
        else {
            // When the call object has been constructed after clicking , but before 'connecting' has been
            // set on the call object, we continue to show the configuration screen.
            // The call object does not correctly reflect local device state until `call.state` moves to `connecting`.
            // Moving to the 'lobby' page too soon leads to components that depend on the `call` object to show incorrect
            // transitional state.
            return 'configuration';
        }
    }
    if (previousCall) {
        const reason = getCallEndReason(previousCall);
        switch (reason) {
            case CallEndReasons.ACCESS_DENIED:
                return 'accessDeniedTeamsMeeting';
            case CallEndReasons.REMOVED_FROM_CALL:
                return 'removedFromCall';
            case CallEndReasons.BAD_REQUEST:
                return 'badRequest';
            case CallEndReasons.LEFT_CALL:
                if (previousCall.diagnostics.network.latest.noNetwork) {
                    return 'joinCallFailedDueToNoNetwork';
                }
                return 'leftCall';
        }
    }
    // No call state - show starting page (configuration)
    return 'configuration';
};
/** @private */
export const IsCallEndedPage = (page) => END_CALL_PAGES.includes(page);
/**
 * Creates a new call control options object and sets the correct values for disabling
 * the buttons provided in the `disabledControls` array.
 * Returns a new object without changing the original object.
 * @param callControlOptions options for the call control component that need to be modified.
 * @param disabledControls An array of controls to disable.
 * @returns a copy of callControlOptions with disabledControls disabled
 * @private
 */
export const disableCallControls = (callControlOptions, disabledControls) => {
    var _a;
    if (callControlOptions === false) {
        return false;
    }
    // Ensure we clone the prop if it is an object to ensure we do not mutate the original prop.
    let newOptions = (_a = (callControlOptions instanceof Object ? Object.assign({}, callControlOptions) : callControlOptions)) !== null && _a !== void 0 ? _a : {};
    if (newOptions === true || newOptions === undefined) {
        newOptions = disabledControls.reduce((acc, key) => {
            // @ts-expect-error TODO: fix noImplicitAny error here
            // Not solveable at this time due to typescript limitations. The typing is too complex for typescript to
            // understand. Will need to revisit when either typescript or the calling component bindings are updated.
            acc[key] = {
                disabled: true
            };
            return acc;
        }, {});
    }
    else {
        disabledControls.forEach(key => {
            // @ts-expect-error refer to above comment
            if (newOptions[key] !== false) {
                // @ts-expect-error refer to above comment
                newOptions[key] = {
                    disabled: true
                };
            }
        });
    }
    return newOptions;
};
/**
 * Check if a disabled object is provided for a button and returns if the button is disabled.
 * A button is only disabled if is explicitly set to disabled.
 *
 * @param option
 * @returns whether a button is disabled
 * @private
 */
export const isDisabled = (option) => {
    if (option === undefined || typeof option === 'boolean') {
        return false;
    }
    return option.disabled;
};
/**
 * Check if we are using safari browser
 * @private
 */
export const _isSafari = (environmentInfo) => {
    return (environmentInfo === null || environmentInfo === void 0 ? void 0 : environmentInfo.environment.browser.toLowerCase()) === 'safari';
};
/**
 * @private
 * This is the util function to create a participant modifier for remote participantList
 * It memoize previous original participant items and only update the changed participant
 * It takes in one modifier function to generate one single participant object, it returns undefined if the object keeps unmodified
 */
export const createParticipantModifier = (createModifiedParticipant) => {
    let previousParticipantState = undefined;
    let modifiedParticipants = {};
    const memoizedParticipants = {};
    return (state) => {
        var _a, _b, _c, _d;
        // if root state is the same, we don't need to update the participants
        if (((_a = state.call) === null || _a === void 0 ? void 0 : _a.remoteParticipants) !== previousParticipantState) {
            modifiedParticipants = {};
            const originalParticipants = Object.entries(((_b = state.call) === null || _b === void 0 ? void 0 : _b.remoteParticipants) || {});
            for (const [key, originalParticipant] of originalParticipants) {
                const modifiedParticipant = createModifiedParticipant(key, originalParticipant);
                if (modifiedParticipant === undefined) {
                    modifiedParticipants[key] = originalParticipant;
                    continue;
                }
                // Generate the new item if original cached item has been changed
                if (((_c = memoizedParticipants[key]) === null || _c === void 0 ? void 0 : _c.originalRef) !== originalParticipant) {
                    memoizedParticipants[key] = {
                        newParticipant: modifiedParticipant,
                        originalRef: originalParticipant
                    };
                }
                // the modified participant is always coming from the memoized cache, whether is was refreshed
                // from the previous closure or not
                const memoizedParticipant = memoizedParticipants[key];
                if (!memoizedParticipant) {
                    throw new Error('Participant modifier encountered an unhandled exception.');
                }
                modifiedParticipants[key] = memoizedParticipant.newParticipant;
            }
            previousParticipantState = (_d = state.call) === null || _d === void 0 ? void 0 : _d.remoteParticipants;
        }
        return Object.assign(Object.assign({}, state), { call: state.call ? Object.assign(Object.assign({}, state.call), { remoteParticipants: modifiedParticipants }) : undefined });
    };
};
/** @private */
export const getBackgroundEffectFromSelectedEffect = (selectedEffect, VideoBackgroundEffectsDependency) => (selectedEffect === null || selectedEffect === void 0 ? void 0 : selectedEffect.effectName) === 'blur' ? VideoBackgroundEffectsDependency.createBackgroundBlurEffect() : (selectedEffect === null || selectedEffect === void 0 ? void 0 : selectedEffect.effectName) === 'replacement' ? VideoBackgroundEffectsDependency.createBackgroundReplacementEffect({
    backgroundImageUrl: selectedEffect.backgroundImageUrl
}) : undefined;
/**
 * @remarks this logic should mimic the onToggleCamera in the common call handlers.
 * @private
 */
export const getSelectedCameraFromAdapterState = (state) => state.devices.selectedCamera || state.devices.cameras[0];
/**
 * Helper to determine if the adapter has a locator or targetCallees
 * @param locatorOrTargetCallees
 * @returns boolean to determine if the adapter has a locator or targetCallees, true is locator, false is targetCallees
 * @private
 */
export const getLocatorOrTargetCallees = (locatorOrTargetCallees) => {
    return !!Array.isArray(locatorOrTargetCallees);
};
/**
 * Return different conditions based on the current and previous state of recording and transcribing
 *
 * @param callRecordState - The current call record state: on, off, stopped
 * @param callTranscribeState - The current call transcribe state: on, off, stopped
 *
 * @remarks - The stopped state means: previously on but currently off
 *
 * @private
 */
export const computeVariant = (callRecordState, callTranscribeState) => {
    if (callRecordState === 'on' && callTranscribeState === 'on') {
        return 'recordingAndTranscriptionStarted';
    }
    else if (callRecordState === 'on' && callTranscribeState === 'off') {
        return 'recordingStarted';
    }
    else if (callRecordState === 'off' && callTranscribeState === 'on') {
        return 'transcriptionStarted';
    }
    else if (callRecordState === 'on' && callTranscribeState === 'stopped') {
        return 'transcriptionStoppedStillRecording';
    }
    else if (callRecordState === 'stopped' && callTranscribeState === 'on') {
        return 'recordingStoppedStillTranscribing';
    }
    else if (callRecordState === 'off' && callTranscribeState === 'stopped') {
        return 'transcriptionStopped';
    }
    else if (callRecordState === 'stopped' && callTranscribeState === 'off') {
        return 'recordingStopped';
    }
    else if (callRecordState === 'stopped' && callTranscribeState === 'stopped') {
        return 'recordingAndTranscriptionStopped';
    }
    else {
        return 'noState';
    }
};
/**
 * @private
 */
export function determineStates(previous, current) {
    // if current state is on, then return on
    if (current) {
        return 'on';
    }
    // if current state is off
    else {
        // if previous state is on and current state is off, return stopped (on -> off)
        if (previous === 'on') {
            return 'stopped';
        }
        // otherwise remain previous state unchanged
        else {
            return previous;
        }
    }
}
// The debounce time for the stopped state to be shown after both states are stopped.
// This is to prevent stopped messages from being lost by transitioning to "Off" too
// quickly if the states are toggled in quick succession.
// This also prevents React strict mode from transitioning to "Off" too quickly.
const ComplianceNotificationOffDebounceTimeMs = 2000;
/**
 * Compute compliance notification based on latest compliance state and cached props.
 * @private
 */
export function computeComplianceNotification(complianceProps, cachedProps) {
    // Only update cached props and variant if there is _some_ change in the latest props.
    // This ensures that state machine is only updated if there is an actual change in the props.
    const shouldUpdateCached = complianceProps.callRecordState !== cachedProps.current.latestBooleanState.callRecordState || complianceProps.callTranscribeState !== cachedProps.current.latestBooleanState.callTranscribeState;
    // The following three operations must be performed in this exact order:
    // [1]: Update cached state to transition the state machine.
    if (shouldUpdateCached) {
        cachedProps.current = {
            latestBooleanState: complianceProps,
            latestStringState: {
                callRecordState: determineStates(cachedProps.current.latestStringState.callRecordState, complianceProps.callRecordState),
                callTranscribeState: determineStates(cachedProps.current.latestStringState.callTranscribeState, complianceProps.callTranscribeState)
            },
            lastUpdated: Date.now()
        };
    }
    // [2]: If the callRecordState and callTranscribeState are both stopped for a predetermined amount of time, mark both states as off.
    // NOTE: this can be removed once lastStoppedRecording in the calling stateful client is GA.
    if (shouldUpdateCached && cachedProps.current.latestStringState.callRecordState === 'stopped' && cachedProps.current.latestStringState.callTranscribeState === 'stopped' && Date.now() - cachedProps.current.lastUpdated > ComplianceNotificationOffDebounceTimeMs) {
        // When both states are stopped, after displaying message "RECORDING_AND_TRANSCRIPTION_STOPPED", change both states to off (going back to the default state).
        cachedProps.current.latestStringState.callRecordState = 'off';
        cachedProps.current.latestStringState.callTranscribeState = 'off';
    }
    // [3]: Compute the variant, using the transitioned state machine.
    const variant = computeVariant(cachedProps.current.latestStringState.callRecordState, cachedProps.current.latestStringState.callTranscribeState);
    // If the variant is not 'noState', then show the notification.
    if (variant !== 'noState') {
        return {
            type: variant,
            timestamp: new Date(Date.now())
        };
    }
    else {
        return undefined;
    }
}
//# sourceMappingURL=Utils.js.map