// (C) Copyright 2020 Hewlett-Packard Enterprise Company, L.P.

define(['hp/core/Style',
    'text!hpPages/graphics/logical_switch_template.html',
    'jquery',
    'lib/excanvas',
    'hp/lib/jquery.hpStatus'],
function(style, templateHtml) { 
"use strict";
  
    /**
     * Draws connection lines for a logical switch.
     * There are four basic sections of code:
     * 1) layout - aligns sizes and determine when to be hp-small
     * 2) cache  - inspect the DOM and stores coordinates to make drawing faster
     * 3) draw   - draw the connection lines based on the cached coordinates
     * 4) mouse  - react to mouse events to update the DOM and cache
     */

    var GraphicLogicalSwitch = (function() {
      
        var LOGICAL_SWITCH = '.hp-logical-switch';
        var CANVAS = LOGICAL_SWITCH + ' canvas';
        var SWITCH_ROWS = '.hp-physical-switch-rows';
        var SWITCH_ROW = '.hp-physical-switch-row';
        var SWITCH = '.hp-switch';
        var LOGICAL_UPLINKS = '.hp-logical-uplinks';
        var LOGICAL_UPLINK = '.hp-logical-uplink';
        var UP_PORT = '.hp-up-port';
        var STACKING_LINK = UP_PORT + '.hp-stacking';
        var BACKPLANE_PORTS = '.hp-backplane-ports';
        var BACKPLANE_PORT = '.hp-backplane-ports ' + UP_PORT;
        var HAS_HIGHLIGHT = 'hp-has-highlight';
        var ALTER = 'hp-alter';
        var EDIT_UPLINK = 'hp-edit-uplink';
        var HIGHLIGHT = 'hp-highlight';
        var SELECTED = 'hp-selected';
        var SMALL = 'hp-small';
        var BODY = '#hp-body-div';
        var MAX_UPLINKS_PER_ROW = 14;
        var GUTTER_PADDING = 8;
        
        /**
         * Constructor
         */
        function GraphicLogicalSwitch() {
          
            var container = null;
            var scrollingContext = null;
            var smallResetDimensions = {width: 0, height: 0};
            var smallResetArea = 0;
            
            // deprecated templates
            var switchRowTemplate = null;
            var switchTemplate = null;
            var switchPortTemplate = null;
            var uplinkTemplate = null;
            
            var resizeTimer = null;
            // Cache of style.map()
            var mapStyle;
            
            // cached state for drawing
            var context;
            var small = false;
            var gutterPadding = GUTTER_PADDING;
            var leftGutter, rightGutter; // {needs: , delta: , used: , width: , right: }
            var switchGutters = []; // {needs: , delta: , used: , coords: }
            // lots of cross linking to speed things up
            var uplinks = {}; // id: {coords: , ports: [], trace: }
            var ports = {}; // id: {coords: }
            var uplinkRows = []; // {coords: , uplinks: []}, ...
            var switchRows = []; // {coords: , ports: [{coords: , switchGutter: }]}
            var stackingLinks = []; // {ports: [], trace: }
            
            // temporary state for drawing
            var hasHighlight = false;
            
            // DRAWING functions
            
            function beginDrawing(highlighted, special) {
                context.beginPath();
                if (highlighted) {
                    context.lineWidth = mapStyle.highlight.width;
                    if (special) {
                        context.strokeStyle = mapStyle.special.color;                      
                    } else {
                        context.strokeStyle = mapStyle.highlight.color;
                    }
                } else if (special && ! hasHighlight) {
                    context.lineWidth = mapStyle.special.width;
                    context.strokeStyle = mapStyle.special.color;
                } else {
                    context.lineWidth = mapStyle.primary.width;
                    context.strokeStyle = mapStyle.primary.color;
                }
            }
            
            function drawPortConnection(port, top) {
                if (port) {
                    var portCoords = port.coords;
                
                    if (portCoords.top > top) {
                        context.moveTo(portCoords.centerX, portCoords.top);
                    } else {
                        context.moveTo(portCoords.centerX, portCoords.bottom);
                    }
                    context.lineTo(portCoords.centerX, top);
                }
            }
            
            function drawSwitchGutterSegment(switchGutterSegment) {
                var length = switchGutterSegment.ports.length;
                
                context.moveTo(switchGutterSegment.left, switchGutterSegment.y);
                context.lineTo(switchGutterSegment.right, switchGutterSegment.y);
                
                for (var i=0; i<length; i++) {
                    drawPortConnection(switchGutterSegment.ports[i], switchGutterSegment.y);
                }
            }
            
            function drawTrace(trace) {
                var length = trace.switchGutterSegments.length;
                var x;
                if (trace.left >= 0) {
                    x = trace.left;
                } else if (trace.right >= 0) {
                    x = trace.right;
                }
                
                if (length > 0) {
                    
                    if (x) {
                        context.moveTo(x, trace.top);
                        context.lineTo(x, trace.bottom);
                    }
                
                    for (var i=0; i<length; i++) {
                        drawSwitchGutterSegment(trace.switchGutterSegments[i]);
                    }
                }
            }
            
            function drawStackingLink(stackingLink) {
                beginDrawing(stackingLink.highlight);
                drawTrace(stackingLink.trace);
                context.lineCap = 'square';
                context.stroke();
            }
            
            function drawUplink(uplink) {
                var coords = uplink.coords;
                var length = uplink.ports.length;
                var trace = uplink.trace;
                var firstSegment;
                
                if (length > 0) {
                    beginDrawing(uplink.highlight, uplink.special);
                
                    if (trace.switchGutterSegments.length > 0) {
                        context.moveTo(coords.left - 1, coords.top + 1);
                        context.lineTo(coords.left - 1, uplink.bottom);
                    
                        if (trace.left >= 0) {
                            context.lineTo(trace.left, uplink.bottom)
                        }
                        else if (trace.right >= 0) {
                            context.lineTo(trace.right, uplink.bottom)
                        }
                        else {
                            // only connection is from uplink to first switch row
                            firstSegment = trace.switchGutterSegments[0];
                            context.lineTo(coords.left - 1, firstSegment.y);
                            if (coords.left < firstSegment.left) {
                                context.lineTo(firstSegment.left, firstSegment.y);
                            } else if (coords.left > firstSegment.right) {
                                context.lineTo(firstSegment.right, firstSegment.y);
                            }
                        }
                    
                        drawTrace(trace);
                    }
                
                    context.lineCap = 'square';
                    context.stroke();
                }
            }
          
            function draw() {
                var canvas = $(CANVAS, container)[0];
                var highlightedUplinks = [];
                var highlightedStackingLinks = [];
                var length, uplink, stackingLink;
                
                if (canvas.getContext) {
                    context = canvas.getContext('2d');
                    context.clearRect(0, 0, canvas.width, canvas.height);
                    hasHighlight = $(LOGICAL_SWITCH, container).hasClass(HAS_HIGHLIGHT);
                    
                    // draw connections, first stacking, then unhighlighted, then highlighted
                    // this is so highlighted ones overlay non highlighted ones
                    if (! $(LOGICAL_SWITCH, container).hasClass(EDIT_UPLINK)) {
                        length = stackingLinks.length;
                        for (var i=0; i<length; i++) {
                            stackingLink = stackingLinks[i];
                            if (stackingLink.highlight) {
                                highlightedStackingLinks.push(stackingLink);
                            } else {
                                if (!small) {
                                    drawStackingLink(stackingLink);	
                                }
                            }
                        }
                    }
                    
                    $.each(uplinks, function (index, uplink) {
                        if (uplink.highlight) {
                            highlightedUplinks.push(uplink);
                        } else {
                            if (!small) {
                                drawUplink(uplink);	
                            }
                        }
                    });
                    
                    length = highlightedStackingLinks.length;
                    for (i=0; i<length; i++) {
                        stackingLink = highlightedStackingLinks[i];
                        drawStackingLink(stackingLink);
                    }
                    
                    length = highlightedUplinks.length;
                    for (i=0; i<length; i++) {
                        uplink = highlightedUplinks[i];
                        drawUplink(uplink);
                    }
                    
                    context = null;
                }
            }
            
            // CACHING functions
            
            function itemCoords(item) {
                var containerOffset = $(LOGICAL_SWITCH, container).offset();
                var itemOffset = item.offset();
                var left = itemOffset.left - containerOffset.left;
                var top = itemOffset.top - containerOffset.top;
                var bottom = top + item.outerHeight();
                var right = left + item.outerWidth();
                var centerX = Math.ceil(left + ((right - left) / 2));
                var centerY = Math.ceil(top + ((bottom - top) / 2));
                
                return {left: left, top: top, bottom: bottom, right: right,
                    centerX: centerX, centerY: centerY};
            }
            
            function addCoords(coords1, coords2, justWidth) {
                coords1.left = Math.min(coords1.left, coords2.left);
                coords1.right = Math.max(coords1.right, coords2.right);
                if (! justWidth) {
                    coords1.top = Math.min(coords1.top, coords2.top);
                    coords1.bottom = Math.max(coords1.bottom, coords2.bottom);
                }
                coords1.centerX = -1; // invalidate
                coords1.centerY = -1; // invalidate
            }
            
            // DEMARCATION - once we have a record of where are the DOM elements are and
            // how they are related, calculate positions so we don't have to when we draw
            
            function firstOrMin(first, second) {
                if (! second) {
                    return first;
                } else {
                    return Math.min(first, second);
                }
            }
            
            function firstOrMax(first, second) {
                if (! second) {
                    return first;
                } else {
                    return Math.max(first, second);
                }
            }
            
            function demarcateSwitchGutterSegment(trace, switchGutterSegment, multiRow,
                divertLeft) {
                var switchGutter = switchGutterSegment.switchGutter;
                var switchGutterOffset = 0;
                var gutterOffset = 0;
                var x;
                
                if (! small) {
                    switchGutterOffset = switchGutter.delta * switchGutter.used;
                }
                switchGutter.used += 1;
                switchGutterSegment.y = switchGutter.coords.bottom - switchGutterOffset -
                    gutterPadding;
                
                if (multiRow) {
                    if (divertLeft) {
                        // need to go left around intervening rows
                        if (!small) {
                            gutterOffset = (leftGutter.delta * leftGutter.used);
                        }
                        x = gutterPadding + gutterOffset;
                        trace.left = firstOrMin(x, trace.left);
                        switchGutterSegment.left = firstOrMin(trace.left,
                            switchGutterSegment.left);
                    } else {
                        // need to go right around intervening rows
                        if (!small) {
                            gutterOffset = (rightGutter.delta * rightGutter.used);
                        }
                        x = rightGutter.right + rightGutter.width -
                            (gutterPadding + gutterOffset);
                        trace.right = firstOrMax(x, trace.right);
                        switchGutterSegment.right = firstOrMax(trace.right,
                            switchGutterSegment.right);
                    }
                }
                
                trace.top = firstOrMin(switchGutterSegment.y, trace.top);
                trace.bottom = firstOrMax(switchGutterSegment.y, trace.bottom);
            }
            
            function demarcateTrace(trace, divertLeft, multiRow) {
                var length = trace.switchGutterSegments.length;
                for (var i=0; i<length; i++) {
                    demarcateSwitchGutterSegment(trace,
                        trace.switchGutterSegments[i], multiRow, divertLeft);
                }
                
                if (multiRow) {
                    if (divertLeft) {
                        leftGutter.used += 1;
                    } else {
                        rightGutter.used += 1;
                    }
                }
            }
            
            function demarcateStackingLink(stackingLink) {
                demarcateTrace(stackingLink.trace, false, false);
            }
            
            function demarcateUplink(uplink, uplinkGutterOffset, divertLeft, multiRow) {
                var length = uplink.trace.switchGutterSegments.length;
                // use an uplink gutter segment
                uplink.bottom = uplink.coords.bottom +
                    gutterPadding + uplinkGutterOffset;
                // make sure the trace comes up to the uplink
                uplink.trace.top = firstOrMin(uplink.bottom, uplink.trace.top);
                uplink.trace.bottom = firstOrMax(uplink.bottom, uplink.trace.bottom);
                
                demarcateTrace(uplink.trace, divertLeft, multiRow);
            }
            
            function demarcateUplinkRow(uplinkRow, multiRow) {
                var length = uplinkRow.uplinks.length;
                var gutterOffset = 0;
                var midPoint = Math.ceil(length / 2);
                
                // do first half from left to right
                for (var i=0; i<midPoint; i++) {
                    if (! small) {
                        gutterOffset = (i * uplinkRow.gutter.delta)
                    }
                    demarcateUplink(uplinkRow.uplinks[i], gutterOffset, true, multiRow);
                }
                // do second half from right to left
                for (i=(length-1); i>=midPoint; i--) {
                    if (! small) {
                        gutterOffset = ((length - 1 - i) * uplinkRow.gutter.delta)
                    }
                    demarcateUplink(uplinkRow.uplinks[i], gutterOffset, false, multiRow);
                }
            }
            
            function demarcate() {
                var length, multiRow;
                
                // do stacking links first so they are closer to the switches
                length = stackingLinks.length;
                for (var i=0; i<length; i++) {
                    demarcateStackingLink(stackingLinks[i]);
                }
                
                length = uplinkRows.length;
                multiRow = (length > 1 || switchRows.length > 1);
                for (i=0; i<length; i++) {
                    demarcateUplinkRow(uplinkRows[i], multiRow);
                }
            }
            
            // GUTTERS - gutters are the blank areas between DOM elements where we draw
            // the connection lines. Each uplink row has a gutter below it. There are
            // switch gutters between each switch row, including a gutter above the first
            // row and below the last row. And, there are left and right vertical gutters
            // on the outer edges. Gutters are used in three passes, the first pass calculates
            // how many connection lines are 'need'ed, the second pass calculates
            // the 'delta' between lines, and the third pass actually consumes space via 'used'.
            
            function useSideGutter(uplinkRow) {
                var length = uplinkRow.uplinks.length;
                var uplink;
                for (var i=0; i<length; i++) {
                    uplink = uplinkRow.uplinks[i];
                    if (uplink.ports.length > 0) {
                        if (i < (length / 2)) {
                            leftGutter.needs += 1;
                        } else {
                            rightGutter.needs += 1;
                        }
                    }
                }
            }
            
            function useUplinkGutters() {
                // use the space available between uplink rows
                var coords, length, reference, uplinkRow, uplinkGutter, height;
                
                coords = itemCoords($(LOGICAL_UPLINKS, container));
                length = uplinkRows.length;
                for (var i=0; i<length; i++) {
                    uplinkRow = uplinkRows[i];
                    uplinkGutter = uplinkRow.gutter;
                    if ((length - 1) === i) {
                        // last row, space to bottom of all uplinks
                        reference = coords.bottom;
                        uplinkRow.last = true;
                    } else {
                        // space to top of next row
                        reference = uplinkRows[i+1].coords.top;
                    }
                    height = reference - uplinkRows[i].coords.bottom;
                    uplinkGutter.delta = Math.floor(
                        (height - (2 * gutterPadding)) /
                        (small ? 1 : uplinkGutter.needs));
                        
                    useSideGutter(uplinkRow);
                }
            }
            
            function useSwitchGutters() {
                // use the space available between switch rows
                var length, switchGutter, height, switchRow;
                
                length = switchGutters.length;
                for (var i=0; i<length; i++) {
                    switchGutter = switchGutters[i];
                    if (0 === i) {
                        switchGutter.first = true;
                    }
                    height = switchGutter.coords.bottom - switchGutter.coords.top;
                    switchGutter.delta = Math.floor(
                        (height - (2 * gutterPadding)) /
                        (small ? 1 : switchGutter.needs));
                }
            }
            
            function useEdgeGutters() {
                // use the space on the left and right edges
                leftGutter.delta = Math.floor((leftGutter.width - gutterPadding) /
                    (small ? 1 : leftGutter.needs));
                rightGutter.width = leftGutter.width;
                rightGutter.delta = Math.floor((rightGutter.width - gutterPadding) /
                    (small ? 1 : rightGutter.needs));
            }
            
            // CACHE OBJECTS - essentially create a javascript object for each interesting
            // DOM element and then adding logical objects and connecting the needed relationships.
            // Record the coordinates of the DOM elements as we go. We will use these later
            // during demarcation and drawing.
            
            function cachePort(portElem, switchRow, switchElem, switchCoords) {
                var id = portElem.attr('id');
                var portCoords = itemCoords(portElem);
                var switchGutter;
                
                if (portCoords.top < (switchCoords.top + 50)) {
                    switchGutter = switchRow.upperGutter;
                } else {
                    switchGutter = switchRow.lowerGutter;
                }
                var port = {
                    coords: portCoords,
                    id: id,
                    switchGutter: switchGutter,
                    // needed for selection highlighting
                    portElem: portElem,
                    switchElem: switchElem
                };
                ports[id] = port;
                
                return port;
            }
            
            function cacheSwitchGutter() {
                var switchGutter = {index: switchGutters.length,
                    delta: 0, needs: 0, used: 0, coords: {},
                    switchRowAbove: null, switchRowBelow: null};
                switchGutters.push(switchGutter);
                return switchGutter;
            }
            
            function cacheSwitch(switchElem) {
                var switchRowsCoords = itemCoords($(SWITCH_ROWS, container));
                var switchCoords = itemCoords(switchElem);
                var length = switchRows.length;
                var switchRow, switchGutter;
                
                // see if we already have a row for this switch
                for (var i=0; i<length; i++) {
                    if (switchRows[i].coords.top === switchCoords.top) {
                        switchRow = switchRows[i];
                        break;
                    }
                }
                
                if (! switchRow) {
                    switchRow = {
                        coords: switchCoords
                        // upperGutter, lowerGutter set below
                    };
                    
                    // upper gutter
                    if (switchGutters.length === 0) {
                        switchGutter = cacheSwitchGutter();
                        // first gutter starts at top of switch rows
                        switchGutter.coords.top = switchRowsCoords.top;
                    } else {
                        switchGutter = switchGutters[switchGutters.length - 1];
                    }
                    switchGutter.coords.bottom = switchCoords.top;
                    switchGutter.switchRowBelow = switchRow;
                    switchRow.upperGutter = switchGutter;
                    
                    // lower gutter
                    switchGutter = cacheSwitchGutter();
                    switchGutter.switchRowAbove = switchRow;
                    switchRow.lowerGutter = switchGutter;
                    switchGutter.coords.top = switchCoords.bottom;
                    // will be overwritten by next switch
                    switchGutter.coords.bottom = switchRowsCoords.bottom; 
                    
                    switchRows.push(switchRow);
                } else {
                    addCoords(switchRow.coords, switchCoords);
                }
                addCoords(switchRow.upperGutter.coords, switchCoords, true);
                
                leftGutter.width = firstOrMin(switchRow.coords.left, leftGutter.width);
                rightGutter.right = firstOrMax(switchRow.coords.right, rightGutter.right);
                
                $(UP_PORT, switchElem).each(function (index, portElem) {
                    cachePort($(portElem), switchRow, switchElem, switchCoords);
                });
            }
            
            function cacheTracePort(trace, port) {
                var switchGutter = port.switchGutter;
                var switchGutterSegment;
                var length = trace.switchGutterSegments.length;
                
                // which switch gutter for this port?
                for (var i=0; i<length; i++) {
                    if (trace.switchGutterSegments[i].switchGutter.index ===
                        switchGutter.index) {
                        switchGutterSegment = trace.switchGutterSegments[i];
                        break;
                    }
                }
                
                if (! switchGutterSegment) {
                    switchGutterSegment = {
                        left: port.coords.centerX,
                        right: port.coords.centerX,
                        switchGutter: switchGutter,
                        //y, set during demarcation
                        ports: [port]
                    };
                    trace.switchGutterSegments.push(switchGutterSegment);
                    switchGutter.needs += 1;
                } else {
                    // record left and right extent of ports
                    switchGutterSegment.left =
                        firstOrMin(port.coords.centerX, switchGutterSegment.left);
                    switchGutterSegment.right =
                        firstOrMax(port.coords.centerX, switchGutterSegment.right);
                    switchGutterSegment.ports.push(port);
                }
            }
            
            function cacheTrace() {
                var trace = {
                    //left: , right: ,
                    //top: , bottom: ,
                    switchGutterSegments: []
                };
                return trace;
            }
            
            function cacheStackingLink(stackingPorts) {
                var length, port, stackingLink, length2, switchGutter;
                
                if (stackingPorts.length > 0 && stackingPorts[0]) {
                    port = stackingPorts[0];
                    stackingLink = {
                        trace: cacheTrace(),
                        ports: stackingPorts
                    }
                    stackingLinks.push(stackingLink);
                    
                    length = stackingPorts.length;
                    for (var i=0; i<length; i++) {
                        port = stackingPorts[i];
                        if (port) {
                            
                            cacheTracePort(stackingLink.trace, port);
                            
                            port.stackingLink = stackingLink;
                        }
                    }
                    
                    if (stackingLink.trace.switchGutterSegments.length > 1) {
                        rightGutter.needs += 1;
                    }
                }
            }
            
            function cacheSwitchStackingLinks() {
                var switch1, switch2, tmpPorts, done, id;
                var donePorts = {}; // id: true/false
                
                $(SWITCH + '[data-link-id]', container).each(function (index, switchElem) {
                    switch1 = $(switchElem);
                    id = switch1.attr('data-link-id');
                    if (id) {
                        switch2 = $('#' + id);
                        tmpPorts = [];
                        done = false;
                    
                        $(BACKPLANE_PORT, switch1).each(function (index2, portElem) {
                            id = $(portElem).attr('id');
                            if (donePorts[id]) {
                                done = true;
                                return false;
                            } else {
                                tmpPorts.push(ports[id]);
                                donePorts[id] = true;
                            }
                        });
                        if (! done) {
                            $(BACKPLANE_PORT, switch2).each(function (index2, portElem) {
                                id = $(portElem).attr('id');
                                if (donePorts[id]) {
                                    done = true;
                                    return false;
                                } else {
                                    tmpPorts.push(ports[id]);
                                    donePorts[id] = true;
                                }
                            });
                        }
                        if (! done) {
                            cacheStackingLink(tmpPorts);
                        }
                    }
                });
            }
            
            function cachePortStackingLinks() {
                var tmpPorts, id1, id2;
                var donePorts = {}; // id: true/false
                
                $(STACKING_LINK, container).each(function (index, portElem) {
                    id1 = $(portElem).attr('id');
                    if (! donePorts[id1]) {
                        id2 = $(portElem).attr('data-link-id');
                        if (id2 && ! donePorts[id2]) {
                            tmpPorts = [ports[id1], ports[id2]];
                            donePorts[id1] = true
                            donePorts[id2] = true
                            
                            cacheStackingLink(tmpPorts);
                        }
                    }
                });
            }
            
            function cacheUplinkRow(uplinkElem, uplinkCoords) {
                var length = uplinkRows.length;
                var uplinkRow;
                
                // see if we already have a row for this uplink
                for (var i=0; i<length; i++) {
                    if (uplinkRows[i].coords.top === uplinkCoords.top) {
                        uplinkRow = uplinkRows[i];
                        break;
                    }
                }
                
                if (! uplinkRow) {
                    uplinkRow = {
                        coords: $.extend({}, uplinkCoords),
                        uplinks: [],
                        gutter: {delta: 0, needs: 0, used: 0}
                    };
                    uplinkRows.push(uplinkRow);
                } else {
                    addCoords(uplinkRow.coords, uplinkCoords);
                }
                
                // first row doesn't matter as we don't need to go around it
                if (uplinkRows.length > 1) {
                    leftGutter.width = firstOrMin(uplinkRow.coords.left, leftGutter.width);
                    rightGutter.right = firstOrMax(uplinkRow.coords.right, rightGutter.right);
                }
                
                return uplinkRow;
            }
            
            function cacheUplink(index, uplinkElem) {
                var uplinkCoords = itemCoords(uplinkElem);
                var uplinkRow = cacheUplinkRow(uplinkElem, uplinkCoords);
                var uplink, portElements, port;
                
                uplink = {
                    id: uplinkElem.attr('id'),
                    coords: uplinkCoords,
                    uplinkRow: uplinkRow,
                    ports: [], // for connections
                    trace: cacheTrace(),
                    special: uplinkElem.hasClass('hp-special'),
                    // for port selection highlighting
                    uplinkElem: uplinkElem,
                    highlight: uplinkElem.hasClass(HIGHLIGHT)
                };
                uplinkRow.uplinks.push(uplink);
                uplinks[uplink.id] = uplink;
                
                portElements = $(UP_PORT + "[data-link-id='" + uplink.id + "']",
                    container);
                portElements.each(function (index, portElem) {
                    port = ports[$(portElem).attr('id')];
                    port.uplink = uplink;
                    uplink.ports.push(port);
                    cacheTracePort(uplink.trace, port);
                });
                if (portElements.length > 0) {
                    uplinkRow.gutter.needs += 1;
                }
                
                return uplink;
            }
            
            function resetCache() {
                uplinkRows = [];
                switchRows = [];
                uplinks = {};
                ports = {};
                stackingLinks = [];
                leftGutter = {width: 100, needs: 0, delta: 0, used: 0};
                rightGutter = {width: 100, needs: 0, delta: 0, used: 0, right: 0};
                switchGutters = [];
                
                small = $(LOGICAL_SWITCH, container).hasClass('hp-small');
                gutterPadding = (small ? 5 : GUTTER_PADDING);
                
                $(SWITCH + ':visible', container).each(function (index, switchElem) {
                    cacheSwitch($(switchElem));
                });
                
                $(LOGICAL_UPLINK + ':visible', container).each(
                    function (index, uplinkElem) {
                        cacheUplink(index, $(uplinkElem));
                    });
                
                cacheSwitchStackingLinks();
                cachePortStackingLinks();
                
                useUplinkGutters();
                useSwitchGutters();
                useEdgeGutters();
                
                demarcate();
            }
            
            // LAYOUT - aligns the uplink and switch DOM elements
            
            function layoutUplinks() {
                var isSmall = $(LOGICAL_SWITCH, container).hasClass('hp-small');
                
                var uplinkListElem = $(LOGICAL_UPLINKS, container);
                var uplinkElems = $(LOGICAL_UPLINK, container);
                var numUplinks, numRows, uplinksPerRow, uplinkWidth;
                var step, indexInRow, row, rowId, rowElem, rowUplinksElem;
                
                if (isSmall) {
                    // calculations
                    numUplinks = uplinkElems.length;
                    numRows = Math.ceil(numUplinks / MAX_UPLINKS_PER_ROW);
                    uplinksPerRow = Math.ceil(numUplinks / numRows);
                    uplinkWidth = uplinkElems.outerWidth();
                    step = (uplinkListElem.innerWidth() - uplinkWidth) / uplinksPerRow;
                    
                    // insert rows
                    for (var i=1; i<= numRows; i++) {
                        rowId = 'hp-uplinks-row-' + i;
                        if ($('#' + rowId, uplinkListElem).length === 0) {
                            rowElem = $('<li></li>').addClass('hp-uplinks-row');
                            rowUplinksElem = $('<ol></ol>').addClass('hp-row-uplinks').
                                attr('id', rowId);
                            rowElem.append(rowUplinksElem);
                            uplinkListElem.append(rowElem);
                        }
                    }
                    
                    // place items in correct rows
                    row = 1;
                    indexInRow = 0;
                    uplinkElems.each(function (index, uplinkElem) {
                        rowId = '#hp-uplinks-row-' + row;
                        $(rowId).append(uplinkElem);
                        $(uplinkElem).css({
                            position: ( indexInRow === 0 ? 'relative' : 'absolute'),
                            left: Math.ceil(indexInRow * step)
                        });
                        indexInRow += 1;
                        if (indexInRow === uplinksPerRow) {
                            row += 1;
                            indexInRow = 0;
                        }
                    });   
                } else {
                    // not small
                    uplinkElems.each(function (index, uplinkElem) {
                        uplinkListElem.append(uplinkElem); // remove from rows
                        $(uplinkElem).css({
                            position: 'relative',
                            left: ''
                        });
                    });
                    $('.hp-uplinks-row', uplinkListElem).remove(); // remove rows, if any
                }
            }
            
            function resetSmall() {
                var result = false;
                var logicalSwitch, dimensions, area;
                if (scrollingContext && scrollingContext.length > 0) {
                    logicalSwitch = $(LOGICAL_SWITCH, container);
                    dimensions = {width: scrollingContext.outerWidth(),
                        height: scrollingContext.outerHeight()};
                    area = dimensions.width * dimensions.height;
                    
                    // don't bother if the context hasn't changed since our last check
                    if (dimensions.width !== smallResetDimensions.width ||
                        dimensions.height !== smallResetDimensions.height) {
                            
                        // Go small if parent is scrolling or if this area
                        // is smaller than an area we've known to need to be small at
                        if ((dimensions.height + 10) <
                            scrollingContext[0].scrollHeight ||
                            area < smallResetArea) {
                            result = ! logicalSwitch.hasClass(SMALL);
                            logicalSwitch.addClass(SMALL);
                            smallResetArea = Math.max(area, smallResetArea);
                        } else {
                            result = logicalSwitch.hasClass(SMALL);
                            logicalSwitch.removeClass(SMALL);
                        }
                        
                        smallResetDimensions = dimensions;
                    }
                }
                return result;
            }
          
            function alignSwitches() {
                var altering = $(LOGICAL_SWITCH, container).hasClass(ALTER);
                var maxWidth = 0;
                $(SWITCH, container).each(function (index, switchElem) {
                    $(switchElem).css('width', '');
                });
                $(SWITCH, container).each(function (index, switchElem) {
                    $(BACKPLANE_PORTS, switchElem).
                        toggle($(BACKPLANE_PORT, switchElem).length > 0 && ! altering);
                    maxWidth = Math.max(maxWidth, $(switchElem).width());
                });
                $(SWITCH, container).each(function (index, switchElem) {
                    $(switchElem).css('width', maxWidth+1); // +1 is for Firefox
                });
            }
            
            function alignUplinks() {
                var maxWidth = 0;
                $(LOGICAL_UPLINK, container).each(function (index, uplink) {
                    maxWidth = Math.max(maxWidth, $(uplink).outerWidth());
                });
                $(LOGICAL_UPLINK, container).each(function (index, uplink) {
                    $(uplink).css('width', maxWidth);
                });
            }
            
            function layout() {
                var logicalSwitch = $(LOGICAL_SWITCH, container);
                var canvas = $(CANVAS, container)[0];
                var repeat = false;
                
                // IE canvas setup
                if (!canvas.getContext && window.G_vmlCanvasManager) {
                    window.G_vmlCanvasManager.initElement(canvas);
                }
                
                // make all logical uplinks the same width
                alignUplinks();
                // make all switches the same width
                alignSwitches();

                repeat = resetSmall();
                
                layoutUplinks();
                
                $(CANVAS, container).attr('width', logicalSwitch.outerWidth()-2).
                    attr('height', logicalSwitch.outerHeight()-2);
                
                return repeat;
            }
            
            function reset(full) {
                if (full) {
                    smallResetDimensions = {width: 0, height: 0}; // force re-evaluation
                    smallResetArea = 0;
                }
                var repeat = layout();
                resetCache();
                draw();
                
                if (repeat) {
                    setTimeout(reset, 100);
                }
            }
            
            // MOUSE EVENTS
            
            function dehighlight() {
                var length, length2;
                
                length = stackingLinks.length;
                for (var i=0; i<length; i++) {
                    stackingLinks[i].highlight = false;
                }
                length = uplinkRows.length;
                for (i=0; i<length; i++) {
                    length2 = uplinkRows[i].uplinks.length;
                    for (var j=0; j<length2; j++) {
                        uplinkRows[i].uplinks[j].highlight = false;
                    }
                }
                
                $(UP_PORT, container).removeClass(HIGHLIGHT);
                $(SWITCH, container).removeClass(HIGHLIGHT);
                $(LOGICAL_UPLINK, container).removeClass(HIGHLIGHT);
                $(LOGICAL_SWITCH, container).removeClass(HAS_HIGHLIGHT);
                
                draw();
            }
            
            function highlightStackingLink(stackingLink) {
                var port;
                var length = stackingLink.ports.length;
                stackingLink.highlight = true;
                for (var i=0; i<length; i++) {
                    port = stackingLink.ports[i];
                    port.portElem.addClass(HIGHLIGHT);
                    port.switchElem.addClass(HIGHLIGHT);
                }
                $(LOGICAL_SWITCH, container).addClass(HAS_HIGHLIGHT);
            }
            
            function dehighlightStackingLink(stackingLink) {
                var port;
                var length = stackingLink.ports.length;
                stackingLink.highlight = false;
                for (var i=0; i<length; i++) {
                    port = stackingLink.ports[i];
                    port.portElem.removeClass(HIGHLIGHT);
                    // remove switch highlight if no other ports are highlighting it
                    if ($(UP_PORT + '.' + HIGHLIGHT, port.switchElem).length === 0) {
                        port.switchElem.removeClass(HIGHLIGHT);
                    }
                }
                // another uplink might be selected 
                if ($(LOGICAL_UPLINK + '.' + SELECTED, container).length === 0) {
                    $(LOGICAL_SWITCH, container).removeClass(HAS_HIGHLIGHT);
                }
            }
            
            function highlightUplink(uplink) {
                var port;
                var length = uplink.ports.length;
                uplink.uplinkElem.addClass(HIGHLIGHT);
                uplink.highlight = true;
                for (var i=0; i<length; i++) {
                    port = uplink.ports[i];
                    port.portElem.addClass(HIGHLIGHT);
                    port.switchElem.addClass(HIGHLIGHT);
                }
                $(LOGICAL_SWITCH, container).addClass(HAS_HIGHLIGHT);
            }
            
            function dehighlightUplink(uplink) {
                var port;
                var length = uplink.ports.length;
                uplink.uplinkElem.removeClass(HIGHLIGHT);
                uplink.highlight = false;
                for (var i=0; i<length; i++) {
                    port = uplink.ports[i];
                    port.portElem.removeClass(HIGHLIGHT);
                    // remove switch highlight if no other ports are highlighting it
                    if ($(UP_PORT + '.' + HIGHLIGHT, port.switchElem).length === 0) {
                        port.switchElem.removeClass(HIGHLIGHT);
                    }
                }
                // another uplink might be selected 
                if ($(LOGICAL_UPLINK + '.' + SELECTED, container).length === 0) {
                    $(LOGICAL_SWITCH, container).removeClass(HAS_HIGHLIGHT);
                }
            }
            
            function onEnterUplink(event) {
                var uplinkElem = $(event.currentTarget);
                var id = uplinkElem.attr('id');
                var uplink = uplinks[id];
                if (uplink && ! uplinkElem.hasClass(SELECTED)) {
                    highlightUplink(uplink);
                    draw();
                }
            }
            
            function onLeaveUplink(event) {
                var uplinkElem = $(event.currentTarget);
                var id = uplinkElem.attr('id');
                var uplink = uplinks[id];
                if (uplink && ! uplinkElem.hasClass(SELECTED)) {
                    dehighlightUplink(uplink);
                    draw();
                }
            }
            
            function onEnterPort(event) {
                var portElem = $(event.currentTarget);
                var id = portElem.attr('id');
                var port = ports[id];
                if (port) {
                    if (port.uplink) {
                        if (! port.uplink.uplinkElem.hasClass(SELECTED)) {
                            highlightUplink(port.uplink);
                            draw();
                        }
                    } else if (port.stackingLink) {
                        if (! port.stackingLink.highlight) {
                            highlightStackingLink(port.stackingLink);
                            draw();
                        }
                    }
                }
            }
            
            function onLeavePort(event) {
                var portElem = $(event.currentTarget);
                var id = portElem.attr('id');
                var port = ports[id];
                if (port) {
                    if (port.uplink) {
                        if (! port.uplink.uplinkElem.hasClass(SELECTED)) {
                            dehighlightUplink(port.uplink);
                            draw();
                        }
                    } else if (port.stackingLink) {
                        dehighlightStackingLink(port.stackingLink);
                        draw();
                    }
                }
            }
            
            var onClickBody = function() {}; // forward reference for Sonar
            
            function deselect() {
                $(LOGICAL_UPLINK, container).removeClass(SELECTED);
                $(UP_PORT, container).removeClass(SELECTED);
                $(BODY).off('click', onClickBody);
            }
            
            function selectUplink(uplinkElem) {
                uplinkElem.addClass(SELECTED);
                // delay to avoid flickering
                setTimeout(function () {$(BODY).on('click', onClickBody);}, 50);
                draw();
            }
            
            function uplinkElemForPortElem(portElem) {
                var id = portElem.attr('id');
                var port = ports[id];
                var result;
                // might not have any uplink port
                if (port && port.uplink) {
                    result = port.uplink.uplinkElem;
                }
                return result;
            }
            
            onClickBody = function (event) {
                if ($(event.target).closest(LOGICAL_UPLINK + ', ' + UP_PORT).length === 0) {
                    deselect();
                    dehighlight();
                    draw();
                }
            }
            
            function onClickUplink(event) {
                var uplinkElem = $(event.currentTarget);
                if (uplinkElem.hasClass(SELECTED)) {
                    uplinkElem.removeClass(SELECTED);
                } else {
                    selectUplink(uplinkElem);
                }
            }
            
            function onClickPort(event) {
                var portElem = $(event.currentTarget);
                var uplinkElem = uplinkElemForPortElem(portElem);
                if (uplinkElem) {
                    if (uplinkElem.hasClass(SELECTED)) {
                        uplinkElem.removeClass(SELECTED);
                    } else {
                        selectUplink(uplinkElem);
                    }
                }
            }
            
            function onResize(event) {
                clearTimeout(resizeTimer);
                resizeTimer = setTimeout(function () {
                    reset(false);
                }, 50);
            }
            
            function disableInteractivity() {
                container.off({'mouseenter': onEnterUplink,
                    'mouseleave': onLeaveUplink, 'click': onClickUplink},
                    LOGICAL_UPLINK);
                container.off({'mouseenter': onEnterPort,
                    'mouseleave': onLeavePort, 'click': onClickPort},
                    UP_PORT);
            }
            
            function enableInteractivity() {
                disableInteractivity(); // always turn off
                container.on({'mouseenter': onEnterUplink,
                    'mouseleave': onLeaveUplink, 'click': onClickUplink},
                    LOGICAL_UPLINK);
                container.on({'mouseenter': onEnterPort,
                    'mouseleave': onLeavePort, 'click': onClickPort},
                    UP_PORT);
            }
            
            function registerListeners() {
                $(window).on('resize', onResize);
                container.parents('.hp-page').on('relayout', reset);
            }
            
            function unregisterListeners() {
                $(window).off('resize', onResize);
                container.parents('.hp-page').off('relayout', reset);
            }
            
            /**
             * @public
             */
            this.init = function(containerArg, scrollingContextArg) {
                mapStyle = style.map();
                var template = $(templateHtml);
                switchPortTemplate = $(UP_PORT, template).remove();
                switchTemplate = $(SWITCH, template).remove();
                switchRowTemplate = $(SWITCH_ROW, template).remove();
                uplinkTemplate = $(LOGICAL_UPLINK, template).remove();
                container = $(containerArg);
                scrollingContext = $(scrollingContextArg);
                
                if (container.is(':empty')) {
                    container.append(template);
                } else {
                    $(LOGICAL_SWITCH, container).prepend($('canvas', template).remove());
                    $('.hp-status', container).hpStatus();
                }
                
                registerListeners();
                enableInteractivity();
            };
            
            this.pause = function () {
                unregisterListeners();
            };
            
            this.resume = function () {
                registerListeners();
                reset(true);
            };
            
            this.done = function () {
                reset(true);
            };
            
            this.setInteractive = function(enabled) {
                if (enabled) {
                    enableInteractivity();
                } else {
                    disableInteractivity();
                    deselect();
                    dehighlight();
                }
            };
            
            // DEPRECATED: the following add*() functions are deprecated
            this.addSwitchRow = function (id) {
                var result = switchRowTemplate.clone().attr('id', id);
                $('.hp-physical-switch-rows', container).append(result);
                return result;
            };
            
            this.addSwitch = function (rowElem, id) {
                var result = switchTemplate.clone().attr('id', id);
                $('.hp-physical-switches', rowElem).append(result);
                return result;
            };
            
            this.addSwitchPort = function (switchElem, id) {
                var result = switchPortTemplate.clone().attr('id', id);
                $('.hp-up-ports', switchElem).append(result);
                return result;
            };
            
            this.addLogicalUplink = function (id) {
                var result = uplinkTemplate.clone().attr('id', id);
                $('.hp-logical-uplinks', container).append(result);
                return result;
            };
            
            this.connect = function (logicalUplinkId, switchId, portIndex) {
                var valid = true;
                var switchElem;
                
                if ($('#' + logicalUplinkId, container).length !== 1) {
                    console.warn('No logical uplink with id', logicalUplinkId);
                    valid = false;
                }
                switchElem = $('#' + switchId, container);
                if (switchElem.length !== 1) {
                    console.warn('No switch with id', switchId);
                    valid = false;
                } else if ($(UP_PORT + ':eq(' + portIndex + ')',
                    switchElem).length !== 1) {
                    console.warn('No port with index', portIndex, 'in switch',
                        switchId);
                    valid = false;
                } else {
                    $(UP_PORT + ':eq(' + portIndex + ')',
                        switchElem).attr('data-link-id', logicalUplinkId);
                }
            };
            
            // Can we remove this?
            this.clear = function () {
                $('.hp-physical-switch-rows', container).empty();
                $('.hp-logical-uplinks', container).empty();
                uplinkRows = [];
                switchRows = [];
            };
        }

        return GraphicLogicalSwitch;
    }());
    
    return GraphicLogicalSwitch;
});
