/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import { type Room } from "matrix-js-sdk/src/matrix";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { logger as rootLogger } from "matrix-js-sdk/src/logger";

import type React from "react";
import { useFeatureEnabled, useSettingValue } from "../useSettings";
import SdkConfig from "../../SdkConfig";
import { useEventEmitter, useEventEmitterState } from "../useEventEmitter";
import { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import { useWidgets } from "../../utils/WidgetUtils";
import { WidgetType } from "../../widgets/WidgetType";
import { useCall, useConnectionState, useParticipantCount } from "../useCall";
import { useRoomMemberCount } from "../useRoomMembers";
import { ConnectionState } from "../../models/Call";
import { placeCall } from "../../utils/room/placeCall";
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import { useRoomState } from "../useRoomState";
import { _t } from "../../languageHandler";
import { isManagedHybridWidget, isManagedHybridWidgetEnabled } from "../../widgets/ManagedHybrid";
import { type IApp } from "../../stores/WidgetStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../dispatcher/actions";
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
import { isVideoRoom } from "../../utils/video-rooms";
import { UIFeature } from "../../settings/UIFeature";
import { type InteractionName } from "../../PosthogTrackers";
import { ElementCallMemberEventType } from "../../call-types";
import { LocalRoom, LocalRoomState } from "../../models/LocalRoom";
import { useScopedRoomContext } from "../../contexts/ScopedRoomContext";
import { SdkContextClass } from "../../contexts/SDKContext";

const logger = rootLogger.getChild("useRoomCall");

export enum PlatformCallType {
    ElementCall,
    JitsiCall,
    LegacyCall,
}

export const getPlatformCallTypeProps = (
    platformCallType: PlatformCallType,
): {
    label: string;
    children?: ReactNode;
    analyticsName: InteractionName;
} => {
    switch (platformCallType) {
        case PlatformCallType.ElementCall:
            return {
                label: _t("voip|element_call"),
                analyticsName: "WebVoipOptionElementCall",
            };
        case PlatformCallType.JitsiCall:
            return {
                label: _t("voip|jitsi_call"),
                analyticsName: "WebVoipOptionJitsi",
            };
        case PlatformCallType.LegacyCall:
            return {
                label: _t("voip|legacy_call"),
                analyticsName: "WebVoipOptionLegacy",
            };
        default:
            throw Error(`Unexpected PlatformCallType ${platformCallType}`);
    }
};

const enum State {
    NoCall,
    NoPermission,
    Unpinned,
    Ongoing,
    NotJoined,
}

/**
 * Utility hook for resolving state and click handlers for Voice & Video call buttons in the room header
 * @param room the room to track
 * @returns the call button attributes for the given room
 */
export const useRoomCall = (
    room: Room | LocalRoom,
): {
    voiceCallDisabledReason: string | null;
    voiceCallClick(this: void, evt: React.MouseEvent | undefined, selectedType: PlatformCallType): void;
    videoCallDisabledReason: string | null;
    videoCallClick(this: void, evt: React.MouseEvent | undefined, selectedType: PlatformCallType): void;
    toggleCallMaximized: () => void;
    isViewingCall: boolean;
    isConnectedToCall: boolean;
    /**
     * The type of call in progress, or `null` if no call is ongoing.
     */
    activeCallSessionType: CallType | null;
    callOptions: PlatformCallType[];
    showVideoCallButton: boolean;
    showVoiceCallButton: boolean;
} => {
    const roomViewStore = useScopedRoomContext("roomViewStore").roomViewStore;
    // settings
    const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
    const widgetsFeatureEnabled = useSettingValue(UIFeature.Widgets);
    const voipFeatureEnabled = useSettingValue(UIFeature.Voip);
    const useElementCallExclusively = useMemo(() => {
        return SdkConfig.get("element_call").use_exclusively;
    }, []);

    const serverIsConfiguredForElementCall = useEventEmitterState(
        CallStore.instance,
        CallStoreEvent.TransportsUpdated,
        () =>
            CallStore.instance.getConfiguredRTCTransports().some((s) => s.type === "livekit" && s.livekit_service_url),
    );

    useEffect(() => {
        if (useElementCallExclusively && !serverIsConfiguredForElementCall) {
            logger.warn(
                "Element Call is configured to be used exclusively, but the server is not configured with a transport",
            );
        }
    }, [useElementCallExclusively, serverIsConfiguredForElementCall]);

    const hasLegacyCall = useEventEmitterState(
        SdkContextClass.instance.legacyCallHandler,
        LegacyCallHandlerEvent.CallsChanged,
        () => SdkContextClass.instance.legacyCallHandler.getCallForRoom(room.roomId) !== null,
    );
    // settings
    const widgets = useWidgets(room);
    const jitsiWidget = useMemo(() => widgets.find((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]);
    const hasJitsiWidget = !!jitsiWidget;
    const managedHybridWidget = useMemo(() => widgets.find(isManagedHybridWidget), [widgets]);
    const hasManagedHybridWidget = !!managedHybridWidget;

    // group call
    const groupCall = useCall(room.roomId);
    const isConnectedToCall = useConnectionState(groupCall) === ConnectionState.Connected;
    const hasGroupCall = groupCall !== null;
    const isViewingCall = useEventEmitterState(
        roomViewStore,
        UPDATE_EVENT,
        () => roomViewStore.isViewingCall() || isVideoRoom(room),
    );

    const participantCount = useParticipantCount(groupCall);
    const activeCallSessionType = useMemo(() => {
        if (!groupCall || participantCount === 0) {
            return null;
        }
        return groupCall.callType;
    }, [participantCount, groupCall]);

    // room
    const memberCount = useRoomMemberCount(room);

    const [mayEditWidgets, mayCreateElementCallState] = useRoomState(room, () => [
        room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
        room.currentState.mayClientSendStateEvent(ElementCallMemberEventType.name, room.client),
    ]);

    const mayCreateElementCalls = mayCreateElementCallState && serverIsConfiguredForElementCall;

    // The options provided to the RoomHeader.
    // If there are multiple options, the user will be prompted to choose.
    const callOptions = useMemo((): PlatformCallType[] => {
        const options: PlatformCallType[] = [];
        if (groupCallsEnabled) {
            if (hasGroupCall || mayCreateElementCalls) {
                options.push(PlatformCallType.ElementCall);
            }
            if (useElementCallExclusively && !hasJitsiWidget) {
                return [PlatformCallType.ElementCall];
            }
        }
        if (memberCount <= 2) {
            options.push(PlatformCallType.LegacyCall);
        } else if (mayEditWidgets || hasJitsiWidget) {
            options.push(PlatformCallType.JitsiCall);
        }
        if (hasGroupCall && WidgetType.CALL.matches(groupCall.widget.type)) {
            // only allow joining the ongoing Element call if there is one.
            return [PlatformCallType.ElementCall];
        }
        return options;
    }, [
        memberCount,
        mayEditWidgets,
        hasJitsiWidget,
        groupCallsEnabled,
        hasGroupCall,
        mayCreateElementCalls,
        useElementCallExclusively,
        groupCall?.widget.type,
    ]);

    let widget: IApp | undefined;
    if (callOptions.includes(PlatformCallType.JitsiCall) || callOptions.includes(PlatformCallType.LegacyCall)) {
        widget = jitsiWidget ?? managedHybridWidget;
    }
    if (callOptions.includes(PlatformCallType.ElementCall)) {
        widget = groupCall?.widget;
    } else {
        widget = groupCall?.widget ?? jitsiWidget;
    }
    const updateWidgetState = useCallback((): void => {
        setCanPinWidget(WidgetLayoutStore.instance.canAddToContainer(room, "top"));
        setWidgetPinned(!!widget && WidgetLayoutStore.instance.isInContainer(room, widget, "top"));
    }, [room, widget]);
    useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateWidgetState);
    useEffect(() => {
        updateWidgetState();
    }, [room, jitsiWidget, groupCall, updateWidgetState]);
    const [canPinWidget, setCanPinWidget] = useState(false);
    const [widgetPinned, setWidgetPinned] = useState(false);
    // We only want to prompt to pin the widget if it's not element call based.
    const isECWidget = WidgetType.CALL.matches(widget?.type ?? "");
    const promptPinWidget = !isECWidget && canPinWidget && !widgetPinned;
    const connectedCalls = useEventEmitterState(CallStore.instance, CallStoreEvent.ConnectedCalls, () =>
        Array.from(CallStore.instance.connectedCalls),
    );

    const state = useMemo((): State => {
        if (connectedCalls.find((call) => call.roomId != room.roomId)) {
            return State.Ongoing;
        }
        if (hasGroupCall && (hasJitsiWidget || hasManagedHybridWidget)) {
            return promptPinWidget ? State.Unpinned : State.Ongoing;
        }
        if (hasLegacyCall) {
            return State.Ongoing;
        }

        if (!callOptions.includes(PlatformCallType.LegacyCall) && !mayCreateElementCalls && !mayEditWidgets) {
            return State.NoPermission;
        }
        // Catch-all for just not having any call options available.
        if (!callOptions.length) {
            return State.NoPermission;
        }
        return State.NoCall;
    }, [
        callOptions,
        connectedCalls,
        hasGroupCall,
        hasJitsiWidget,
        hasLegacyCall,
        hasManagedHybridWidget,
        mayCreateElementCalls,
        mayEditWidgets,
        promptPinWidget,
        room.roomId,
    ]);

    const voiceCallClick = useCallback(
        (evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => {
            evt?.stopPropagation();
            if (widget && promptPinWidget) {
                WidgetLayoutStore.instance.moveToContainer(room, widget, "top");
            } else {
                placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined, true);
            }
        },
        [promptPinWidget, room, widget],
    );
    const videoCallClick = useCallback(
        (evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => {
            evt?.stopPropagation();
            if (widget && promptPinWidget) {
                WidgetLayoutStore.instance.moveToContainer(room, widget, "top");
            } else {
                // If we have pressed shift then always skip the lobby, otherwise `undefined` will defer
                // to the defaults of the call implementation.
                placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined, false);
            }
        },
        [widget, promptPinWidget, room],
    );

    let voiceCallDisabledReason: string | null;
    let videoCallDisabledReason: string | null;
    switch (state) {
        case State.NoPermission:
            voiceCallDisabledReason = _t("voip|disabled_no_perms_start_voice_call");
            videoCallDisabledReason = _t("voip|disabled_no_perms_start_video_call");
            break;
        case State.Ongoing:
            voiceCallDisabledReason = _t("voip|disabled_ongoing_call");
            videoCallDisabledReason = _t("voip|disabled_ongoing_call");
            break;
        case State.Unpinned:
        case State.NotJoined:
        case State.NoCall:
            voiceCallDisabledReason = null;
            videoCallDisabledReason = null;
    }
    const toggleCallMaximized = useCallback(() => {
        defaultDispatcher.dispatch<ViewRoomPayload>({
            action: Action.ViewRoom,
            room_id: room.roomId,
            metricsTrigger: undefined,
            view_call: !isViewingCall,
        });
    }, [isViewingCall, room.roomId]);

    const roomDoesNotExist = room instanceof LocalRoom && room.state !== LocalRoomState.CREATED;

    // We hide the voice call button if it'd have the same effect as the video call button
    let hideVoiceCallButton =
        isManagedHybridWidgetEnabled(room) ||
        // Disable voice calls if Legacy calls are disabled
        (!callOptions.includes(PlatformCallType.LegacyCall) &&
            // Disable voice calls in ECall if the room is a group (we only present video calls for groups of users)
            (!callOptions.includes(PlatformCallType.ElementCall) || memberCount > 2));

    let hideVideoCallButton = false;
    // We hide both buttons if:
    // - they require widgets but widgets are disabled
    // - if the Voip feature is disabled.
    // - The room is not created yet (rendering "send first message view")
    if ((memberCount > 2 && !widgetsFeatureEnabled) || !voipFeatureEnabled || roomDoesNotExist) {
        hideVoiceCallButton = true;
        hideVideoCallButton = true;
    }

    /**
     * We've gone through all the steps
     */
    return {
        voiceCallDisabledReason,
        voiceCallClick,
        videoCallDisabledReason,
        videoCallClick,
        toggleCallMaximized: toggleCallMaximized,
        isViewingCall: isViewingCall,
        isConnectedToCall: isConnectedToCall,
        activeCallSessionType: activeCallSessionType,
        callOptions,
        showVoiceCallButton: !hideVoiceCallButton,
        showVideoCallButton: !hideVideoCallButton,
    };
};
