/* @flow */

import { getLogger } from 'jitsi-meet-logger';

import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
import {
    openRemoteControlAuthorizationDialog
} from '../../react/features/remote-control';
import {
    DISCO_REMOTE_CONTROL_FEATURE,
    EVENT_TYPES,
    PERMISSIONS_ACTIONS,
    REMOTE_CONTROL_EVENT_NAME
} from '../../service/remotecontrol/Constants';
import { getJitsiMeetTransport } from '../transport';

import RemoteControlParticipant from './RemoteControlParticipant';

declare var APP: Object;
declare var config: Object;
declare var interfaceConfig: Object;
declare var JitsiMeetJS: Object;

const ConferenceEvents = JitsiMeetJS.events.conference;
const logger = getLogger(__filename);

/**
 * The transport instance used for communication with external apps.
 *
 * @type {Transport}
 */
const transport = getJitsiMeetTransport();

/**
 * This class represents the receiver party for a remote controller session.
 * It handles "remote-control-event" events and sends them to the
 * API module. From there the events can be received from wrapper application
 * and executed.
 */
export default class Receiver extends RemoteControlParticipant {
    _controller: ?string;
    _enabled: boolean;
    _hangupListener: Function;
    _remoteControlEventsListener: Function;
    _userLeftListener: Function;

    /**
     * Creates new instance.
     */
    constructor() {
        super();
        this._controller = null;
        this._remoteControlEventsListener
            = this._onRemoteControlEvent.bind(this);
        this._userLeftListener = this._onUserLeft.bind(this);
        this._hangupListener = this._onHangup.bind(this);

        // We expect here that even if we receive the supported event earlier
        // it will be cached and we'll receive it.
        transport.on('event', event => {
            if (event.name === REMOTE_CONTROL_EVENT_NAME) {
                this._onRemoteControlAPIEvent(event);

                return true;
            }

            return false;
        });
    }

