/**
 * Provides useful utilities for views.
 */
;(function (define) {
    'use strict';
    define(["jquery", "underscore", "gettext", "common/js/components/views/feedback_notification",
        "common/js/components/views/feedback_prompt"],
    function ($, _, gettext, NotificationView, PromptView) {
        var toggleExpandCollapse, showLoadingIndicator, hideLoadingIndicator, confirmThenRunOperation,
            runOperationShowingMessage, disableElementWhileRunning, getScrollOffset, setScrollOffset,
            setScrollTop, redirect, reload, hasChangedAttributes, deleteNotificationHandler,
            validateRequiredField, validateURLItemEncoding, validateTotalKeyLength, checkTotalKeyLengthViolations;

        // see https://openedx.atlassian.net/browse/TNL-889 for what is it and why it's 65
        var MAX_SUM_KEY_LENGTH = 65;

        /**
         * Toggles the expanded state of the current element.
         */
        toggleExpandCollapse = function(target, collapsedClass) {
            // Support the old 'collapsed' option until fully switched over to is-collapsed
            if (!collapsedClass) {
                collapsedClass = 'collapsed';
            }
            target.closest('.expand-collapse').toggleClass('expand collapse');
            target.closest('.is-collapsible, .window').toggleClass(collapsedClass);
            target.closest('.is-collapsible').children('article').slideToggle();
        };

        /**
         * Show the page's loading indicator.
         */
        showLoadingIndicator = function() {
            $('.ui-loading').show();
        };

        /**
         * Hide the page's loading indicator.
         */
        hideLoadingIndicator = function() {
            $('.ui-loading').hide();
        };

        /**
         * Confirms with the user whether to run an operation or not, and then runs it if desired.
         */
        confirmThenRunOperation = function(title, message, actionLabel, operation, onCancelCallback) {
            return new PromptView.Warning({
                title: title,
                message: message,
                actions: {
                    primary: {
                        text: actionLabel,
                        click: function(prompt) {
                            prompt.hide();
                            operation();
                        }
                    },
                    secondary: {
                        text: gettext('Cancel'),
                        click: function(prompt) {
                            if (onCancelCallback) {
                                onCancelCallback();
                            }
                            return prompt.hide();
                        }
                    }
                }
            }).show();
        };

        /**
         * Shows a progress message for the duration of an asynchronous operation.
         * Note: this does not remove the notification upon failure because an error
         * will be shown that shouldn't be removed.
         * @param message The message to show.
         * @param operation A function that returns a promise representing the operation.
         */
        runOperationShowingMessage = function(message, operation) {
            var notificationView;
            notificationView = new NotificationView.Mini({
                title: gettext(message)
            });
            notificationView.show();
            return operation().done(function() {
                notificationView.hide();
            });
        };

        /**
         * Disables a given element when a given operation is running.
         * @param {jQuery} element the element to be disabled.
         * @param operation the operation during whose duration the
         * element should be disabled. The operation should return
         * a JQuery promise.
         */
        disableElementWhileRunning = function(element, operation) {
            element.addClass("is-disabled").attr('aria-disabled', true);
            return operation().always(function() {
                element.removeClass("is-disabled").attr('aria-disabled', false);
            });
        };

        /**
         * Returns a handler that removes a notification, both dismissing it and deleting it from the database.
         * @param callback function to call when deletion succeeds
         */
        deleteNotificationHandler = function(callback) {
            return function (event) {
                event.preventDefault();
                $.ajax({
                    url: $(this).data('dismiss-link'),
                    type: 'DELETE',
                    success: callback
                });
            };
        };

        /**
         * Performs an animated scroll so that the window has the specified scroll top.
         * @param scrollTop The desired scroll top for the window.
         */
        setScrollTop = function(scrollTop) {
            $('html, body').animate({
                scrollTop: scrollTop
            }, 500);
        };

        /**
         * Returns the relative position that the element is scrolled from the top of the view port.
         * @param element The element in question.
         */
        getScrollOffset = function(element) {
            var elementTop = element.offset().top;
            return elementTop - $(window).scrollTop();
        };

        /**
         * Scrolls the window so that the element is scrolled down to the specified relative position
         * from the top of the view port.
         * @param element The element in question.
         * @param offset The amount by which the element should be scrolled from the top of the view port.
         */
        setScrollOffset = function(element, offset) {
            var elementTop = element.offset().top,
                newScrollTop = elementTop - offset;
            setScrollTop(newScrollTop);
        };

        /**
         * Redirects to the specified URL. This is broken out as its own function for unit testing.
         */
        redirect = function(url) {
            window.location = url;
        };

        /**
         * Reloads the page. This is broken out as its own function for unit testing.
         */
        reload = function() {
            window.location.reload();
        };

        /**
         * Returns true if a model has changes to at least one of the specified attributes.
         * @param model The model in question.
         * @param attributes The list of attributes to be compared.
         * @returns {boolean} Returns true if attribute changes are found.
         */
        hasChangedAttributes = function(model, attributes) {
            var i, changedAttributes = model.changedAttributes();
            if (!changedAttributes) {
                return false;
            }
            for (i=0; i < attributes.length; i++) {
                if (_.has(changedAttributes, attributes[i])) {
                    return true;
                }
            }
            return false;
        };

        /**
         * Helper method for course/library creation - verifies a required field is not blank.
         */
        validateRequiredField = function (msg) {
            return msg.length === 0 ? gettext('Required field.') : '';
        };

        /**
         * Helper method for course/library creation.
         * Check that a course (org, number, run) doesn't use any special characters
         */
        validateURLItemEncoding = function (item, allowUnicode) {
            var required = validateRequiredField(item);
            if (required) {
                return required;
            }
            if (allowUnicode) {
                if (/\s/g.test(item)) {
                    return gettext('Please do not use any spaces in this field.');
                }
            }
            else {
                if (item !== encodeURIComponent(item) || item.match(/[!'()*]/)) {
                    return gettext('Please do not use any spaces or special characters in this field.');
                }
            }
            return '';
        };

        // Ensure that sum length of key field values <= ${MAX_SUM_KEY_LENGTH} chars.
        validateTotalKeyLength = function (key_field_selectors) {
            var totalLength = _.reduce(
                key_field_selectors,
                function (sum, ele) { return sum + $(ele).val().length;},
                0
            );
            return totalLength <= MAX_SUM_KEY_LENGTH;
        };

        checkTotalKeyLengthViolations = function(selectors, classes, key_field_selectors, message_tpl) {
            if (!validateTotalKeyLength(key_field_selectors)) {
                $(selectors.errorWrapper).addClass(classes.shown).removeClass(classes.hiding);
                $(selectors.errorMessage).html('<p>' + _.template(message_tpl, {limit: MAX_SUM_KEY_LENGTH}) + '</p>');
                $(selectors.save).addClass(classes.disabled);
            } else {
                $(selectors.errorWrapper).removeClass(classes.shown).addClass(classes.hiding);
            }
        };

        return {
            'toggleExpandCollapse': toggleExpandCollapse,
            'showLoadingIndicator': showLoadingIndicator,
            'hideLoadingIndicator': hideLoadingIndicator,
            'confirmThenRunOperation': confirmThenRunOperation,
            'runOperationShowingMessage': runOperationShowingMessage,
            'disableElementWhileRunning': disableElementWhileRunning,
            'deleteNotificationHandler': deleteNotificationHandler,
            'setScrollTop': setScrollTop,
            'getScrollOffset': getScrollOffset,
            'setScrollOffset': setScrollOffset,
            'redirect': redirect,
            'reload': reload,
            'hasChangedAttributes': hasChangedAttributes,
            'validateRequiredField': validateRequiredField,
            'validateURLItemEncoding': validateURLItemEncoding,
            'validateTotalKeyLength': validateTotalKeyLength,
            'checkTotalKeyLengthViolations': checkTotalKeyLengthViolations
        };
    });
}).call(this, define || RequireJS.define);
