// (C) Copyright 2020 Hewlett-Packard Enterprise Company, L.P.
/*global clearTimeout*/
define(['hp/presenter/MapPresenter',
    'hp/presenter/AssociatedRoute',
    'hp/core/LinkTargetBuilder',
    'hp/core/UrlFragment',
    'hp/core/StatusState',
    'hp/core/Style',
    'jquery',
    'lib/excanvas',
    'hp/lib/jquery.hpSelect',
    'hp/lib/jquery.hpSafeClone'],
function (presenter, associatedRoute, linkTargetBuilder, urlFragment, statusState, style) { 
"use strict";

    /**
     * Managed the visualization of resource associations in buckets/layers
     *
     * The basic algorithm is to take the results of an index service
     * associations tree and create a row/bucket for each category and
     * an items in the row for each resource. If the number of items in
     * a category gets too big, they are collapsed into a single summary
     * item. Connection lines are drawn between related items. If
     * the user hovers over an item, that item's connection lines and
     * directly associated items are highlighted.
     */

    var MapView = (function () {
      
        var CONTAINER = '.hp-map';
        var SPINNER = '#hp-map-spinner';
        var CANVAS = 'canvas';
        var BUCKETS = '.hp-buckets';
        var BUCKET_ITEMS = '.hp-bucket-items';
        var BUCKET_ITEM = '.hp-bucket-item';
        var SELECTED = 'hp-selected'; // on an item
        var SELECTION_RELATED = 'hp-selection-related'; // items related to selected item
        var HAS_HIGHLIGHT = 'hp-has-highlight';
        var HIGHLIGHT_FOCUS = 'hp-highlight-focus';
        var HIGHLIGHT = 'hp-highlight';
        var STATUSES = ['ok', 'error', 'warning', 'disabled', 'unknown'];
      
        /**
         * Constructor
         */
        function MapView() {
          
            // The route context we are running in. The caller sets this
            // before we are initialized so we can eventually tell our
            // presenter what to care about.
            var route;
            var container;
            var urlView; // what view to use for deep links
            // HTML templates pulled from the DOM so we can clone them.
            var bucketTemplate;
            var itemTemplate;
            var summaryItemTemplate;
            var canvas;
            var context; // for drawing
            // We delay layout to avoid jumpiness
            var layoutTimer;
            // We delay drawing to avoid flickering during mouse movement
            var drawTimer;
            // Cache of style.map()
            var mapStyle;
            var buckets = [];
            var items = {}; // uri -> item
            var map;
            
            // Drawing 
            
            // find the center of the item in relative coordinates
            function itemCenter(item, containerOffset) {
                var itemOffset = item.offset();
                return {left: Math.ceil(itemOffset.left + (item.outerWidth() / 2) -
                        containerOffset.left),
                    top: Math.ceil(itemOffset.top + (item.outerHeight() / 2) -
                        containerOffset.top)};
            }
            
            function clearCanvas() {
                if (context) {
                    context.clearRect(0, 0, canvas.width, canvas.height);
                }
            }
            
            function getInRangeValue(val) {
                if (val < 0) {
                    val = 0;
                } else if (val > 1) {
                    val = 1;
                }
                return val;
            }
            
            function initGradient(grad, from, to) {
                var stop;
                if (mapStyle.canvasGradients) {
                    stop = (mapStyle.anchorOffset * 3.5) / (to.top - from.top);
                    grad.addColorStop(0, mapStyle.selected.color);
                    grad.addColorStop(getInRangeValue(stop - 0.02), mapStyle.selected.color);
                    grad.addColorStop(stop, mapStyle.selected.subduedColor);
                    grad.addColorStop(getInRangeValue(1.0 - stop), mapStyle.selected.subduedColor);
                    grad.addColorStop(getInRangeValue(1.0 - stop + 0.02), mapStyle.selected.color);
                    grad.addColorStop(1, mapStyle.selected.color);
                    context.strokeStyle = grad;
                }
            }
            
            // no direct relationship to the selected item, diagonal
            function drawDiagonal(from, to, rowDelta) {
                var grad;
                context.beginPath();
                context.moveTo(from.left, from.top);
                if (context.strokeStyle === mapStyle.selected.color &&
                    rowDelta > 1) {
                    grad = context.createLinearGradient(from.left, from.top, to.left, to.top);
                    initGradient(grad, from, to);                        
                }
                context.lineTo(to.left, to.top);
                context.stroke();
            }
            
            // selected item is the parent, step across then down
            function drawParentStep(from, to, subdue) {
                var grad;
                // draw horizontal line solid
                context.beginPath();
                context.moveTo(from.left, from.top);
                context.lineTo(to.left, from.top);
                context.stroke();
                // draw gradient vertical line if crossing rows
                context.beginPath();
                context.moveTo(to.left, from.top);
                if (subdue) {
                    grad = context.createLinearGradient(to.left, from.top, to.left, to.top);
                    initGradient(grad, from, to);
                }
                context.lineTo(to.left, to.top);
                context.stroke();
            }
            
            // selected item is the child, draw across then up
            function drawChildStep(from, to, subdue) {
                var grad;
                // draw horizontal line solid
                context.beginPath();
                context.moveTo(to.left, to.top);
                context.lineTo(from.left, to.top);
                context.stroke();
                // draw gradient vertical line if crossing rows
                context.beginPath();
                context.moveTo(from.left, to.top);
                if (subdue) {
                    grad = context.createLinearGradient(from.left, from.top, from.left, to.top);
                    initGradient(grad, from, to);
                }
                context.lineTo(from.left, from.top);
                context.stroke();
            }
            
            function drawLink(mapLink, containerOffset) {
                var parentMapNode = mapLink.parentNode();
                var childMapNode = mapLink.childNode();
                var fromItem = items[parentMapNode.id()];
                var toItem = items[childMapNode.id()];
                var from = itemCenter(fromItem, containerOffset);
                var to = itemCenter(toItem, containerOffset);
                var rowDelta = childMapNode.row().index() - parentMapNode.row().index();
                
                // Anchor a fixed distance from the top and bottom of an item
                from.top += ((fromItem.outerHeight() / 2) -
                    mapStyle.anchorOffset);
                to.top -= ((toItem.outerHeight() / 2) -
                    mapStyle.anchorOffset);
                
                // Set the right line style
                if (mapLink.isHighlighted()) {
                    context.strokeStyle = mapStyle.highlight.color;
                    context.lineWidth = mapStyle.highlight.width;
                } else {
                    context.strokeStyle = mapStyle.selected.color;
                    context.lineWidth = mapStyle.selected.width;
                }
                
                if (parentMapNode.row().singleNode() && childMapNode.row().singleNode()) {
                    // Both items are the only ones in their row.
                    // If one of the items is the selected item,
                    // draw a rectilinear step line between them.
                    // Usually, this helps de-clutter the diagram.
                    if (parentMapNode.isSelected()) {
                        from.top -= mapStyle.anchorOffset;
                        drawParentStep(from, to,
                            (rowDelta > 1 && ! mapLink.isHighlighted()));
                    } else if (childMapNode.isSelected()) {
                        to.top += mapStyle.anchorOffset;
                        drawChildStep(from, to,
                            (rowDelta > 1 && ! mapLink.isHighlighted()));
                    } else {
                        drawDiagonal(from, to, rowDelta);
                    }
                } else {
                    // at least one row without a column,
                    // just draw a diagonal line
                    drawDiagonal(from, to, rowDelta);
                }
            }
            
            // draws connection lines from the links defined
            function draw() {
                var containerOffset = $(container).offset();
                var mapLinks, haveHighlight, length, i, mapLink;
                //var startTime = new Date(), endTime;
                
                // account for scrolling
                containerOffset.top -= $(container).scrollTop();
                containerOffset.left -= $(container).scrollLeft();
              
                if (context && map) {
                  
                    clearCanvas();
                    context.lineCap = 'square';
                    
                    mapLinks = map.links();
                    haveHighlight = map.hasHighlight();
                    length = mapLinks.length;
                    for (i=0; i<length; i++) {
                        mapLink = mapLinks[i];
                        // Draw the line if this link is highlighted or
                        // if we aren't highlighting.
                        if (mapLink.isDrawable(haveHighlight)) {
                            drawLink(mapLink, containerOffset);
                        }
                    }
                }
                //endTime = new Date();
                //console.log('!!! related view drawing took', (endTime.getTime() - startTime.getTime()) + "ms", " to draw", length);
            }

            // Highlighting
            
            // Sets the appropriate highlight classes.
            function highlight() {
                var bucketItems = $(BUCKET_ITEM);
                var redrawDelay = mapStyle.redrawDelay;
                var mapNodes, length, i, mapNode;
                
                bucketItems.removeClass(HIGHLIGHT_FOCUS + ' ' + HIGHLIGHT);
                
                if (map) {
                    mapNodes = map.highlightedNodes();
                    $(BUCKETS).toggleClass(HAS_HIGHLIGHT, mapNodes.length > 0);
                
                    length = mapNodes.length;
                    for (i=0; i<length; i++) {
                        mapNode = mapNodes[i];
                        items[mapNode.id()].addClass(HIGHLIGHT);
                    }
                    
                    if (length === 0) {
                        // removing highlight, slow down drawing default links
                        // so we don't flicker too much as the user mouses around
                        redrawDelay = redrawDelay * 4;
                    }
                } else {
                    $(BUCKETS).removeClass(HAS_HIGHLIGHT);
                }
                
                // Clear before redraw delay to have lines leave well before
                // drawing new ones. This reduces flickering.
                clearCanvas();
                
                clearTimeout(drawTimer);
                drawTimer = setTimeout(draw, redrawDelay);
            }
            
            // Layout
            
            function compressBuckets() {
                // For rows with too many items, adjust their left offset
                // so they are evenly distributed but overlapping.
                $(BUCKET_ITEMS).each(function (index, bucketItems) {
                    var items = $(BUCKET_ITEM, bucketItems);
                    var last = items.last();
                    if (last.outerWidth() * items.length >
                        $(bucketItems).innerWidth()) {
                        // we can't fit them all, overlap them
                        last.css({position : 'absolute', right: 0});
                        var step = ($(bucketItems).innerWidth() -
                            (last.outerWidth() + 0)) /
                            (items.length - 1);
                        items.not(':last').each(function (index, item) {
                            $(item).css({
                                position: 'absolute',
                                left: Math.ceil(index * step)
                            });
                        });
                        items.first().css('position', 'relative');
                        $(bucketItems).css('text-align', 'left');
                    } else {
                        // items will all fit, clear any prior adjustments
                        $(bucketItems).css('text-align', '');
                        items.each(function (index, item) {
                            $(item).css({
                                position: 'relative',
                                right: '', 
                                left: ''
                            });
                        });
                    }
                });
            }
            
            // Adjust item positioning for the currrent resolution.
            function layout() {
                var availableWidth = 0;
                var columnWidth;
                var mapRows, mapRow, mapNode, selectedColumn, item, bucket, length, i;
                
                if (map) {
                
                    $(BUCKET_ITEMS).each(function (index, elem) {
                        availableWidth = Math.max(availableWidth, $(elem).width());
                    });
                    columnWidth = Math.min(200,
                        Math.floor(availableWidth / map.numColumns()));
                    selectedColumn = map.selectedNode().row().column();

                    // For rows with only one item, adjust the bucket padding
                    // to stagger single items in different columns.
                    mapRows = map.rows();
                    length = mapRows.length;
                    for (i=0; i<length; i++) {
                        mapRow = mapRows[i];
                        if (mapRow.column() && mapRow.column() > selectedColumn) {
                            mapNode = mapRow.singleNode();
                            item = items[mapNode.id()];
                            bucket = buckets[mapRow.index()];
                            bucket.css('padding-left',
                                Math.min(columnWidth * mapRow.column(),
                                    availableWidth - item.outerWidth()));
                        }
                        else if (mapRow.column() && mapRow.column() < selectedColumn) {
                            mapNode = mapRow.singleNode();
                            item = items[mapNode.id()];
                            bucket = buckets[mapRow.index()];
                            bucket.css('padding-right',
                                Math.min(columnWidth * Math.abs(mapRow.column()),
                                    availableWidth - item.outerWidth()));
                        }
                    }

                    compressBuckets();
                
                    // align the canvas dimensions with the buckets
                    $(CANVAS, container).attr('width', $(BUCKETS).outerWidth()).
                        attr('height', $(BUCKETS).outerHeight());
                        
                    $(BUCKETS).removeClass('hp-disabled');
                
                    highlight();
                } else {
                    $(CANVAS, container).attr('width', 0).attr('height', 0);
                }
            }
            
            function onResize(event) {
                if (!event || event.target === window) {
                    clearTimeout(layoutTimer);
                    layoutTimer = setTimeout(layout, 50);
                }
            }
            
            // Construction
            
            function updateSingleMapNode(mapNode) {
                var item = items[mapNode.id()];
                var resource = mapNode.resource();
                var link = linkTargetBuilder.makeLink(resource.name,
                    resource.uri, urlView, resource.category);
                if (link.indexOf('<a') === 0) {
                    link = $(link).addClass('hp-jump');
                    $('.hp-jump', item).replaceWith(link);
                } else {
                    $(item).css('cursor', 'default');
                }
                
                $('.hp-status', item).hpStatus(statusState.getHealthStatus(resource.status));
                $('.hp-name', item).html(resource.name);
                if(resource.attributes && resource.attributes.model) {
                    $('.hp-model', item).text(resource.attributes.model);
                }
            }
            
            function updateSummaryMapNode(mapNode) {
                var item = items[mapNode.id()];
                var summary = mapNode.summary();
                var params = summary.associationParams;
                var status, length = STATUSES.length;
                var statusCount;
                var link, url;
                
                $('.hp-category-name', item).
                    text(linkTargetBuilder.categoryLabel(summary.category));
                $('.hp-category-count', item).text(summary.total)
                
                link = linkTargetBuilder.makeLink('related-link', null,
                    urlView, summary.category, null, null, params);
                url = $(link).attr('href');
                if (url) {
                    $('.hp-jump', item).attr('href', url);
                    $('.hp-category-count', item).attr('href', url);
                }
                
                if (!summary.status) {
                    // if there is no status, then hide the status row
                    $('.hp-summary-status', item).hide();
                } else {
                    $('.hp-summary-status', item).show();
                    for (var i=0; i<length; i++) {
                        status = STATUSES[i];
                        statusCount = summary.status[status] ? summary.status[status] : 0;
                        var statusParams = params.concat(['f_q=status:' + status]);
                        link = linkTargetBuilder.makeLink(
                            'related-link', null, urlView,
                            summary.category, null, null, statusParams);
                        url = $(link).attr('href');
                        if (url) {
                            $('.hp-summary-' + status, item).attr('href', url);
                        }
                        
                        $('.hp-summary-' + status + ' .hp-count', item).
                            text(statusCount);
                        if (statusCount <= 0) {
                            $('.hp-summary-' + status, item).addClass('hp-disabled');
                            $('.hp-summary-' + status + ' .hp-status', item).
                                addClass('hp-inactive');
                        } else {
                            $('.hp-summary-' + status, item).removeClass('hp-disabled');
                            $('.hp-summary-' + status + ' .hp-status', item).
                                removeClass('hp-inactive');
                        }
                    }
                } 
            }
            
            function insertItem(bucket, item, mapNode) {
                item.attr('data-id', mapNode.id());
                if (mapNode.isSelected()) {
                    item.addClass(SELECTED);
                } else if (mapNode.isSelectionRelated()) {
                    item.addClass(SELECTION_RELATED);
                }
                $(BUCKET_ITEMS, bucket).append(item);
                items[mapNode.id()] = item;
            }
            
            function addSingleMapNode(bucket, mapNode) {
                var item = itemTemplate.hpSafeClone();
                
                insertItem(bucket, item, mapNode);
                updateSingleMapNode(mapNode);
            }
            
            function addSummaryMapNode(bucket, mapNode) {
                var item = summaryItemTemplate.hpSafeClone();
                
                insertItem(bucket, item, mapNode);
                updateSummaryMapNode(mapNode);
            }
            
            function addMapRow(mapRow) {
                var bucket = bucketTemplate.hpSafeClone();
                var label = linkTargetBuilder.categoryLabel(mapRow.category());
                var mapNodes = mapRow.nodes();
                var mapNode, length = mapNodes.length;
                
                // setup DOM
                $('> label', bucket).text(label);
                $(BUCKETS).append(bucket);
                var labelHeight = $('> label', bucket).outerHeight();
                
                for (var i=0; i<length; i++) {
                    mapNode = mapNodes[i];
                    if (mapNode.summary()) {
                        addSummaryMapNode(bucket, mapNode);
                    } else {
                        addSingleMapNode(bucket, mapNode);
                    }
                }
                buckets[mapRow.index()] = bucket;
                
                var bucketHeight = bucket.outerHeight();
                if (labelHeight >= bucketHeight) {
                    // make bucket's height equal to labelHeight
                    bucket.css("height", labelHeight);
                }
                // +1 for safety, to prevent 0/2 case
                var labelTop = (bucket.outerHeight() - labelHeight + 1) / 2;
                $('> label', bucket).css("top", labelTop);
            }
            
            function addMap() {
                var mapRows = map.rows();
                var length;
                
                clearTimeout(layoutTimer);

                length = mapRows.length;
                for (var i=0; i<length; i++) {
                    addMapRow(mapRows[i]);
                }
                
                layoutTimer = setTimeout(onResize, style.animationDelay());
            }
            
            function updateMapRow(mapRow) {
                var mapNodes = mapRow.nodes();
                var length = mapNodes.length;
                var mapNode;
                
                for (var i=0; i<length; i++) {
                    mapNode = mapNodes[i];
                    if (mapNode.summary()) {
                        updateSummaryMapNode(mapNode);
                    } else {
                        updateSingleMapNode(mapNode);
                    }
                }
            }
            
            function updateMap() {
                var mapRows = map.rows();
                var length;
                length = mapRows.length;
                for (var i = 0; i<length; i++) {
                    updateMapRow(mapRows[i]);
                }
            }
            
            function reset() {
                buckets = [];
                items = {};
                if (map) {
                    map.destroy();
                    map = null;
                }
                clearCanvas();
                $(CANVAS, container).attr('width', 0).attr('height', 0);
                $(BUCKETS).empty().removeClass('hp-has-highlight');
            }
            
            // Presenter Events
            
            // React to an index service association tree
            function onMapChange(mapData) {
                if (map && mapData.refreshed) {
                    if (map.hasDifferentNodes(mapData)) {
                        reset();
                        map = mapData;
                        addMap();
                    } else {
                        map = mapData;
                        updateMap();
                    }
                } else {
                    $(SPINNER).hide();
                    map = mapData;
                    addMap();
                }
            }
            
            // Clean up old content when the tree starts changing
            function onMapChanging() {
                // clear out prior state
                reset();
                $(BUCKETS).addClass('hp-disabled');
                $(SPINNER).show();
            }
            
            // Clean up old content when the tree has error
            function onMapError() {
                // clear out prior state
                reset();
                $(BUCKETS).addClass('hp-disabled');
                // TODO: When the appliance is not reachable for
                // any reason, we need to have a UX discussion to
                // have a consistent UI for all pages.
            }
            
            // DOM Events
            
            function onClickItem(event) {
                var item = $(event.currentTarget);
                var jump = $('.hp-jump', item);
                if (jump[0] !== event.target &&
                    $(event.target).parents('.hp-summary-status').length === 0) {
                    if (jump.attr('href')) {
                        jump.trigger('click');
                    }
                    return false;
                }
            }
            
            // In case the highlighting gets wedged, typically because
            // the browser doesn't deliver a mouseleave event,
            // allow clicking to turn off highlighting.
            function onBodyClick(event) {
                $('.' + HIGHLIGHT_FOCUS).removeClass(HIGHLIGHT_FOCUS);
                map.removeHighlight();
                highlight();
                $('body').off('click', onBodyClick);
            }
            
            // When the mouse enters an item, set it to be the
            // highlight focus and redo the highlighting.
            function onEnterItem(event) {
                var target = $(event.currentTarget);
                if (target.hasClass('hp-bucket-item')) {
                    $(BUCKET_ITEM).removeClass(HIGHLIGHT_FOCUS);
                    target.addClass(HIGHLIGHT_FOCUS);
                    map.highlightNode(target.attr('data-id'));
                    highlight();
                    $('body').on('click', onBodyClick);
                }
            }
            
            function onLeaveItem(event) {
                var target = $(event.currentTarget);
                if (target.hasClass('hp-bucket-item')) {
                    target.removeClass(HIGHLIGHT_FOCUS);
                    map.removeHighlight();
                    highlight();
                    $('body').off('click', onBodyClick);
                }
            }
            
            function onClickAssociated(event) {
                var elem = $(event.currentTarget);
                associatedRoute.routeToAssociated(elem.attr('data-assoc-name'),
                    elem.attr('data-assoc-uri'), urlView);
            }
          
            /**
             * @public
             */
            this.route = route;
            
            this.resume = function () {
                $('.hp-details-pane').addClass('hp-no-notifications');
                $('.hp-details-header .hp-map-control, .hp-details-header .hp-related-control').
                    addClass('hp-active');
                presenter.on('mapChanging', onMapChanging);
                presenter.on('mapChange', onMapChange);
                presenter.on('mapError', onMapError);
                $(window).on('resize', onResize);
                $('.hp-page').on('relayout', layout);
                $(container).on('scroll', draw);
                presenter.resume();
                layout();
            };
            
            this.pause = function () {
                clearTimeout(layoutTimer);
                clearTimeout(drawTimer);
                $('.hp-details-pane').removeClass('hp-no-notifications');
                $('.hp-details-header .hp-map-control, .hp-details-header .hp-related-control').
                    removeClass('hp-active');
                presenter.pause();
                presenter.off('mapChanging', onMapChanging);
                presenter.off('mapChange', onMapChange);
                presenter.off('mapError', onMapError);
                $(window).off('resize', onResize);
                $('.hp-page').off('relayout', layout);
                $(container).off('scroll', draw);
            };
            
            this.init = function () {
                mapStyle = style.related();
                // Cache whether we can use canvas gradients, excanvas doesn't
                // support them
                mapStyle.canvasGradients = $('html').hasClass('canvas');
                
                itemTemplate = $('#hp-bucket-item-template').detach().
                    removeAttr('id').removeClass('hp-template');
                summaryItemTemplate = $('#hp-bucket-summary-item-template').
                    detach().removeAttr('id').removeClass('hp-template');
                bucketTemplate = $('#hp-bucket-template').detach().attr('id', '');
                
                if (this.route) {
                    container = $(CONTAINER);
                    presenter.init(this.route);
                    urlView = urlFragment.getView(this.route);
                }
                
                canvas = $(CANVAS, container)[0];
                if (!canvas.getContext && window.G_vmlCanvasManager) {
                    window.G_vmlCanvasManager.initElement(canvas);
                }
                if (! context && canvas.getContext) {
                    context = canvas.getContext('2d');
                }
                
                $(BUCKETS).on('click', BUCKET_ITEM, onClickItem);
                $(BUCKETS).on('click', 'a[data-assoc-uri]', onClickAssociated);
                
                $(BUCKETS).on({'mouseenter': onEnterItem,
                    'mouseleave': onLeaveItem}, BUCKET_ITEM);
                
                this.resume();
            };
            
            // Functions for using MapView outside of a route
            this.container = container;
            
            this.setMap = function (map) {
                onMapChange(map);
            };
            
            this.unsetMap = function () {
                onMapChanging();
            };
        }

        return MapView;
    }());
    
    return MapView;
});
