/* --------------------------------------------------------------
 overview_settings_modal_controller.js 2017-12-06
 Gambio GmbH
 http://www.gambio.de
 Copyright (c) 2017 Gambio GmbH
 Released under the GNU General Public License (Version 2)
 [http://www.gnu.org/licenses/gpl-2.0.html]
 --------------------------------------------------------------
 */

jse.libs.overview_settings_modal_controller = jse.libs.overview_settings_modal_controller || {};

/**
 * Overview settings modal controller class.
 *
 * @module Admin/Libs/overview_settings_modal
 * @exports jse.libs.overview_settings_modal
 */
(function (exports) {
    /**
     * Class representing a controller for the orders overview settings modal.
     */
    class OverviewSettingsModalController {
        /**
         * Creates an instance of OrdersOverviewSettingsModalController.
         *
         * @param {jQuery}    $element              Module element.
         * @param {Object}    userCfgService        User configuration service library.
         * @param {Object}    loadingSpinner        Loading spinner library.
         * @param {Number}    userId                ID of currently signed in user.
         * @param {String}    defaultColumnSettings Default column settings.
         * @param {Object}    translator            Translator library.
         * @param {String}    page                  Page name (e.g.: 'orders', 'invoices').
         */
        constructor($element, userCfgService, loadingSpinner, userId, defaultColumnSettings, translator, page) {
            // Elements
            this.$element = $element;
            this.$submitButton = $element.find('button.submit-button');
            this.$settings = $element.find('ul.settings');
            this.$modal = $element.parents('.modal');
            this.$modalFooter = $element.find('.modal-footer');
            this.$resetDefaultLink = $element.find('a.reset-action');

            // Loading spinner
            this.$spinner = null;

            // Selector strings
            this.sortableHandleSelector = 'span.sort-handle';
            this.rowHeightValueSelector = 'select#setting-value-row-height';
            this.displayTooltipValueSelector = 'input#setting-value-display-tooltips';

            // Class names
            this.errorMessageClassName = 'error-message';
            this.loadingClassName = 'loading';

            // Libraries
            this.userCfgService = userCfgService;
            this.loadingSpinner = loadingSpinner;
            this.translator = translator;

            // Prefixes
            this.settingListItemIdPrefix = 'setting-';
            this.settingValueIdPrefix = 'setting-value-';

            // User configuration keys
            this.CONFIG_KEY_COLUMN_SETTINGS = `${page}OverviewSettingsColumns`;
            this.CONFIG_KEY_ROW_HEIGHT_SETTINGS = `${page}OverviewSettingsRowHeight`;
            this.CONFIG_KEY_DISPLAY_TOOLTIPS_SETTINGS = `${page}OverviewSettingsDisplayTooltips`;

            // Default values
            this.DEFAULT_ROW_HEIGHT_SETTING = 'large';
            this.DEFAULT_COLUMN_SETTINGS = defaultColumnSettings.split(',');
            this.DEFAULT_DISPLAY_TOOLTIPS_SETTINGS = 'true';

            // ID of currently signed in user.
            this.userId = userId;
        }

        /**
         * Binds the event handlers.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         */
        initialize() {
            // Attach event handler for click action on the submit button.
            this.$submitButton.on('click', event => this._onSubmitButtonClick(event));

            // Attach event handler for click action on the reset-default link.
            this.$resetDefaultLink.on('click', event => this._onResetSettingsLinkClick(event));

            // Attach event handlers to modal.
            this.$modal
                .on('show.bs.modal', event => this._onModalShow(event))
                .on('shown.bs.modal', event => this._onModalShown(event));

            return this;
        }

        /**
         * Fades out the modal content.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         *
         * @private
         */
        _onModalShow() {
            this.$element.addClass(this.loadingClassName);

            return this;
        }

        /**
         * Updates the settings, clears any error messages and initializes the sortable plugin.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         *
         * @private
         */
        _onModalShown() {
            this
                ._refreshSettings()
                ._clearErrorMessage()
                ._initSortable();

            return this;
        }

        /**
         * Activates the jQuery UI Sortable plugin on the setting list items element.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         *
         * @private
         */
        _initSortable() {
            // jQuery UI Sortable plugin options.
            const options = {
                items: '> li',
                axis: 'y',
                cursor: 'move',
                handle: this.sortableHandleSelector,
                containment: 'parent'
            };

            // Activate sortable plugin.
            this.$settings
                .sortable(options)
                .disableSelection();

            return this;
        }

        /**
         * Returns a sorted array containing the IDs of all activated settings.
         *
         * @return {Array}
         *
         * @private
         */
        _serializeColumnSettings() {
            // Map iterator function to remove the 'setting-' prefix from list item ID.
            const removePrefixIterator = item => item.replace(this.settingListItemIdPrefix, '');

            // Filter iterator function, to accept only list items with activated checkboxes.
            const filterIterator = item => this.$settings.find('#' + this.settingValueIdPrefix + item)
                .is(':checked');

            // Return array with sorted, only active columns.
            return this.$settings
                .sortable('toArray')
                .map(removePrefixIterator)
                .filter(filterIterator);
        }

        /**
         * Returns the value of the selected row height option.
         *
         * @return {String}
         *
         * @private
         */
        _serializeRowHeightSetting() {
            return this
                .$element
                .find(this.rowHeightValueSelector)
                .val();
        }

        /**
         * Returns the value of the selected tooltip display option.
         *
         * @return {String}
         *
         * @private
         */
        _serializeDisplayTooltipSetting() {
            return this
                .$element
                .find(this.displayTooltipValueSelector)
                .prop('checked');
        }

        /**
         * Shows the loading spinner, saves the settings to the user configuration,
         * closes the modal to finally re-render the datatable.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         *
         * @private
         */
        _onSubmitButtonClick() {
            // Retrieve setting values.
            const columnSettings = this._serializeColumnSettings();
            const rowHeightSetting = this._serializeRowHeightSetting();
            const displayTooltipSetting = this._serializeDisplayTooltipSetting();

            // Remove any error message and save settings.
            this
                ._toggleLoadingSpinner(true)
                ._clearErrorMessage()
                ._saveColumnSettings(columnSettings)
                .then(() => this._saveDisplayTooltipSetting(displayTooltipSetting))
                .then(() => this._saveRowHeightSetting(rowHeightSetting))
                .then(() => this._onSaveSuccess())
                .catch(() => this._onSaveError());

            return this;
        }

        /**
         * Prevents the browser to apply the default behavoir and
         * resets the column order and row size to the default setting values.
         *
         * @param {jQuery.Event} event Fired event.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         *
         * @private
         */
        _onResetSettingsLinkClick(event) {
            // Prevent default behavior.
            event.preventDefault();
            event.stopPropagation();

            // Reset to default settings.
            this._setDefaultSettings();

            return this;
        }

        /**
         * Shows and hides the loading spinner.
         *
         * @param {Boolean} doShow Show the loading spinner?
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         */
        _toggleLoadingSpinner(doShow) {
            if (doShow) {
                // Fade out modal content.
                this.$element.addClass(this.loadingClassName);

                // Show loading spinner.
                this.$spinner = this.loadingSpinner.show(this.$element);

                // Fix spinner z-index.
                this.$spinner.css({'z-index': 9999});
            } else {
                // Fade out modal content.
                this.$element.removeClass(this.loadingClassName);

                // Hide the loading spinner.
                this.loadingSpinner.hide(this.$spinner);
            }

            return this;
        }

        /**
         * Handles the behavior on successful setting save action.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         *
         * @private
         */
        _onSaveSuccess() {
            window.location.reload();
            return this;
        }

        /**
         * Removes any error message, if found.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         *
         * @private
         */
        _clearErrorMessage() {
            // Error message.
            const $errorMessage = this.$modalFooter.find(`.${this.errorMessageClassName}`);

            // Remove if it exists.
            if ($errorMessage.length) {
                $errorMessage.remove();
            }

            return this;
        }

        /**
         * Handles the behavior on thrown error while saving settings.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         *
         * @private
         */
        _onSaveError() {
            // Error message.
            const errorMessage = this.translator.translate('TXT_SAVE_ERROR', 'admin_general');

            // Define error message element.
            const $error = $('<span/>', {class: this.errorMessageClassName, text: errorMessage});

            // Hide the loading spinner.
            this._toggleLoadingSpinner(false);

            // Add error message to modal footer.
            this.$modalFooter
                .prepend($error)
                .hide()
                .fadeIn();

            return this;
        }

        /**
         * Returns the configuration value for the column settings.
         *
         * @return {Promise}
         *
         * @private
         */
        _getColumnSettings() {
            // Configuration data.
            const data = {
                userId: this.userId,
                configurationKey: this.CONFIG_KEY_COLUMN_SETTINGS
            };

            // Request data from user configuration service.
            return this._getFromUserCfgService(data);
        }

        /**
         * Returns the configuration value for the row heights.
         *
         * @return {Promise}
         *
         * @private
         */
        _getRowHeightSetting() {
            // Configuration data.
            const data = {
                userId: this.userId,
                configurationKey: this.CONFIG_KEY_ROW_HEIGHT_SETTINGS
            };

            // Request data from user configuration service.
            return this._getFromUserCfgService(data);
        }

        /**
         * Returns the configuration value for the tooltip display option.
         *
         * @return {Promise}
         *
         * @private
         */
        _getDisplayTooltipSetting() {
            // Configuration data.
            const data = {
                userId: this.userId,
                configurationKey: this.CONFIG_KEY_DISPLAY_TOOLTIPS_SETTINGS
            };

            // Request data from user configuration service.
            return this._getFromUserCfgService(data);
        }

        /**
         * Returns the value for the passed user configuration data.
         *
         * @param {Object} data                   User configuration data.
         * @param {Number} data.userId            User ID.
         * @param {String} data.configurationKey  User configuration key.
         *
         * @return {Promise}
         *
         * @private
         */
        _getFromUserCfgService(data) {
            // Promise handler.
            const handler = (resolve, reject) => {
                // User configuration service request options.
                const options = {
                    onError: () => reject(),
                    onSuccess: response => resolve(response.configurationValue),
                    data
                };

                // Get configuration value.
                this.userCfgService.get(options);
            };

            return new Promise(handler);
        }

        /**
         * Saves the data via the user configuration service.
         *
         * @param {Object} data                     User configuration data.
         * @param {Number} data.userId              User ID.
         * @param {String} data.configurationKey    User configuration key.
         * @param {String} data.configurationValue  User configuration value.
         *
         * @return {Promise}
         *
         * @private
         */
        _setWithUserCfgService(data) {
            // Promise handler.
            const handler = (resolve, reject) => {
                // User configuration service request options.
                const options = {
                    onError: () => reject(),
                    onSuccess: response => resolve(),
                    data
                };

                // Set configuration value.
                this.userCfgService.set(options);
            };

            return new Promise(handler);
        }

        /**
         * Saves the column settings via the user configuration service.
         *
         * @param {String[]} columnSettings Sorted array with active column.
         *
         * @return {Promise}
         *
         * @private
         */
        _saveColumnSettings(columnSettings) {
            // Check argument.
            if (!columnSettings || !Array.isArray(columnSettings)) {
                throw new Error('Missing or invalid column settings');
            }

            // User configuration request data.
            const data = {
                userId: this.userId,
                configurationKey: this.CONFIG_KEY_COLUMN_SETTINGS,
                configurationValue: JSON.stringify(columnSettings)
            };

            // Save via user configuration service.
            return this._setWithUserCfgService(data);
        }

        /**
         * Saves the row height setting via the user configuration service.
         *
         * @param {String} rowHeightSetting Value of the selected row height setting.
         *
         * @return {Promise}
         *
         * @private
         */
        _saveRowHeightSetting(rowHeightSetting) {
            // Check argument.
            if (!rowHeightSetting || typeof rowHeightSetting !== 'string') {
                throw new Error('Missing or invalid row height setting');
            }

            // User configuration request data.
            const data = {
                userId: this.userId,
                configurationKey: this.CONFIG_KEY_ROW_HEIGHT_SETTINGS,
                configurationValue: rowHeightSetting
            };

            // Save via user configuration service.
            return this._setWithUserCfgService(data);
        }

        /**
         * Saves the display tooltip setting via the user configuration service.
         *
         * @param {String} displayTooltipSetting Value.
         *
         * @return {Promise}
         *
         * @private
         */
        _saveDisplayTooltipSetting(displayTooltipSetting) {
            // User configuration request data.
            const data = {
                userId: this.userId,
                configurationKey: this.CONFIG_KEY_DISPLAY_TOOLTIPS_SETTINGS,
                configurationValue: displayTooltipSetting
            };

            // Save via user configuration service.
            return this._setWithUserCfgService(data);
        }

        /**
         * Retrieves the saved setting configuration and reorders/updates the settings.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         *
         * @private
         */
        _refreshSettings() {
            // Show loading spinner.
            this._toggleLoadingSpinner(true);

            // Error handler function to specify the behavior on errors while processing.
            const onRefreshSettingsError = error => {
                // Output warning.
                console.warn('Error while refreshing', error);

                // Hide the loading spinner.
                this._toggleLoadingSpinner(false);
            };

            // Remove any error message, set row height,
            // reorder and update the settings and hide the loading spinner.
            this
                ._clearErrorMessage()
                ._getRowHeightSetting()
                .then(rowHeightValue => this._setRowHeight(rowHeightValue))
                .then(() => this._getDisplayTooltipSetting())
                .then(displayTooltipSetting => this._setDisplayTooltipSetting(displayTooltipSetting))
                .then(() => this._getColumnSettings())
                .then(columnSettings => this._setColumnSettings(columnSettings))
                .then(() => this._toggleLoadingSpinner(false))
                .catch(onRefreshSettingsError);

            return this;
        }

        /**
         * Sets the row height setting value.
         *
         * @param {String} value Row height value.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         *
         * @private
         */
        _setRowHeight(value = this.DEFAULT_ROW_HEIGHT_SETTING) {
            this
                .$element
                .find(this.rowHeightValueSelector)
                .val(value);

            return this;
        }

        /**
         * Sets the display tooltips setting value.
         *
         * @param {String} value Display tooltips value.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         *
         * @private
         */
        _setDisplayTooltipSetting(value = this.DEFAULT_DISPLAY_TOOLTIPS_SETTINGS) {
            this
                .$element
                .find(this.displayTooltipValueSelector)
                .prop('checked', value === 'true')
                .trigger('change');

            return this;
        }

        /**
         * Reorders and updates the column setting values.
         *
         * @param {String|Array} columnSettings Stringified JSON array containing the saved column settings.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         *
         * @private
         */
        _setColumnSettings(columnSettings = this.DEFAULT_COLUMN_SETTINGS) {
            // Regex for escape character.
            const ESCAPE_CHAR = /\\/g;

            // No need to parse from JSON on default value as it is an array.
            if (!Array.isArray(columnSettings)) {
                // Remove escape characters from and parse array from JSON.
                columnSettings = columnSettings.replace(ESCAPE_CHAR, '');
                columnSettings = JSON.parse(columnSettings);
            }

            // Cache container to temporarily hold all active list items in sorted order.
            // The children of this element will be prepended to the setting list item container to retain the
            // sorting order.
            const $sortedItems = $('<div/>');

            // Iterator function to prepend active list items to the top and activate the checkbox.
            const settingIterator = setting => {
                // List item ID.
                const id = this.settingListItemIdPrefix + setting;

                // Affected setting list item.
                const $listItem = this.$settings.find(`#${id}`);

                // Checkbox of affected list item.
                const $checkbox = $listItem.find('#' + this.settingValueIdPrefix + setting);

                // Activate checkbox.
                if (!$checkbox.is(':checked')) {
                    $checkbox.parent().trigger('click');
                }

                // Move to cache container.
                $listItem.appendTo($sortedItems);
            };

            // Move active list items to the top bearing the sorting order in mind.
            columnSettings.forEach(settingIterator);

            // Prepend cached elements to item list.
            $sortedItems
                .children()
                .prependTo(this.$settings);

            return this;
        }

        /**
         * Resets the column order and row height settings to the default.
         *
         * @return {OverviewSettingsModalController} Same instance for method chaining.
         *
         * @private
         */
        _setDefaultSettings() {
            // Default values.
            const columnSettings = this.DEFAULT_COLUMN_SETTINGS;
            const rowHeight = this.DEFAULT_ROW_HEIGHT_SETTING;

            // Set column settings.
            // Cache container to temporarily hold all active list items in sorted order.
            // The children of this element will be prepended to the setting list item container to retain the
            // sorting order.
            const $sortedItems = $('<div/>');

            // Iterator function to prepend active list items to the top and activate the checkbox.
            const settingIterator = setting => {
                // List item ID.
                const id = this.settingListItemIdPrefix + setting;

                // Affected setting list item.
                const $listItem = this.$settings.find(`#${id}`);

                // Checkbox of affected list item.
                const $checkbox = $listItem.find('#' + this.settingValueIdPrefix + setting);

                // Activate checkbox.
                if (!$checkbox.is(':checked')) {
                    $checkbox.parent().trigger('click');
                }

                // Move to cache container.
                $listItem.appendTo($sortedItems);
            };

            // Deactivate all checkboxes.
            this
                .$settings
                .find(':checkbox')
                .each((index, element) => {
                    const $checkbox = $(element);

                    if ($checkbox.is(':checked')) {
                        $checkbox.parent().trigger('click');
                    }
                });

            // Move active list items to the top bearing the sorting order in mind.
            columnSettings.forEach(settingIterator);

            // Prepend cached elements to item list.
            $sortedItems
                .children()
                .prependTo(this.$settings);

            // Set row height.
            this
                .$element
                .find(this.rowHeightValueSelector)
                .val(rowHeight);

            return this;
        }
    }

    exports.class = OverviewSettingsModalController;
}(jse.libs.overview_settings_modal_controller));