/**
 * Module for creating an manipulating modal dialogs.
 * Normal layout of a dialog:
 *
 * <div class="modal fade">
 *     <div class="modal-dialog">
 *         <div class="modal-content">
 *             <div class="modal-header"></div>
 *             <div class="modal-body"></div>
 *             <div class="modal-footer"></div>
 *         </div>
 *     </div>
 * </div>
 *
 * @param {type} param1
 * @param {type} param2
 */
humhub.module('ui.modal', function (module, require, $) {
    var util = require('util');
    var object = util.object;
    var additions = require('ui.additions');
    var loader = require('ui.loader');
    var client = require('client', true);
    var Widget = require('ui.widget').Widget;

    //Keeps track of all initialized modals
    var modals = {};

    var ERROR_DEFAULT_TITLE = 'Error';
    var ERROR_DEFAULT_MESSAGE = 'An unknown error occurred!';

    /**
     * The Modal class can be used to create new modals or manipulate existing modals.
     * If the constructor finds an element with the given id we use the existing modal,
     * if the id is not already used, we create a new modal dom element.
     *
     * @param {string} id - id of the modal
     */
    var Modal = function (node, options) {
        if (!$(node).length) {
            node = this.createModal(node);
        }
        Widget.call(this, node, options);
    };

    object.inherits(Modal, Widget);

    Modal.component = 'humhub-ui-modal';

    Modal.prototype.getModalInstance = function () {
        return bootstrap.Modal.getOrCreateInstance(this.$);
    };

    Modal.prototype.init = function () {
        this.initModal(this.options);
        modals[this.$.attr('id')] = this;
    };

    /**
     * Template for the modal splitted into different parts. Those can be overwritten by changing or overwriting module.template.
     */
    Modal.template = {
        container: '<div class="modal fade" aria-hidden="true"><div class="modal-dialog"><div class="modal-content"></div></div></div>',
        title: '<h5 class="modal-title"></h5>',
        closeButton: '<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>',
        body: '<div class="modal-body"></div>',
        footer: '<div class="modal-footer"></div>',
    };
    Modal.template.header = '<div class="modal-header">' + Modal.template.title + Modal.template.closeButton + '</div>';

    /**
     * Creates a new modal dom skeleton.
     * @param {type} id the modal id
     * @returns {undefined}
     */
    Modal.prototype.createModal = function (id) {
        var modal = $(this.getTemplate('container')).attr('id', id);
        $('body').append(modal);
        return modal;
    };

    Modal.prototype.getTemplate = function (id) {
        return Modal.template[id];
    };

    /**
     * Initializes default modal events and sets initial data.
     * @returns {undefined}
     */
    Modal.prototype.initModal = function (options) {
        var that = this;

        //Set default modal manipulation event handlers
        this.$.off('click.modal').on('click.modal', '[data-modal-close]', function () {
            that.close();
        }).on('click', '[data-modal-clear-error]', function () {
            that.clearErrorMessage();
        });

        this.set(options);

        // Store initial data to reset it
        this.initialOptions = options;
        this.initialDialog = this.getDialog().clone(true);
    };

    Modal.prototype.checkAriaLabel = function () {
        var $title = this.$.find('.modal-title');
        if ($title.length) {
            $title.attr('id', this.getTitleId());
            this.$.attr('aria-labelledby', this.getTitleId());
        } else {
            this.$.removeAttr('aria-labelledby');
        }
    };

    Modal.prototype.getTitleId = function () {
        return this.$.attr('id') + '-title';
    };

    /**
     * Closes the modal with fade animation and sets the loader content
     * @returns {undefined}
     */
    Modal.prototype.close = function (reset) {
        var instance = this.getModalInstance();

        // Bootstrap silently ignores hide() while the show transition is still
        // running, which leaves the modal stuck open forever. Defer the hide
        // until the transition finishes in that case. Only do this while
        // transitioning in (_isShown is true) — while transitioning out the
        // modal is already closing, and a deferred hide would close the modal
        // the next time it is shown again.
        if (instance._isTransitioning && instance._isShown) {
            this.$.one('shown.bs.modal', function () {
                instance.hide();
            });
        } else {
            instance.hide();
        }

        if (reset) {
            this.reset();
        }
    };

    /**
     * Sets the loader content and shows the modal
     * @returns {undefined}
     */
    Modal.prototype.loader = function (evt) {
        this.reset();
        this.show();
        if (typeof(evt) === 'object' && typeof(evt.$trigger) === 'object' && evt.$trigger.data('message')) {
            this.setHeader(evt.$trigger.data('message'));
        }
    };

    /**
     * Sets the loader to footer in order to inactivate the action buttons
     */
    Modal.prototype.footerLoader = function (evt) {
        if (evt instanceof $.Event) {
            evt.$form = evt.$trigger.closest('form');
            evt.length = evt.$form.length;
        }
        loader.set(this.getFooter(), {css: {padding: '13px 0 14px'}});
    }

    /**
     * Sets the default content (a loader animation)
     * @returns {undefined}
     */
    Modal.prototype.reset = function () {
        this.isFilled = false;
        // Remove scripts from previous modal window
        this.$.find('script').remove();
        // Reset dialog to initial html
        this.getDialog().replaceWith(this.initialDialog.clone(true));
        // Reset options in order to display initial window on load new window
        this.options = this.initialOptions;
        // Reset listeners to avoid unexpected js behaviour
        this.resetListener();
    };

    /**
     * Resets some listeners of this modal isntance.
     * @returns {undefined}
     */
    Modal.prototype.resetListener = function () {
        this.$.off('submitted');
    };

    /**
     * Sets the given content and applies content additions.
     * @param {string|jQuery} content - content to be set
     * @param {function} callback - callback function is called after html was inserted
     * @returns {undefined}
     */
    Modal.prototype.setContent = function (content, callback) {
        var that = this;
        return new Promise(function (resolve, reject) {
            // TODO: assure content order header/content/footer
            try {
                that.clearErrorMessage();
                that.getContent().html(content).promise().always(function () {
                    that.applyAdditions();
                });
                that.isFilled = true;
                resolve(that);
            } catch (err) {
                that.setErrorMessage(err.message);
                // We try to apply additions anyway
                that.applyAdditions();
                reject(err);
            }
        });
    };

    Modal.prototype.applyAdditions = function () {
        additions.applyTo(this.getContent());
    };

    Modal.prototype.load = function (url, cfg, originalEvent) {
        var that = this;
        cfg = setDefaultRequestData(cfg);

        return new Promise(function (resolve, reject) {
            if (!that.isVisible()) {
                that.loader(url);
            }
            client.get(url, cfg, originalEvent).then(function (response) {
                that.setDialog(response);
                resolve(response);
                that.focus();
            }).catch(reject);
        });
    };

    Modal.prototype.post = function (url, cfg, originalEvent) {
        var that = this;
        cfg = setDefaultRequestData(cfg);

        return new Promise(function (resolve, reject) {
            if (!that.isVisible()) {
                that.loader(url);
            }
            client.post(url, cfg, originalEvent).then(function (response) {
                that.setDialog(response);
                resolve(response);
            }).catch(reject);
        });
    };

    var setDefaultRequestData = function (cfg) {
        cfg = cfg || {};
        cfg.data = cfg.data || {};
        cfg.viewContext = cfg.viewContext || 'modal';
        return cfg;
    };

    /**
     * Sets an errormessage and title. This function either creates an standalone
     * error modal with title and message, or adds/replaces a errorboxmessage to
     * already exising and filled modals.
     * @param {type} title
     * @param {type} message
     * @returns {undefined}
     */
    Modal.prototype.error = function (title, message) {

        if (arguments.length === 1 && title) {
            message = (title.getFirstError) ? title.getFirstError() : title;
            title = (title.getErrorTitle) ? title.getErrorTitle() : ERROR_DEFAULT_TITLE;
        }

        title = title || ERROR_DEFAULT_TITLE;
        message = message || ERROR_DEFAULT_MESSAGE;

        //If there is no content yet we create an error only content
        if (!this.isFilled) {
            this.clear();
            this.setHeader(title);
            this.setBody('');
            this.setErrorMessage(message);
            this.show();
        } else {
            //TODO: allow to set errorMessage and title even for inline messages
            this.setErrorMessage(message);
        }
    };

    /**
     * Removes existing error messages
     * @returns {undefined}
     */
    Modal.prototype.clearErrorMessage = function () {
        var modalError = this.getErrorMessage();
        if (modalError.length) {
            modalError.fadeOut('fast', function () {
                modalError.remove();
            });
        }
    };

    /**
     * Adds or replaces an errormessagebox
     * @param {type} message
     * @returns {undefined}
     */
    Modal.prototype.setErrorMessage = function (message) {
        var $errorMessage = this.getErrorMessage();
        if ($errorMessage.length) {
            $errorMessage.css('opacity', 0);
            $errorMessage.text(message);
            $errorMessage.animate({'opacity': 1}, 'fast');
        } else {
            this.getBody().prepend('<div class="modal-error alert alert-danger">' + message + '</div>');
        }
    };

    /**
     * Returns the current errormessagebox
     * @returns {humhub.ui.modal_L18.Modal.prototype@call;getContent@call;find}
     */
    Modal.prototype.getErrorMessage = function () {
        return this.getContent().find('.modal-error');
    };

    /**
     * Shows the modal
     * @returns {undefined}
     */
    Modal.prototype.show = function () {
        if (!this.$.is(':visible')) {
            this.set(this.options);
            this.getModalInstance().show();
            this.focus();
        }

        this.getDialog().show();
    };

    /**
     * Clears the modal content
     * @returns {undefined}
     */
    Modal.prototype.clear = function () {
        this.getContent().empty();
    };

    /**
     * Retrieves the modal content jQuery representation
     * @returns {humhub.ui.modal_L18.Modal.prototype@pro;$modal@call;find}
     */
    Modal.prototype.getContent = function () {
        //We use the :first selector since jQuery refused to execute javascript if we set content with inline js
        return this.$.find('.modal-content:first');
    };

    /**
     * Retrieves the modal dialog jQuery representation
     * @returns {humhub.ui.modal_L18.Modal.prototype@pro;$modal@call;find}
     */
    Modal.prototype.getDialog = function () {
        return this.$.find('.modal-dialog');
    };

    /**
     * Returns the modal footer
     * @returns {humhub.ui.modal_L18.Modal.prototype@pro;$modal@call;find}
     */
    Modal.prototype.getFooter = function () {
        return this.$.find('.modal-footer');
    };

    /**
     * Searches for forms within the modal
     * @returns {humhub.ui.modal_L18.Modal.prototype@pro;$modal@call;find}
     */
    Modal.prototype.getForm = function () {
        return this.$.find('form');
    };

    /**
     * Adds or replaces a modal-title with close button and a title text.
     * @param {type} title
     * @returns {undefined}
     */
    Modal.prototype.setHeader = function (title) {
        let $header = this.getHeader();
        if (!$header.length) {
            $header = $(this.getTemplate('header'));
            this.getContent().prepend($header);
        }
        let $title = $header.find('.modal-title');
        if (!$title.length) {
            $title = $(this.getTemplate('title'));
            $header.prepend($title);
        }

        // Set title id for aria-labelledby
        $title.attr('id', this.getTitleId()).html(title);
    };

    Modal.prototype.setFooter = function (footer) {
        var $footer = this.getFooter();
        if (!$footer.length) {
            $footer = $(this.getTemplate('footer'));
            this.getContent().append($footer);
        }

        $footer.html(footer);
    };

    Modal.prototype.set = function (options) {
        this.options = options;

        if (this.options.header) {
            this.setHeader(this.options.header);
        }

        if (this.options.body) {
            this.setBody(this.options.body);
        }

        if (this.options.content) {
            this.setContent(this.options.content);
        }

        if (this.options.footer) {
            this.setFooter(this.options.footer);
        }

        if (this.options.size) {
            this.getDialog().removeClass(['modal-sm', 'modal-lg', 'modal-xl']).addClass('modal-' + this.options.size);
        }

        this.options.backdrop = object.defaultValue(options.backdrop, this.$.data('bs-backdrop'));
        this.options.keyboard = object.defaultValue(options.keyboard, this.$.data('bs-keyboard'));

        // Update Bootstrap modal instance
        const bsModalInstance = this.getModalInstance();
        if (bsModalInstance) {
            this.$.attr('data-bs-backdrop', this.options.backdrop);
            bsModalInstance._config.backdrop = this.options.backdrop;
            this.$.attr('data-bs-keyboard', this.options.keyboard);
            bsModalInstance._config.keyboard = this.options.keyboard;
        }

        return this;
    };

    /**
     * Retrieves the modal-header element
     * @returns {humhub.ui.modal_L18.Modal.prototype@pro;$modal@call;find}
     */
    Modal.prototype.getHeader = function () {
        return this.$.find('.modal-header');
    };

    /**
     * Adds or replaces the current modal-body
     * @param {type} content
     * @returns {undefined}
     */
    Modal.prototype.setBody = function (content) {
        var $body = this.getBody();
        if (!$body.length) {
            this.getContent().append($(this.getTemplate('body')));
            $body = this.getBody();
        }
        $body.html(content);
    };

    Modal.prototype.setDialog = function (content) {
        if (content instanceof client.Response) {
            if (content.dataType === 'json') {
                content = content.output;
            } else {
                content = content.html;
            }
        }

        if (content) {
            this.$.empty().append(content);
            this.applyAdditions();
            this.focus();
            this.checkAriaLabel();
            this.updateDialogOptions();
            this.$.scrollTop(0);
        } else {
            this.close(true);
        }

        return this;
    };

    Modal.prototype.focus = function () {
        var that = this;
        setTimeout(function () {
            var $input = that.$.find('select[autofocus]:visible:not(:disabled), input[autofocus][type="text"]:visible:not(:disabled), textarea[autofocus]:visible:not(:disabled), [contenteditable="true"][autofocus]:visible:not(:disabled)').first();

            if ($input.data('select2')) {
                $input.select2('focus');
            } else {
                $input.focus();
            }
        }, 100);
    };

    /**
     * Update Modal options from Dialog HumHub options defined in [[Modal::renderDialogBegin()]]
     */
    Modal.prototype.updateDialogOptions = function () {
        this.set({
            backdrop: this.getDialog().data('hh-backdrop'),
            keyboard: this.getDialog().data('hh-keyboard')
        });
    };

    /**
     * Retrieves the modal-body element
     * @returns {humhub.ui.modal_L18.Modal.prototype@pro;$modal@call;find}
     */
    Modal.prototype.getBody = function () {
        return this.$.find('.modal-body');
    };

    var ConfirmModal = function (node, options) {
        Modal.call(this, node, options);
    };

    object.inherits(ConfirmModal, Modal);

    ConfirmModal.prototype.open = function (cfg) {
        var that = this;
        return new Promise(function (resolve, reject) {
            cfg = cfg || {};

            cfg.handler = resolve;
            cfg.reject = reject;

            that.clear();
            cfg['header'] = cfg['header'] || module.config.defaultConfirmHeader;
            cfg['body'] = cfg['body'] || module.config.defaultConfirmBody;
            cfg['confirmText'] = cfg['confirmText'] || module.config.defaultConfirmText;
            cfg['cancelText'] = cfg['cancelText'] || module.config.defaultCancelText;
            that.setHeader(cfg['header']);
            that.setBody(cfg['body']);
            that.initButtons(cfg);
            that.show();
        });
    };

    ConfirmModal.prototype.clear = function (cfg) {
        this.$.find('[data-modal-confirm]').off('click');
        this.$.find('[data-modal-cancel]').off('click');
    };

    ConfirmModal.prototype.initButtons = function (cfg) {
        //Set button text
        var $cancelButton = this.$.find('[data-modal-cancel]');
        $cancelButton.text(cfg['cancelText']);

        var $confirmButton = this.$.find('[data-modal-confirm]');
        $confirmButton.text(cfg['confirmText']);

        //Init handler
        var that = this;
        if (cfg['handler']) {
            $confirmButton.one('click', function () {
                that.clear();
                cfg['handler'](true);
            });

            var $closeButtonsIcons = this.$.find('[data-modal-close]');
            $closeButtonsIcons.one('click', function () {
                that.clear();
                cfg['handler'](false);
            });
        }
    };

    var init = function () {
        module.global = Modal.instance('#globalModal');
        module.global.$.on('hidden.bs.modal', function (e) {
            module.global.reset();
        });

        module.globalConfirm = ConfirmModal.instance('#globalModalConfirm');

        _setModalEnforceFocus();
        _setGlobalModalTargetHandler();

        $(document).on('show.bs.modal', '.modal', function (event) {
            $(this).appendTo($('body'));
            $(this).attr('aria-hidden', 'false');
        });

        $(document).on('shown.bs.modal', '.modal.show', function (event) {
            _setModalsAndBackdropsOrder();
        });

        $(document).on('hide.bs.modal', '.modal', function (event) {
            $(this).addClass('fade');
        });

        $(document).on('hidden.bs.modal', '.modal', function (event) {
            _setModalsAndBackdropsOrder();
            $(this).attr('aria-hidden', 'true');
        });
    };

    var _setModalsAndBackdropsOrder = function () {
        var modalZIndex = 1040;
        $('.modal.show').each(function (index) {
            $(this).css('zIndex', ++modalZIndex);
        });
        $('.modal-backdrop.show').css('zIndex', modalZIndex - 1);
        $('.modal.show:visible:last').focus();
    };

    /**
     * To allow other frameworks to overlay focusable nodes over an active modal we have
     * to explicitly allow it within this overwritten function.
     *
     */
    var _setModalEnforceFocus = function () {
        $.fn.modal.Constructor.prototype.enforceFocus = function () {
            var that = this;
            $(document).on('focusin.modal', function (e) {
                var $target = $(e.target);
                if ($target.hasClass('select2-input') || $target.hasClass('select2-search__field') || $target.hasClass('hexInput')) {
                    return true;
                }

                var $parent = $(e.target.parentNode);
                if ($parent.hasClass('cke_dialog_ui_input_select') || $parent.hasClass('cke_dialog_ui_input_text')) {
                    return true;
                }

                if ($target.closest('.ProseMirror-prompt').length) {
                    return true;
                }

                // Allow stacking of modals
                if ($target.closest('.modal.in').length) {
                    return true;
                }

                if (that.$element[0] !== e.target && !that.$element.has(e.target).length) {
                    that.$element.focus();
                }
            });
        };
    };

    var _setGlobalModalTargetHandler = function () {
        // unbind all previously-attached events
        $("a[data-bs-target='#globalModal']").off('.humhub:globalModal');

        // deprecated use action handler instead @see get action
        $(document).off('click.humhub:globalModal').on('click.humhub:globalModal', "a[data-bs-target='#globalModal']", function (evt) {
            evt.preventDefault();

            var options = {
                'show': true,
                'backdrop': $(this).data('bs-backdrop'),
                'keyboard': $(this).data('bs-keyboard')
            };

            var globalModalElement = document.getElementById('globalModal');
            var globalModal = bootstrap.Modal.getInstance(globalModalElement) || new bootstrap.Modal(globalModalElement, options);

            var target = $(this).attr("href");

            client.html(target).then(function (response) {
                module.global.setDialog(response);
                if (!module.global.$.is(':visible')) {
                    globalModal.show();
                }
            }).catch(function (error) {
                module.log.error(error, true);
                globalModal.hide();
            });
        });
    };

    var submit = function (evt, options) {
        evt.$form = evt.$form || evt.$trigger.closest('form');

        if (!evt.$form.length) {
            evt.$form = evt.$target;
        }

        var id = evt.$trigger.data('modal-id');
        if (!id) {
            // try to autodetect modal id if we're currently in a modal
            var $parent = evt.$trigger.closest('.modal');
            if ($parent.length) {
                id = $parent.attr('id');
            }
        }

        var modal = (id) ? module.get(id) : module.global;
        return client.submit(evt, setDefaultRequestData(options)).then(function (response) {
            if (response.success) {
                modal.close();
            } else {
                modal.setDialog(response);
                if (!modal.$.is(':visible')) {
                    modal.show();
                }
            }

            modal.$.trigger('submitted', response);
            return response;
        }).catch(function (error) {
            module.log.error(error, true);
            modal.close();
        });
    };

    var load = function (evt, options) {
        var id = evt.$trigger.data('modal-id');
        if (!id) {
            // try to autodetect modal id if we're currently in a modal
            var $parent = evt.$trigger.closest('.modal');
            if ($parent.length) {
                id = $parent.attr('id');
            }
        }

        var modal = (id) ? module.get(id) : module.global;
        return modal.load(evt, setDefaultRequestData(options))
            .catch(function (err) {
                module.log.error(err, true);
                modal.close();
            });
    };

    var unload = function () {
        $('.modal').each(function () {
            var modal = Modal.instance(this);
            if (modal && typeof modal.close === 'function' && !$(this).find('.exclude-from-pjax-client').length) {
                modal.close();
            }
        });
    }

    var post = function (evt, options) {
        var id = evt.$trigger.data('modal-id');
        if (!id) {
            // try to autodetect modal id if we're currently in a modal
            var $parent = evt.$trigger.closest('.modal');
            if ($parent.length) {
                id = $parent.attr('id');
            }
        }

        var modal = (id) ? module.get(id) : module.global;
        return modal.post(evt, setDefaultRequestData(options)).catch(function (err) {
            module.log.error(err, true);
            modal.close();
        });
    };

    var show = function (evt) {
        var modal = get(evt.$target);
        if (modal) {
            modal.show();
        }
    };

    var get = function (id, options) {
        var modal = !(modals[id]) ? new Modal(id) : modals[id];
        if (options) {
            modal.set(options);
        }
        return modal;
    };

    var confirm = function (evt) {
        if (!(evt instanceof $.Event || evt instanceof $)) { // Simple config given
            return module.globalConfirm.open(evt);
        }

        var confirmOptions = (evt instanceof $.Event) ? _getConfirmOptionsByTrigger(evt.$trigger) : _getConfirmOptionsByTrigger(evt);

        return module.confirm(confirmOptions);
    };

    var _getConfirmOptionsByTrigger = function ($trigger) {
        return {
            'body': $trigger.data('action-confirm'),
            'header': $trigger.data('action-confirm-header') || $trigger.data('action-confirm-title'),
            'confirmText': $trigger.data('action-confirm-text'),
            'cancelText': $trigger.data('action-cancel-text')
        };
    };

    const footerLoader = function (evt) {
        module.global.footerLoader(evt);
    }

    const setContent = function (html) {
        module.global.setContent(html);
    }

    module.export({
        init: init,
        sortOrder: 100,
        confirm: confirm,
        Modal: Modal,
        ConfirmModal: ConfirmModal,
        get: get,
        post: post,
        load: load,
        unload: unload,
        show: show,
        submit: submit,
        footerLoader,
        setContent
    });
});
