import JitsiMeetJS from '../lib-jitsi-meet';
import {
    getUserSelectedOutputDeviceId,
    updateSettings
} from '../settings';

import {
    ADD_PENDING_DEVICE_REQUEST,
    CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
    NOTIFY_CAMERA_ERROR,
    NOTIFY_MIC_ERROR,
    REMOVE_PENDING_DEVICE_REQUESTS,
    SET_AUDIO_INPUT_DEVICE,
    SET_VIDEO_INPUT_DEVICE,
    UPDATE_DEVICE_LIST
} from './actionTypes';
import {
    areDeviceLabelsInitialized,
    getDeviceIdByLabel,
    getDevicesFromURL,
    setAudioOutputDeviceId
} from './functions';

const logger = require('jitsi-meet-logger').getLogger(__filename);

/**
 * Maps the WebRTC string for device type to the keys used to store configure,
 * within redux, which devices should be used by default.
 */
const DEVICE_TYPE_TO_SETTINGS_KEYS = {
    audioInput: {
        currentDeviceId: 'micDeviceId',
        userSelectedDeviceId: 'userSelectedMicDeviceId',
        userSelectedDeviceLabel: 'userSelectedMicDeviceLabel'
    },
    audioOutput: {
        currentDeviceId: 'audioOutputDeviceId',
        userSelectedDeviceId: 'userSelectedAudioOutputDeviceId',
        userSelectedDeviceLabel: 'userSelectedAudioOutputDeviceLabel'
    },
    videoInput: {
        currentDeviceId: 'audioOutputDeviceId',
        userSelectedDeviceId: 'userSelectedCameraDeviceId',
        userSelectedDeviceLabel: 'userSelectedCameraDeviceLabel'
    }
};

/**
 * Adds a pending device request.
 *
 * @param {Object} request - The request to be added.
 * @returns {{
 *      type: ADD_PENDING_DEVICE_REQUEST,
 *      request: Object
 * }}
 */
export function addPendingDeviceRequest(request) {
    return {
        type: ADD_PENDING_DEVICE_REQUEST,
        request
    };
}

/**
 * Configures the initial A/V devices before the conference has started.
 *
 * @returns {Function}
 */
export function configureInitialDevices() {
    return (dispatch, getState) => {
        const deviceLabels = getDevicesFromURL(getState());
        let updateSettingsPromise;

        if (deviceLabels) {
            updateSettingsPromise = dispatch(getAvailableDevices()).then(() => {
                const state = getState();

                if (!areDeviceLabelsInitialized(state)) {
                    // The labels are not available if the A/V permissions are
                    // not yet granted.

                    Object.keys(deviceLabels).forEach(key => {
                        dispatch(addPendingDeviceRequest({
                            type: 'devices',
                            name: 'setDevice',
                            device: {
                                kind: key.toLowerCase(),
                                label: deviceLabels[key]
                            },
                            // eslint-disable-next-line no-empty-function
                            responseCallback() {}
                        }));
                    });

                    return;
                }

                const newSettings = {};

                Object.keys(deviceLabels).forEach(key => {
                    const label = deviceLabels[key];
                    const deviceId = getDeviceIdByLabel(state, label, key);

                    if (deviceId) {
                        const settingsTranslationMap = DEVICE_TYPE_TO_SETTINGS_KEYS[key];

                        newSettings[settingsTranslationMap.currentDeviceId] = deviceId;
                        newSettings[settingsTranslationMap.userSelectedDeviceId] = deviceId;
                        newSettings[settingsTranslationMap.userSelectedDeviceLabel] = label;
                    }
                });

                dispatch(updateSettings(newSettings));
            });
        } else {
            updateSettingsPromise = Promise.resolve();
        }

        return updateSettingsPromise
            .then(() => {
                const userSelectedAudioOutputDeviceId = getUserSelectedOutputDeviceId(getState());

                return setAudioOutputDeviceId(userSelectedAudioOutputDeviceId, dispatch)
                    .catch(ex => logger.warn(`Failed to set audio output device.
                        Default audio output device will be used instead ${ex}`));
            });
    };
}

