/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
*/
define([
        "underscore", "jquery", "brooklyn"
], function (_, $, BrooklynConfig) {

    var ViewUtils = {
        myDataTable:function($table, extra) {
            $.fn.dataTableExt.sErrMode = 'throw';
            var settings = {
                "bDestroy": true,
                "iDisplayLength": 25,
                "bDeferRender": true,
                "sPaginationType": "full_numbers",
                "sDom": "fp<'brook-db-top-toolbar'>tilp<'brook-db-bot-toolbar'>",
                "oLanguage": {
                    "sSearch": "",
                    "sInfo": "Showing _START_ - _END_ of _TOTAL_ ",
                    "sInfoEmpty": "<i>No data</i> ",
                    "sEmptyTable": "<i>No matching records available</i>",
                    "sZeroRecords": "<i>No matching records found</i>",
                    "oPaginate": {
                        "sFirst": "&lt;&lt;",
                        "sPrevious": "&lt;",
                        "sNext": "&gt;",
                        "sLast": "&gt;&gt;"
                    },
                    "sInfoFiltered": "(of _MAX_)",
                    "sLengthMenu": '( <select>' +
                                        '<option value="10">10</option>' +
                                        '<option value="25">25</option>' +
                                        '<option value="50">50</option>' +
                                        '<option value="-1">all</option>' +
                                    '</select> / page )'
                }
            };
            _.extend(settings, extra);
            
            ViewUtils.fadeToIndicateInitialLoad($table);
 
            return $table.dataTable(settings);
        },
        myDataTableToolbarAddHtml: function($table,html) {
            $('.brook-db-bot-toolbar', $table.parent().parent()).append(html)
            $('.brook-db-top-toolbar', $table.parent().parent()).append(html)
        },
        addRefreshButton: function($table) {
            this.myDataTableToolbarAddHtml($table,
                '<i class="refresh table-toolbar-icon bootstrap-glyph icon-refresh handy smallpadside" rel="tooltip" title="Reload content immediately"></i>');
        },
        addFilterEmptyButton: function($table) {
            this.myDataTableToolbarAddHtml($table,
                '<i class="filterEmpty table-toolbar-icon bootstrap-glyph icon-eye-open handy bottom smallpadside" rel="tooltip" title="Show/hide empty records"></i>');
        },
        addAutoRefreshButton: function($table) {
            this.myDataTableToolbarAddHtml($table,
                '<i class="toggleAutoRefresh table-toolbar-icon bootstrap-glyph icon-pause handy smallpadside" rel="tooltip" title="Toggle auto-refresh"></i>');
        },
        /* fnConvertData takes the entries in collection (value, optionalKeyOrIndex) and returns a list
         * whose first element is the ID (hidden first column of table)
         * and other elements are the other columns in the table;
         * alternatively it can return null if the entry should be excluded
         * 
         * option refreshAllRows can be passed to force all rows to be re-rendered;
         * useful if rendering data may have changed even if value has not
         */ 
        updateMyDataTable: function(table, collection, fnConvertData, options) {
            if (table==null) return;
            if (options==null) options = {}
            var oldDisplayDataList = []
            try {
                oldDisplayDataList = table.dataTable().fnGetData();
            } catch (e) {
                // (used to) sometimes get error accessing column 1 of row 0, though table seems empty
                // caused by previous attempt to refresh from a closed view
                log("WARNING: could not fetch data; clearing")
                log(e)
                log(e.stack)
                table.dataTable().fnClearTable()
            }
            var oldDisplayIndexMap = {}
            var oldDisplayData = {}
            for (var idx in oldDisplayDataList) {
                var data = oldDisplayDataList[idx]
                oldDisplayIndexMap[data[0]] = idx
                oldDisplayData[data[0]] = data
            }
            var newDisplayData = {}
            var updateDisplayData = []
            ViewUtils.each(collection, function(data,index) { 
                var newRow = fnConvertData(data, index)
                if (newRow!=null) {
                    var id = newRow[0]

                    var displayIndex = oldDisplayIndexMap[id];
                    if (displayIndex!=null) {
                        updateDisplayData[displayIndex] = newRow
                        delete oldDisplayIndexMap[id]
                    } else {
                        newDisplayData[id] = newRow
                    }
                }
            })
            // first update (so indices don't change)
            for (var prop in updateDisplayData) {
                var rowProps = updateDisplayData[prop]
                var oldProps = oldDisplayData[rowProps[0]]
                for (idx in rowProps) {
                    var v = rowProps[idx]
                    if (options['refreshAllRows'] || !_.isEqual(v,oldProps[idx])) {
                        // update individual columns as values change
                        try {
                            table.fnUpdate( v, Number(prop), idx, false, false )
                        } catch (e) {
                            // often occurs if we haven't properly closed view, e.g. on entity switch
                            log("WARNING: cannot update row")
                            log(e)
                            log(e.stack)
                            log(v)
                            log(prop)
                            log(idx)
                        }
                    } else {
//                        log("NO CHANGE")
                    }
                }
            }
            // then delete old ones
            for (var prop in oldDisplayIndexMap) {
                var index = oldDisplayIndexMap[prop]
                table.fnDeleteRow( Number(index), null, false )
            }
            // and now add new ones
            for (var prop in newDisplayData) {
                try {
                    table.fnAddData( newDisplayData[prop], false )
                } catch (e) {
                    // errors sometimes if we load async
                    log("WARNING: cannot add to row")
                    log(e)
                    log(e.stack)
                    log(prop)
                    log(newDisplayData[prop])
                }
            }
            try {
                // redraw, but keeping pagination
                table.fnStandingRedraw();
            } catch (e) {
                log("WARNING: could not redraw")
                log(e)
                log(e.stack)                
            }
            ViewUtils.cancelFadeOnceLoaded(table)
        },
        toggleFilterEmpty: function($table, column) {
            var hideEmpties = $('.filterEmpty', $table.parent().parent()).toggleClass('icon-eye-open icon-eye-close').hasClass('icon-eye-close');
            if (hideEmpties) {
                $table.dataTable().fnFilter('.+', column, true);
            } else {
                $table.dataTable().fnFilter('.*', column, true);
            }
        },
        toggleAutoRefresh: function(pane) {
            var isEnabled = $('.toggleAutoRefresh', pane.$el).toggleClass('icon-pause icon-play').hasClass('icon-pause');
            pane.enableAutoRefresh(isEnabled);
        },
        attachToggler: function($scope) {
            var $togglers;
            if ($scope === undefined) {
                $togglers = $(".toggler-header");
            } else {
                $togglers = $(".toggler-header", $scope);
            }
            $togglers.click(this.onTogglerClick);
        },
        onTogglerClick: function(event) {
            ViewUtils.onTogglerClickElement($(event.currentTarget).closest(".toggler-header"));
        },
        onTogglerClickElement: function(root) {
            root.toggleClass("user-hidden");
            $(".toggler-icon", root).toggleClass("icon-chevron-left").toggleClass("icon-chevron-down");
            var next = root.next();
            if (root.hasClass("user-hidden")) {
                next.slideUp('fast');
            } else {
                next.slideDown('fast');
            }
        },
        showTogglerClickElement: function(root) {
            root.removeClass("user-hidden");
            $(".toggler-icon", root).removeClass("icon-chevron-left").addClass("icon-chevron-down");
            root.next().slideDown('fast');
        },
        updateTextareaWithData: function($div, data, showIfEmpty, doSlideDown, minPx, maxPx) {
            var $ta = $("textarea", $div);
            var show = showIfEmpty;
            if (data !== undefined) {
                $ta.val(data);
                show = true;
            } else {
                $ta.val("");
            }
            if (show) {
                this.setHeightAutomatically($ta, minPx, maxPx, false);
                if (doSlideDown) { $div.slideDown(100); }
            } else {
                $div.hide();
            }
        },
        setHeightAutomatically: function($ta, minPx, maxPx, deferred) {
            var height = $ta.prop("scrollHeight"), that = this;
            if ($ta.css("padding-top")) height -= parseInt($ta.css("padding-top"), 10)
            if ($ta.css("padding-bottom")) height -= parseInt($ta.css("padding-bottom"), 10)
//            log("scroll height "+height+" - old real height "+$ta.css("height"))
            if (height==0 && !deferred) {
                _.defer(function() { that.setHeightAutomatically($ta, minPx, maxPx, true) })
            } else {
                height = Math.min(height, maxPx);
                height = Math.max(height, minPx);
                $ta.css("height", height);
            }
            return height;
        },
        each: function(collection, fn) {
            if (_.isFunction(collection.each)) {
                // some objects (such as backbone collections) are not iterable
                // (either by "for x in" or "_.each") so call the "each" method explicitly on them 
                return collection.each(fn)
            } else {
                // try underscore
                return _.each(collection, fn);
            }
        },
        // makes tooltips appear as white-on-black-bubbles rather than boring black-on-yellow-boxes
        // but NB if the html is updated the tooltip can remain visible until page refresh
        processTooltips: function($el) {
            $el.find('*[rel="tooltip"]').tooltip();
        },
        fadeToIndicateInitialLoad: function($el) {
            // in case the server response time is low, fade out while it refreshes
            // (since we can't show updated details until we've retrieved app + entity details)
            try {                
                $el.fadeTo(1000, 0.3)
//                    .queue(
//                        function() {
//                            // does nothing yet -- see comment in brooklyn.css on .view_not_available
//                            $el.append('<div class="view_not_available"></div>')
//                        });
                // above works to insert the div, though we don't have styling on it
                // but curiously it also causes the parent to go to opacity 0 !?! 
            } catch (e) {
                // ignore - normal during tests
            }
        },
        cancelFadeOnceLoaded: function($el) {
            try {
//                $el.children('.view_not_available').remove();
                $el.stop(true, false).fadeTo(200, 1);
            } catch (e) {
                // ignore - normal during tests
            }
        },
        
        
        
        // TODO the get and fetch methods below should possibly be on a BrooklynView prototype
        // see also notes in router.js
        // (perhaps as part of that introduce a callWithFixedDelay method which does the tracking, 
        // so we can cleanly unregister, and perhaps an onServerFailure method, and with that we 
        // could perhaps get rid of, or at least dramatically simplify, the get/fetch)
        
        /* variant of $.get with automatic failure handling and recovery;
         * options should be omitted except by getRepeatedlyWithDelay */
        get: function(view, url, success, options) {
            if (view.viewIsClosed) return ;
            
            if (!options) options = {}
            if (!options.count) options.count = 1
            else options.count++;
//          log("getting, count "+options.count+", delay "+period+": "+url)
            
            var disabled = (options['enablement'] && !options['enablement']()) 
                || !BrooklynConfig.view.refresh
            if (options.count > 1 && disabled) {
                // not enabled, just requeue
                if (options['period']) 
                    setTimeout(function() { ViewUtils.get(view, url, success, options)}, options['period'])
                return;
            }
            
            /* inspects the status object returned from an ajax call in a view;
             * if not valid, it fades the view and increases backoff delays and resubmits;
             * if it is valid, it returns true so the caller can continue
             * (restoring things such as the view, timer, etc, if they were disabled);
             * 
             * takes some of the options as per fetchRepeatedlyWithDelay
             * (though they are less well tested here)
             * 
             * note that the status text object is rarely useful; normally the fail(handler) is invoked,
             * as above (#get)
             */
            var checkAjaxStatusObject = function(status, view, options) {
                if (view.viewIsClosed) return false;
                if (status == "success" || status == "notmodified") {
                    // unfade and restore
                    if (view._loadingProblem) {
                        log("getting view data is back to normal - "+url)
                        log(view)
                        view._loadingProblem = false;
                        
                        var fadeTarget = view.$el;
                        if ("fadeTarget" in options) {
                            fadeTarget = options["fadeTarget"]
                        }
                        if (fadeTarget) ViewUtils.cancelFadeOnceLoaded(fadeTarget)
                        
                        if (options['originalPeriod']) 
                            options.period = options['originalPeriod']; 
                    }
                    
                    return true;
                }
                if (status == "error" || status == "timeout" || status == "parsererror") {
                    // fade and log problem
                    if (!view._loadingProblem) {
                        log("error getting view data from "+url+" - is the server reachable?")
                        view._loadingProblem = true;
                    }
                    // fade the view, on error
                    var fadeTarget = view.$el;
                    if ("fadeTarget" in options) {
                        fadeTarget = options["fadeTarget"]
                    }
                    if (fadeTarget) ViewUtils.fadeToIndicateInitialLoad(fadeTarget)

                    if (options['period']) {
                        if (!options['originalPeriod']) options.originalPeriod = options['period'];
                        var period = options['period'];
                        
                        // attempt exponential backoff up to every 15m
                        period *= 2;
                        var max = (options['backoffMaxPeriod'] || 15*60*1000);
                        if (period > max) period = max;
                        options.period = period
                        setTimeout(function() { ViewUtils.get(view, url, success, options)}, period)
                    } 
                    
                    return false;
                }
                return true;
            }
            
            return $.get(url, function(data, status) {
                if (!checkAjaxStatusObject(status, view, options)) {
                    return;
                }
                if (success) success(data);
                if (options['period']) 
                    setTimeout(function() { ViewUtils.get(view, url, success, options)}, options['period'])
            }).fail(function() {
                checkAjaxStatusObject("error", view, options)
            })
        },
        /** invokes a get against the given url repeatedly, with fading and backoff on failures,
         * cf fetchRepeatedlyWithDelay, but here the user's callback function is invoked on success
         */
        getRepeatedlyWithDelay: function(view, url, success, options) {
            if (!options) options = {}
            if (!options['period']) options.period = 3000
            ViewUtils.get(view, url, success, options)
        },

        /** As fetchRepeatedlyWithDelay(view, model, options), but without updating a view. */
        fetchModelRepeatedlyWithDelay: function(model, options) {
            this.fetchRepeatedlyWithDelay(undefined, model, options);
        },

        /* invokes fetch on the model, associated with the view.
         * automatically closes when view closes, 
         * and fades display and exponentially-backs off on problems.
         * options include:
         * 
         *   enablement (function returning t/f whether the invocation is enabled)
         *   period (millis, currently 3000 = 3s default);
         *   originalPeriod (millis, becomes the period if successful; primarily for internal use);
         *   backoffMaxPeriod (millis, max time to wait between retries, currently 15*60*1000 = 10m default);
         *    
         *   doitnow (if true, kicks off a run immediately, else only after the timer)
         *   
         *   fadeTarget (jquery element to fade; defaults to view.$el; null can be set to prevent fade);
         *   
         *   fetchOptions (additional options to pass to fetch; however success and error should not be present);
         *   success (function to invoke on success, before re-queueing);
         *   error (optional function to invoke on error, before requeueing);
         */
        fetchRepeatedlyWithDelay: function(view, model, options) {
            if (view && view.viewIsClosed) return;
            
            if (!options) options = {}
            if (!options.count) options.count = 1
            else options.count++;
            
            var period = options['period'] || 3000
            var originalPeriod = options['originalPeriod'] || period
//            log("fetching, count "+options.count+", delay "+period+": "+model.url)
            
            var fetcher = function() {
                if (view && view.viewIsClosed) return;
                var disabled = (options['enablement'] && !options['enablement']()) 
                    || !BrooklynConfig.view.refresh
                if (options.count > 1 && disabled) {
                    // not enabled, just requeue
                    ViewUtils.fetchRepeatedlyWithDelay(view, model, options);
                    return;
                }
                var fetchOptions = options['fetchOptions'] ? _.clone(options['fetchOptions']) : {}
                fetchOptions.success = function(modelR,response,optionsR) {
                        var fn = options['success']
                        if (fn) fn(modelR,response,optionsR);
                        if (view && view._loadingProblem) {
                            log("fetching view data is back to normal - "+model.url)
                            view._loadingProblem = false;
                            
                            var fadeTarget = view.$el;
                            if ("fadeTarget" in options) {
                                fadeTarget = options["fadeTarget"]
                            }
                            if (fadeTarget) ViewUtils.cancelFadeOnceLoaded(fadeTarget)
                        }
                        options.period = originalPeriod;
                        ViewUtils.fetchRepeatedlyWithDelay(view, model, options);
                }
                fetchOptions.error = function(modelR,response,optionsR) {
                        var fn = options['error']
                        if (fn) fn(modelR,response,optionsR);
                        if (view && !view._loadingProblem) {
                            log("error fetching view data from "+model.url+" - is the server reachable?")
                            log(response)
                            view._loadingProblem = true;
                        }
                        // fade the view, on error
                        if (view) {
                            var fadeTarget = view.$el;
                            if ("fadeTarget" in options) {
                                fadeTarget = options["fadeTarget"]
                            }
                            if (fadeTarget) ViewUtils.fadeToIndicateInitialLoad(fadeTarget)
                        }
                        // attempt exponential backoff up to every 15m
                        period *= 2;
                        var max = (options['backoffMaxPeriod'] || 15*60*1000);
                        if (period > max) period = max;
                        options = _.clone(options)
                        options.originalPeriod = originalPeriod;
                        options.period = period;
                        ViewUtils.fetchRepeatedlyWithDelay(view, model, options);
                };
                model.fetch(fetchOptions)
            };
            if (options['doitnow']) {
                options.doitnow = false;
                fetcher();
            } else {
                setTimeout(fetcher, period);
            }
        },
        /** @deprecated since 0.7.0 use computeStatusIconInfo */
        computeStatusIcon: function(serviceUp, lifecycleState) {
            return this.computeStatusIconInfo(serviceUp, lifecycleState).url;
        },
        /** returns object with properties:
         *  String word;
         *  String url;
         *  boolean problem;
         */
        computeStatusIconInfo: function(serviceUp, lifecycleState) {
            var result = {};
            
            if (lifecycleState != null)
                lifecycleState = lifecycleState.toLowerCase();
            
            if (serviceUp===false || serviceUp=="false") serviceUp=false;
            else if (serviceUp===true || serviceUp=="true") serviceUp=true;
            else {
                if (serviceUp!=null && serviceUp !== "" && serviceUp !== undefined && serviceUp.toLowerCase().indexOf("loading")<0) {
                    log("Unknown 'serviceUp' value:")
                    log(serviceUp)
                }
                serviceUp = null;
            }
            var mode = null;
            var imgext = "png";
            var problem = false;
            
            if (lifecycleState=="running") {
                if (serviceUp==false) {
                    mode = "running-onfire";
                    problem = true;
                } else {
                    mode = "running";
                }
            } else if (lifecycleState=="stopped" || lifecycleState=="created") {
                if (serviceUp==true) {
                    mode = "stopped-onfire";
                    problem = true;
                } else {
                    mode = "stopped";
                }
            } else if (lifecycleState=="starting") {
                mode = "starting";
                imgext = "gif";  //animated
            } else if (lifecycleState=="stopping") {
                mode = "stopping";
                imgext = "gif";  //animated
            } else if (lifecycleState=="on_fire" || /* just in case */ lifecycleState=="on-fire" || lifecycleState=="onfire") {
                mode = "onfire";
                problem = true;
            }
            
            if (mode==null) {
                // no lifecycle state, rely on serviceUp
                if (lifecycleState!=null && lifecycleState !== "" && lifecycleState !== undefined && lifecycleState.toLowerCase().indexOf("loading")<0) {
                    log("Unknown 'lifecycleState' value:")
                    log(lifecycleState)
                }
                if (serviceUp) mode = "running"; 
                else if (serviceUp===false) mode = "stopped";
            }

            result.word = mode;
            result.problem = problem;
            if (mode==null) {
                // no status info at all
                result.url = null;
            } else {
                result.url = "./assets/img/"+"icon-status-"+mode+"."+imgext;
            }
            
            return result;
        }
    };
    return ViewUtils;
});
