/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */

/**
 * Icinga.Loader
 *
 * This is where we take care of XHR requests, responses and failures.
 */
(function(Icinga, $) {

    'use strict';

    Icinga.Loader = function (icinga) {

        /**
         * YES, we need Icinga
         */
        this.icinga = icinga;

        /**
         * Our base url
         */
        this.baseUrl = icinga.config.baseUrl;

        this.failureNotice = null;

        /**
         * Pending requests
         */
        this.requests = {};

        this.iconCache = {};

        this.autorefreshEnabled = true;
    };

    Icinga.Loader.prototype = {

        initialize: function () {
            this.icinga.timer.register(this.autorefresh, this, 500);
        },

        /**
         * Load the given URL to the given target
         *
         * @param {string}  url             URL to be loaded
         * @param {object}  target          Target jQuery element
         * @param {object}  data            Optional parameters, usually for POST requests
         * @param {string}  method          HTTP method, default is 'GET'
         * @param {string}  action          How to handle the response ('replace' or 'append'), default is 'replace'
         * @param {boolean} autorefresh     Whether the cause is a autorefresh or not
         * @param {object}  progressTimer   A timer to be stopped when the request is done
         */
        loadUrl: function (url, $target, data, method, action, autorefresh, progressTimer) {
            var id = null;

            // Default method is GET
            if ('undefined' === typeof method) {
                method = 'GET';
            }
            if ('undefined' === typeof action) {
                action = 'replace';
            }
            if ('undefined' === typeof autorefresh) {
                autorefresh = false;
            }

            this.icinga.logger.debug('Loading ', url, ' to ', $target);

            // We should do better and ignore requests without target and/or id
            if (typeof $target !== 'undefined' && $target.attr('id')) {
                id = $target.attr('id');
            }

            // If we have a pending request for the same target...
            if (typeof this.requests[id] !== 'undefined') {
                if (autorefresh) {
                    return false;
                }
                // ... ignore the new request if it is already pending with the same URL. Only abort GETs, as those
                // are the only methods that are guaranteed to return the same value
                if (this.requests[id].url === url && method === 'GET') {
                    this.icinga.logger.debug('Request to ', url, ' is already running for ', $target);
                    return this.requests[id];
                }

                // ...or abort the former request otherwise
                this.icinga.logger.debug(
                    'Aborting pending request loading ',
                    url,
                    ' to ',
                    $target
                );

                this.requests[id].abort();
            }

            // Not sure whether we need this Accept-header
            var headers = { 'X-Icinga-Accept': 'text/html' };

            // Ask for a new window id in case we don't already have one
            if (this.icinga.ui.hasWindowId()) {
                headers['X-Icinga-WindowId'] = this.icinga.ui.getWindowId();
            } else {
                headers['X-Icinga-WindowId'] = 'undefined';
            }

            // This is jQuery's default content type
            var contentType = 'application/x-www-form-urlencoded; charset=UTF-8';

            var isFormData = typeof window.FormData !== 'undefined' && data instanceof window.FormData;
            if (isFormData) {
                // Setting false is mandatory as the form's data
                // won't be recognized by the server otherwise
                contentType = false;
            }

            var self = this;
            var req = $.ajax({
                type   : method,
                url    : url,
                data   : data,
                headers: headers,
                context: self,
                contentType: contentType,
                processData: ! isFormData
            });

            req.$target = $target;
            req.url = url;
            req.done(this.onResponse);
            req.fail(this.onFailure);
            req.complete(this.onComplete);
            req.autorefresh = autorefresh;
            req.action = action;
            req.addToHistory = true;
            req.progressTimer = progressTimer;

            if (id) {
                this.requests[id] = req;
            }
            if (! autorefresh) {
                req.$target.addClass('impact');
            }
            this.icinga.ui.refreshDebug();
            return req;
        },

        /**
         * Mimic XHR form submission by using an iframe
         *
         * @param {object} $form    The form being submitted
         * @param {string} action   The form's action URL
         * @param {object} $target  The target container
         */
        submitFormToIframe: function ($form, action, $target) {
            var self = this;

            $form.prop('action', self.icinga.utils.addUrlParams(action, {
                '_frameUpload': true
            }));
            $form.prop('target', 'fileupload-frame-target');
            $('#fileupload-frame-target').on('load', function (event) {
                var $frame = $(event.target);
                var $contents = $frame.contents();

                var $redirectMeta = $contents.find('meta[name="redirectUrl"]');
                if ($redirectMeta.length) {
                    self.redirectToUrl($redirectMeta.attr('content'), $target);
                } else {
                    // Fetch the frame's new content and paste it into the target
                    self.renderContentToContainer(
                        $contents.find('body').html(),
                        $target,
                        'replace'
                    );
                }

                $frame.prop('src', 'about:blank'); // Clear the frame's dom
                $frame.off('load'); // Unbind the event as it's set on demand
            });
        },

        /**
         * Create an URL relative to the Icinga base Url, still unused
         *
         * @param {string} url Relative url
         */
        url: function (url) {
            if (typeof url === 'undefined') {
                return this.baseUrl;
            }
            return this.baseUrl + url;
        },

        stopPendingRequestsFor: function ($el) {
            var id;
            if (typeof $el === 'undefined' || ! (id = $el.attr('id'))) {
                return;
            }

            if (typeof this.requests[id] !== 'undefined') {
                this.requests[id].abort();
            }
        },

        filterAutorefreshingContainers: function () {
            return $(this).data('icingaRefresh') > 0;
        },

        autorefresh: function () {
            var self = this;
            if (self.autorefreshEnabled !== true) {
                return;
            }

            $('.container').filter(this.filterAutorefreshingContainers).each(function (idx, el) {
                var $el = $(el);
                var id = $el.attr('id');
                if (typeof self.requests[id] !== 'undefined') {
                    self.icinga.logger.debug('No refresh, request pending for ', id);
                    return;
                }

                var interval = $el.data('icingaRefresh');
                var lastUpdate = $el.data('lastUpdate');

                if (typeof interval === 'undefined' || ! interval) {
                    self.icinga.logger.info('No interval, setting default', id);
                    interval = 10;
                }

                if (typeof lastUpdate === 'undefined' || ! lastUpdate) {
                    self.icinga.logger.info('No lastUpdate, setting one', id);
                    $el.data('lastUpdate',(new Date()).getTime());
                    return;
                }
                interval = interval * 1000;

                // TODO:
                if ((lastUpdate + interval) > (new Date()).getTime()) {
                    // self.icinga.logger.info(
                    //     'Skipping refresh',
                    //     id,
                    //     lastUpdate,
                    //     interval,
                    //     (new Date()).getTime()
                    // );
                    return;
                }

                if (self.loadUrl($el.data('icingaUrl'), $el, undefined, undefined, undefined, true) === false) {
                    self.icinga.logger.debug(
                        'NOT autorefreshing ' + id + ', even if ' + interval + ' ms passed. Request pending?'
                    );
                } else {
                    self.icinga.logger.debug(
                        'Autorefreshing ' + id + ' ' + interval + ' ms passed'
                    );
                }
                el = null;
            });
        },

        /**
         * Disable the autorefresh mechanism
         */
        disableAutorefresh: function () {
            this.autorefreshEnabled = false;
        },

        /**
         * Enable the autorefresh mechanism
         */
        enableAutorefresh: function () {
            this.autorefreshEnabled = true;
        },

        processNotificationHeader: function(req) {
            var header = req.getResponseHeader('X-Icinga-Notification');
            var self = this;
            if (! header) return false;
            var list = header.split('&');
            $.each(list, function(idx, el) {
                var parts = decodeURIComponent(el).split(' ');
                self.createNotice(parts.shift(), parts.join(' '));
            });
            return true;
        },

        addUrlFlag: function(url, flag)
        {
            if (url.match(/\?/)) {
                return url + '&' + flag;
            } else {
                return url + '?' + flag;
            }
        },

        /**
         * Process the X-Icinga-Redirect HTTP Response Header
         *
         * If the response includes the X-Icinga-Redirect header, redirects to the URL associated with the header.
         *
         * @param   {object}    req     Current request
         *
         * @returns {boolean}           Whether we're about to redirect
         */
        processRedirectHeader: function(req) {
            var icinga      = this.icinga,
                redirect    = req.getResponseHeader('X-Icinga-Redirect');

            if (! redirect) {
                return false;
            }

            redirect = decodeURIComponent(redirect);
            if (redirect.match(/__SELF__/)) {
                if (req.autorefresh) {
                    // Redirect to the current window's URL in case it's an auto-refresh request. If authenticated
                    // externally this ensures seamless re-login if the session's expired
                    redirect = redirect.replace(
                        /__SELF__/,
                        encodeURIComponent(
                            document.location.pathname + document.location.search + document.location.hash
                        )
                    );
                } else {
                    // Redirect to the URL which required authentication. When clicking a link this ensures that we
                    // redirect to the link's URL instead of the current window's URL (see above)
                    redirect = redirect.replace(/__SELF__/, req.url);
                }
            }

            this.redirectToUrl(redirect, req.$target, req.url, req.getResponseHeader('X-Icinga-Rerender-Layout'));
            return true;
        },

        /**
         * Redirect to the given url
         *
         * @param {string}  url
         * @param {object}  $target
         * @param {string]  origin
         * @param {boolean} rerenderLayout
         */
        redirectToUrl: function (url, $target, origin, rerenderLayout) {
            var icinga = this.icinga;

            if (typeof rerenderLayout === 'undefined') {
                rerenderLayout = false;
            }

            icinga.logger.debug(
                'Got redirect for ', $target, ', URL was ' + url
            );

            if (rerenderLayout) {
                var parts = url.split(/#!/);
                url = parts.shift();
                var redirectionUrl = this.addUrlFlag(url, 'renderLayout');
                var r = this.loadUrl(redirectionUrl, $('#layout'));
                r.historyUrl = url;
                if (parts.length) {
                    r.loadNext = parts;
                } else if (!! document.location.hash) {
                    // Retain detail URL if the layout is rerendered
                    parts = document.location.hash.split('#!').splice(1);
                    if (parts.length) {
                        r.loadNext = $.grep(parts, function (url) {
                            if (url !== origin) {
                                icinga.logger.debug('Retaining detail url ' + url);
                                return true;
                            }

                            icinga.logger.debug('Discarding detail url ' + url + ' as it\'s the origin of the redirect');
                            return false;
                        });
                    }
                }
            } else {
                if (url.match(/#!/)) {
                    var parts = url.split(/#!/);
                    icinga.ui.layout2col();
                    this.loadUrl(parts.shift(), $('#col1'));
                    this.loadUrl(parts.shift(), $('#col2'));
                } else {
                    if ($target.attr('id') === 'col2') { // TODO: multicol
                        if ($('#col1').data('icingaUrl').split('?')[0] === url.split('?')[0]) {
                            icinga.ui.layout1col();
                            $target = $('#col1');
                            delete(this.requests['col2']);
                        }
                    }

                    this.loadUrl(url, $target);
                }
            }
        },

        cacheLoadedIcons: function($container) {
            // TODO: this is just a prototype, disabled for now
            return;

            var self = this;
            $('img.icon', $container).each(function(idx, img) {
                var src = $(img).attr('src');
                if (typeof self.iconCache[src] !== 'undefined') {
                    return;
                }
                var cache = new Image();
                cache.src = src
                self.iconCache[src] = cache;
            });
        },

        /**
         * Handle successful XHR response
         */
        onResponse: function (data, textStatus, req) {
            var self = this;
            if (this.failureNotice !== null) {
                if (! this.failureNotice.hasClass('fading-out')) {
                    this.failureNotice.remove();
                }
                this.failureNotice = null;
            }

            var url = req.url;
            this.icinga.logger.debug(
                'Got response for ', req.$target, ', URL was ' + url
            );
            this.processNotificationHeader(req);

            var cssreload = req.getResponseHeader('X-Icinga-Reload-Css');
            if (cssreload) {
                this.icinga.ui.reloadCss();
            }

            if (req.getResponseHeader('X-Icinga-Redirect')) {
                return;
            }

            // div helps getting an XML tree
            var $resp = $('<div>' + req.responseText + '</div>');
            var active = false;
            var rendered = false;
            var classes;

            if (req.autorefresh) {
                // TODO: next container url
                active = $('[href].active', req.$target).attr('href');
            }

            var target = req.getResponseHeader('X-Icinga-Container');
            var newBody = false;
            var oldNotifications = false;
            if (target) {
                if (target === 'ignore') {
                    return;
                }
                // If we change the target, oncomplete will fail to clean up
                // This fixes the problem, not using req.$target would be better
                delete this.requests[req.$target.attr('id')];

                req.$target = $('#' + target);
                if (target === 'layout') {
                    oldNotifications = $('#notifications li').detach();
                }
                // We assume target === 'layout' right now. This might not be correct
                this.icinga.ui.layout1col();
                newBody = true;
            }

            var moduleName = req.getResponseHeader('X-Icinga-Module');
            classes = $.grep(req.$target.classes(), function (el) {
               if (el === 'icinga-module' || el.match(/^module\-/)) {
                   return false;
               }
               return true;
            });
            if (moduleName) {
                req.$target.data('icingaModule', moduleName);
                classes.push('icinga-module');
                classes.push('module-' + moduleName);
            } else {
                req.$target.removeData('icingaModule');
                if (req.$target.attr('data-icinga-module')) {
                    req.$target.removeAttr('data-icinga-module');
                }
            }
            req.$target.attr('class', classes.join(' '));

            var title = req.getResponseHeader('X-Icinga-Title');
            if (title && ! req.autorefresh && req.$target.closest('.dashboard').length === 0) {
                this.icinga.ui.setTitle(decodeURIComponent(title));
            }

            var refresh = req.getResponseHeader('X-Icinga-Refresh');
            if (refresh) {
                req.$target.data('icingaRefresh', refresh);
            } else {
                req.$target.removeData('icingaRefresh');
                if (req.$target.attr('data-icinga-refresh')) {
                    req.$target.removeAttr('data-icinga-refresh');
                }
            }

            // Set a window identifier if the server asks us to do so
            var windowId = req.getResponseHeader('X-Icinga-WindowId');
            if (windowId) {
                this.icinga.ui.setWindowId(windowId);
            }

            // Handle search requests, still hardcoded.
            if (req.url.match(/^\/search/) &&
                req.$target.data('icingaUrl').match(/^\/search/) &&
                $('.dashboard', $resp).length > 0 &&
                $('.dashboard .container', req.$target).length > 0)
            {
                // TODO: We need dashboard pane and container identifiers (not ids)
                var targets = [];
                $('.dashboard .container', req.$target).each(function (idx, el) {
                    targets.push($(el));
                });

                var i = 0;
                // Searching for '.dashboard .container' in $resp doesn't dork?!
                $('.dashboard .container', $resp).each(function (idx, el) {
                    var $el = $(el);
                    if ($el.hasClass('dashboard')) {
                        return;
                    }
                    var url = $el.data('icingaUrl');
                    targets[i].data('icingaUrl', url);
                    var title = $('h1', $el).first();
                    $('h1', targets[i]).first().replaceWith(title);

                    self.loadUrl(url, targets[i]);
                    i++;
                });
                rendered = true;
            }

            req.$target.data('icingaUrl', req.url);

            this.icinga.ui.initializeTriStates($resp);

            if (rendered) {
                return;
            }

            if (typeof req.progressTimer !== 'undefined') {
                this.icinga.timer.unregister(req.progressTimer);
            }

            // .html() removes outer div we added above
            this.renderContentToContainer($resp.html(), req.$target, req.action, req.autorefresh);
            if (oldNotifications) {
                oldNotifications.appendTo($('#notifications'));
            }
            if (url.match(/#/)) {
                this.icinga.ui.focusElement(url.split(/#/)[1], req.$target);
            }
            if (newBody) {
                this.icinga.ui.fixDebugVisibility().triggerWindowResize();
            }
            self.cacheLoadedIcons(req.$target);
        },

        /**
         * Regardless of whether a request succeeded of failed, clean up
         */
        onComplete: function (req, textStatus) {
            // Remove 'impact' class if there was such
            if (req.$target.hasClass('impact')) {
                req.$target.removeClass('impact');
            }

            if (! req.autorefresh) {
                // TODO: Hook for response/url?
                var url = req.url;

                if (req.$target[0].id === 'col1') {
                    self.icinga.behaviors.navigation.trySetActiveByUrl(url);
                }

                var $forms = $('[action="' + this.icinga.utils.parseUrl(url).path + '"]');
                var $matches = $.merge($('[href="' + url + '"]'), $forms);
                $matches.each(function (idx, el) {
                    var $el = $(el);
                    if ($el.closest('#menu').length) {
                        if ($el.is('form')) {
                            $('input', $el).addClass('active');
                        }
                        // Interrupt .each, only one menu item shall be active
                        return false;
                    }
                });
            }

            // Update history when necessary
            if (! req.autorefresh && req.addToHistory) {
                if (req.$target.hasClass('container')) {
                    // We only want to care about top-level containers
                    if (req.$target.parent().closest('.container').length === 0) {
                        this.icinga.history.pushCurrentState();
                    }
                } else {
                    // Request wasn't for a container, so it's usually the body
                    // or the full layout. Push request URL to history:
                    var url = typeof req.historyUrl !== 'undefined' ? req.historyUrl : req.url;
                    this.icinga.history.pushUrl(url);
                }
            }

            req.$target.data('lastUpdate', (new Date()).getTime());
            delete this.requests[req.$target.attr('id')];
            this.icinga.ui.fadeNotificationsAway();

            this.processRedirectHeader(req);

            if (typeof req.loadNext !== 'undefined' && req.loadNext.length) {
                if ($('#col2').length) {
                    var r = this.loadUrl(req.loadNext[0], $('#col2'));
                    r.addToHistory = req.addToHistory;
                    this.icinga.ui.layout2col();
                } else {
                  this.icinga.logger.error('Failed to load URL for #col2', req.loadNext);
                }
            }

            req.$target.trigger('rendered');

            this.icinga.ui.refreshDebug();
        },

        /**
         * Handle failed XHR response
         */
        onFailure: function (req, textStatus, errorThrown) {
            var url = req.url;

            /*
             * Test if a manual actions comes in and autorefresh is active: Stop refreshing
             */
            if (req.addToHistory && ! req.autorefresh) {
                req.$target.data('icingaRefresh', 0);
                req.$target.data('icingaUrl', url);
                icinga.history.pushCurrentState();
            }

            if (typeof req.progressTimer !== 'undefined') {
                this.icinga.timer.unregister(req.progressTimer);
            }

            if (req.status > 0) {
                this.icinga.logger.error(
                    req.status,
                    errorThrown + ':',
                    $(req.responseText).text().replace(/\s+/g, ' ').slice(0, 100)
                );
                this.renderContentToContainer(
                    req.responseText,
                    req.$target,
                    req.action,
                    req.autorefresh
                );
            } else {
                if (errorThrown === 'abort') {
                    this.icinga.logger.debug(
                        'Request to ' + url + ' has been aborted for ',
                        req.$target
                    );

                    // Aborted requests should not be added to browser history
                    req.addToHistory = false;
                } else {
                    if (this.failureNotice === null) {
                        this.failureNotice = this.createNotice(
                            'error',
                            'The connection to the Icinga web server was lost at ' +
                            this.icinga.utils.timeShort() +
                            '.',
                            true
                        );

                        this.icinga.ui.fixControls();
                    }

                    this.icinga.logger.error(
                        'Failed to contact web server loading ',
                        url,
                        ' for ',
                        req.$target
                    );
                }
            }
        },

        /**
         * Create a notification. Can be improved.
         */
        createNotice: function (severity, message, persist) {
            var c = severity;
            if (persist) {
                c += ' persist';
            }
            var $notice = $(
                '<li class="' + c + '">' + message + '</li>'
            ).appendTo($('#notifications'));

            this.icinga.ui.fixControls();

            if (!persist) {
                this.icinga.ui.fadeNotificationsAway();
            }

            return $notice;
        },

        /**
         * Smoothly render given HTML to given container
         */
        renderContentToContainer: function (content, $container, action, autorefresh) {
            // Container update happens here
            var scrollPos = false;
            var self = this;
            var containerId = $container.attr('id');
            if (typeof containerId !== 'undefined') {
                if (autorefresh) {
                    scrollPos = $container.scrollTop();
                } else {
                    scrollPos = 0;
                }
            }
            if (autorefresh && $.contains($container[0], document.activeElement)) {
                var origFocus = self.icinga.utils.getDomPath(document.activeElement);
            }

            $container.trigger('beforerender');

            var discard = false;
            $.each(self.icinga.behaviors, function(name, behavior) {
                if (behavior.renderHook) {
                    var changed = behavior.renderHook(content, $container, action, autorefresh);
                    if (!changed) {
                        discard = true;
                    } else {
                        content = changed;
                    }
                }
            });
            if (discard) {
                return;
            }

            // TODO: We do not want to wrap this twice...
            var $content = $('<div>' + content + '</div>');

            // Disable all click events while rendering
            // (Disabling disabled, was ways too slow)
            // $('*').click(function (event) {
            //     event.stopImmediatePropagation();
            //     event.stopPropagation();
            //     event.preventDefault();
            // });

            $('.container', $container).each(function() {
                self.stopPendingRequestsFor($(this));
            });

            if (false &&
                $('.dashboard', $content).length > 0 &&
                $('.dashboard', $container).length === 0
            ) {
                // $('.dashboard', $content)
                // $container.html(content);

            } else {
                if ($container.closest('.dashboard').length) {
                    var title = $('h1', $container).first().detach();
                    $container.html(title).append(content);
                } else if (action === 'replace') {
                    $container.html(content);
                } else {
                    $container.append(content);
                }
            }
            this.icinga.ui.assignUniqueContainerIds();

            if (origFocus && origFocus.length > 0 && origFocus[0] !== '') {
                setTimeout(function() {
                    $(self.icinga.utils.getElementByDomPath(origFocus)).focus();
                }, 0);
            }

            if (scrollPos !== false) {
                $container.scrollTop(scrollPos);
            }
            var icinga = this.icinga;
            //icinga.events.applyHandlers($container);
            icinga.ui.initializeControls($container);
            icinga.ui.fixControls();

            // Re-enable all click events (disabled as of performance reasons)
            // $('*').off('click');
        },

        /**
         * On shutdown we kill all pending requests
         */
        destroy: function() {
            $.each(this.requests, function(id, request) {
                request.abort();
            });
            this.icinga = null;
            this.requests = {};
        }

    };

}(Icinga, jQuery));