/**
 * Queries for connected A/V input and output devices and updates the redux
 * state of known devices.
 *
 * @returns {Function}
 */
export function getAvailableDevices() {
    return dispatch => new Promise(resolve => {
        const { mediaDevices } = JitsiMeetJS;

        if (mediaDevices.isDeviceListAvailable()
                && mediaDevices.isDeviceChangeAvailable()) {
            mediaDevices.enumerateDevices(devices => {
                dispatch(updateDeviceList(devices));

                resolve(devices);
            });
        } else {
            resolve([]);
        }
    });
}

/**
 * Signals that an error occurred while trying to obtain a track from a camera.
 *
 * @param {Object} error - The device error, as provided by lib-jitsi-meet.
 * @param {string} error.name - The constant for the type of the error.
 * @param {string} error.message - Optional additional information about the
 * error.
 * @returns {{
 *     type: NOTIFY_CAMERA_ERROR,
 *     error: Object
 * }}
 */
export function notifyCameraError(error) {
    return {
        type: NOTIFY_CAMERA_ERROR,
        error
    };
}

/**
 * Signals that an error occurred while trying to obtain a track from a mic.
 *
 * @param {Object} error - The device error, as provided by lib-jitsi-meet.
 * @param {Object} error.name - The constant for the type of the error.
 * @param {string} error.message - Optional additional information about the
 * error.
 * @returns {{
 *     type: NOTIFY_MIC_ERROR,
 *     error: Object
 * }}
 */
export function notifyMicError(error) {
    return {
        type: NOTIFY_MIC_ERROR,
        error
    };
}

/**
 * Remove all pending device requests.
 *
 * @returns {{
 *      type: REMOVE_PENDING_DEVICE_REQUESTS
 * }}
 */
export function removePendingDeviceRequests() {
    return {
        type: REMOVE_PENDING_DEVICE_REQUESTS
    };
}

/**
 * Signals to update the currently used audio input device.
 *
 * @param {string} deviceId - The id of the new audio input device.
 * @returns {{
 *      type: SET_AUDIO_INPUT_DEVICE,
 *      deviceId: string
 * }}
 */
export function setAudioInputDevice(deviceId) {
    return {
        type: SET_AUDIO_INPUT_DEVICE,
        deviceId
    };
}

/**
 * Signals to update the currently used video input device.
 *
 * @param {string} deviceId - The id of the new video input device.
 * @returns {{
 *      type: SET_VIDEO_INPUT_DEVICE,
 *      deviceId: string
 * }}
 */
export function setVideoInputDevice(deviceId) {
    return {
        type: SET_VIDEO_INPUT_DEVICE,
        deviceId
    };
}

/**
 * Signals to update the list of known audio and video devices.
 *
 * @param {Array<MediaDeviceInfo>} devices - All known available audio input,
 * audio output, and video input devices.
 * @returns {{
 *      type: UPDATE_DEVICE_LIST,
 *      devices: Array<MediaDeviceInfo>
 * }}
 */
export function updateDeviceList(devices) {
    return {
        type: UPDATE_DEVICE_LIST,
        devices
    };
}

/**
 * Signals to check new and old devices for newly added devices and notify.
 *
 * @param {Array<MediaDeviceInfo>} newDevices - Array of the new devices.
 * @param {Array<MediaDeviceInfo>} oldDevices - Array of the old devices.
 * @returns {{
 *      type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
 *      newDevices: Array<MediaDeviceInfo>,
 *      oldDevices: Array<MediaDeviceInfo>
 * }}
 */
export function checkAndNotifyForNewDevice(newDevices, oldDevices) {
    return {
        type: CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
        newDevices,
        oldDevices
    };
}