    /**
     * Enables / Disables the remote control.
     *
     * @param {boolean} enabled - The new state.
     * @returns {void}
     */
    _enable(enabled: boolean) {
        if (this._enabled === enabled) {
            return;
        }
        this._enabled = enabled;
        if (enabled === true) {
            logger.log('Remote control receiver enabled.');

            // Announce remote control support.
            APP.connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true);
            APP.conference.addConferenceListener(
                ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
                this._remoteControlEventsListener);
            APP.conference.addListener(JitsiMeetConferenceEvents.BEFORE_HANGUP,
                this._hangupListener);
        } else {
            logger.log('Remote control receiver disabled.');
            this._stop(true);
            APP.connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE);
            APP.conference.removeConferenceListener(
                ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
                this._remoteControlEventsListener);
            APP.conference.removeListener(
                JitsiMeetConferenceEvents.BEFORE_HANGUP,
                this._hangupListener);
        }
    }

    /**
     * Removes the listener for ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED
     * events. Sends stop message to the wrapper application. Optionally
     * displays dialog for informing the user that remote control session
     * ended.
     *
     * @param {boolean} [dontShowDialog] - If true the dialog won't be
     * displayed.
     * @returns {void}
     */
    _stop(dontShowDialog: boolean = false) {
        if (!this._controller) {
            return;
        }
        logger.log('Remote control receiver stop.');
        this._controller = null;
        APP.conference.removeConferenceListener(ConferenceEvents.USER_LEFT,
            this._userLeftListener);
        if (this.remoteControlExternalAuth) {
            transport.sendEvent({
                name: REMOTE_CONTROL_EVENT_NAME,
                type: EVENT_TYPES.stop
            });
        }
        if (!dontShowDialog) {
            APP.UI.messageHandler.openMessageDialog(
                'dialog.remoteControlTitle',
                'dialog.remoteControlStopMessage'
            );
        }
    }

    /**
     * Calls this._stop() and sends stop message to the controller participant.
     *
     * @returns {void}
     */
    stop() {
        if (!this._controller) {
            return;
        }
        this.sendRemoteControlEvent(this._controller, {
            type: EVENT_TYPES.stop
        });
        this._stop();
    }

    /**
     * Listens for data channel EndpointMessage events. Handles only events of
     * type remote control. Sends "remote-control-event" events to the API
     * module.
     *
     * @param {JitsiParticipant} participant - The controller participant.
     * @param {Object} event - EndpointMessage event from the data channels.
     * @param {string} event.name - The function process only events with
     * name REMOTE_CONTROL_EVENT_NAME.
     * @returns {void}
     */
    _onRemoteControlEvent(participant: Object, event: Object) {
        if (event.name !== REMOTE_CONTROL_EVENT_NAME) {
            return;
        }

        const remoteControlEvent = Object.assign({}, event);

        if (this._enabled) {
            if (this._controller === null
                    && event.type === EVENT_TYPES.permissions
                    && event.action === PERMISSIONS_ACTIONS.request) {
                const userId = participant.getId();

                if (!config.remoteControlExternalAuth) {
                    APP.store.dispatch(
                        openRemoteControlAuthorizationDialog(userId));

                    return;
                }

                // FIXME: Maybe use transport.sendRequest in this case???
                remoteControlEvent.userId = userId;
                remoteControlEvent.userJID = participant.getJid();
                remoteControlEvent.displayName = participant.getDisplayName()
                    || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
                remoteControlEvent.screenSharing
                    = APP.conference.isSharingScreen;
            } else if (this._controller !== participant.getId()) {
                return;
            } else if (event.type === EVENT_TYPES.stop) {
                this._stop();

                return;
            }
            transport.sendEvent(remoteControlEvent);
        } else {
            logger.log('Remote control event is ignored because remote '
                + 'control is disabled', event);
        }
    }

    /**
     * Handles remote control permission events.
     *
     * @param {string} userId - The user id of the participant related to the
     * event.
     * @param {PERMISSIONS_ACTIONS} action - The action related to the event.
     * @returns {void}
     */
    _onRemoteControlPermissionsEvent(userId: string, action: string) {
        switch (action) {
        case PERMISSIONS_ACTIONS.grant:
            this.grant(userId);
            break;
        case PERMISSIONS_ACTIONS.deny:
            this.deny(userId);
            break;
        case PERMISSIONS_ACTIONS.error:
            this.sendRemoteControlEvent(userId, {
                type: EVENT_TYPES.permissions,
                action
            });
            break;
        default:

                // Unknown action. Ignore.
        }
    }

    /**
     * Denies remote control access for user associated with the passed user id.
     *
     * @param {string} userId - The id associated with the user who sent the
     * request for remote control authorization.
     * @returns {void}
     */
    deny(userId: string) {
        this.sendRemoteControlEvent(userId, {
            type: EVENT_TYPES.permissions,
            action: PERMISSIONS_ACTIONS.deny
        });
    }

    /**
     * Grants remote control access to user associated with the passed user id.
     *
     * @param {string} userId - The id associated with the user who sent the
     * request for remote control authorization.
     * @returns {void}
     */
    grant(userId: string) {
        APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT,
            this._userLeftListener);
        this._controller = userId;
        logger.log(`Remote control permissions granted to: ${userId}`);
        if (APP.conference.isSharingScreen) {
            this.sendRemoteControlEvent(userId, {
                type: EVENT_TYPES.permissions,
                action: PERMISSIONS_ACTIONS.grant
            });
        } else {
            APP.conference.toggleScreenSharing()
                .then(() => {
                    if (APP.conference.isSharingScreen) {
                        this.sendRemoteControlEvent(userId, {
                            type: EVENT_TYPES.permissions,
                            action: PERMISSIONS_ACTIONS.grant
                        });
                    } else {
                        this.sendRemoteControlEvent(userId, {
                            type: EVENT_TYPES.permissions,
                            action: PERMISSIONS_ACTIONS.error
                        });
                    }
                })
                .catch(() => {
                    this.sendRemoteControlEvent(userId, {
                        type: EVENT_TYPES.permissions,
                        action: PERMISSIONS_ACTIONS.error
                    });
                });
        }
    }

    /**
     * Handles remote control events from the external app. Currently only
     * events with type = EVENT_TYPES.supported or EVENT_TYPES.permissions.
     *
     * @param {RemoteControlEvent} event - The remote control event.
     * @returns {void}
     */
    _onRemoteControlAPIEvent(event: Object) {
        switch (event.type) {
        case EVENT_TYPES.permissions:
            this._onRemoteControlPermissionsEvent(event.userId, event.action);
            break;
        case EVENT_TYPES.supported:
            this._onRemoteControlSupported();
            break;
        }
    }

    /**
     * Handles events for support for executing remote control events into
     * the wrapper application.
     *
     * @returns {void}
     */
    _onRemoteControlSupported() {
        logger.log('Remote Control supported.');
        if (config.disableRemoteControl) {
            logger.log('Remote Control disabled.');
        } else {
            this._enable(true);
        }
    }

    /**
     * Calls the stop method if the other side have left.
     *
     * @param {string} id - The user id for the participant that have left.
     * @returns {void}
     */
    _onUserLeft(id: string) {
        if (this._controller === id) {
            this._stop();
        }
    }

    /**
     * Handles hangup events. Disables the receiver.
     *
     * @returns {void}
     */
    _onHangup() {
        this._enable(false);
    }
}
