/*
 * Copyright (c) 2015 Mountainstorm
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */


+function ($) {
    'use strict'

    // Cross Browser starts/endsWith support
    // =====================================
    String.prototype.startsWith = function (prefix) {
        return this.indexOf(prefix) == 0;
    };

    String.prototype.endsWith = function (suffix) {
        return this.indexOf(suffix, this.length - suffix.length) !== -1;
    };

    // GRAPHVIZSVG PUBLIC CLASS DEFINITION
    // ===================================

    // @ts-ignore: Keep traditional constructor function pattern for jQuery plugin compatibility
    var GraphvizSvg = function (element, options) {
        this.type = null
        this.options = null
        this.enabled = null
        this.$element = null
        // Initial mouse position when mouse has not moved yet.
        this.mousePosition = {
            x: window.innerWidth / 2,
            y: window.innerHeight / 2
        };

        this.init('graphviz.svg', element, options)
    }

    GraphvizSvg.VERSION = '1.0.1'

    GraphvizSvg.GVPT_2_PX = 32.5 // used to ease removal of extra space

    GraphvizSvg.DEFAULTS = {
        url: null,
        svg: null,
        shrink: '0.10pt',
        tooltips: {
            init: function ($graph) {
                var $a = $(this)
                $a.tooltip({
                    container: $graph,
                    placement: 'auto left',
                    animation: false,
                    viewport: null
                }).on('hide.bs.tooltip', function () {
                    // keep them visible even if you acidentally mouse over
                    if ($a.attr('data-tooltip-keepvisible')) {
                        return false
                    }
                })
            },
            show: function () {
                var $a = $(this)
                $a.attr('data-tooltip-keepvisible', true)
                $a.tooltip('show')
            },
            hide: function () {
                var $a = $(this)
                $a.removeAttr('data-tooltip-keepvisible')
                $a.tooltip('hide')
            },
            update: function () {
                var $this = $(this)
                if ($this.attr('data-tooltip-keepvisible')) {
                    $this.tooltip('show')
                    return
                }
            }
        },
        zoom: true,
        highlight: {
            selected: function (col, bg) {
                return col
            },
            unselected: function (col, bg) {
                return jQuery.Color(col).transition(bg, 0.9)
            }
        },
        ready: null
    }

    GraphvizSvg.prototype.onMouseUpdate = function (e) {
        this.mousePosition = {
            x: e.pageX,
            y: e.pageY
        };
    }

    GraphvizSvg.prototype.init = function (type, element, options) {
        this.enabled = true
        this.type = type
        this.$element = $(element)
        this.options = this.getOptions(options)

        if (options.url) {
            var that = this
            $.get(options.url, null, function (data) {
                var svg = $("svg", data)
                that.$element.html(document.adoptNode(svg[0]))
                that.setup()
            }, "xml")
        } else {
            if (options.svg) {
                this.$element.html(options.svg)
            }
            this.setup()
        }

        document.addEventListener('mousemove', this.onMouseUpdate.bind(this), false);
        document.addEventListener('mouseenter', this.onMouseUpdate.bind(this), false);
    }

    GraphvizSvg.prototype.getDefaults = function () {
        return GraphvizSvg.DEFAULTS
    }

    GraphvizSvg.prototype.getOptions = function (options) {
        options = $.extend({}, this.getDefaults(), this.$element.data(), options)

        if (options.shrink) {
            if (typeof options.shrink != 'object') {
                options.shrink = {
                    x: options.shrink,
                    y: options.shrink
                }
            }
            options.shrink.x = this.convertToPx(options.shrink.x)
            options.shrink.y = this.convertToPx(options.shrink.y)
        }
        return options
    }

    GraphvizSvg.prototype.setup = function () {
        var options = this.options

        // save key elements in the graph for easy access
        var $svg = $(this.$element.children('svg'))
        var $graph = $svg.children('g:first')
        this.$svg = $svg
        this.$graph = $graph
        this.$background = $graph.children('polygon:first') // might not exist
        this.$nodes = $graph.children('.node')
        this.$edges = $graph.children('.edge')
        this.$clusters = $graph.children('.cluster')
        this._nodesByName = {}
        this._edgesByName = {}

        // add top level class and copy background color to element
        this.$element.addClass('graphviz-svg')
        if (this.$background.length) {
            this.$element.css('background', this.$background.attr('fill'))
        }

        // setup all the nodes and edges
        var that = this
        this.$nodes.each(function () { that.setupNodesEdges($(this), true) })
        this.$edges.each(function () { that.setupNodesEdges($(this), false) })

        // remove the graph title element
        var $title = this.$graph.children('title')
        this.$graph.attr('data-name', $title.text())
        $title.remove()

        if (options.zoom) {
            this.setupZoom()
        }

        // tell people we're done
        if (options.ready) {
            options.ready.call(this)
        }
    }

    GraphvizSvg.prototype.setupNodesEdges = function ($el, isNode) {
        var that = this
        var options = this.options

        // save the colors of the paths, ellipses and polygons
        $el.find('polygon, ellipse, path').each(function () {
            var $this = $(this)
            // save original colors
            $this.data('graphviz.svg.color', {
                fill: $this.attr('fill'),
                stroke: $this.attr('stroke')

            })

            // shrink it if it's a node
            if (isNode && options.shrink) {
                that.scaleNode($this)
            }
        })

        // save the node name and check if theres a comment above; save it
        var $title = $el.children('title')
        if ($title[0]) {
            // remove any compass points:
            var title = $title.text().replace(/:[snew][ew]?/g, '')
            $el.attr('data-name', title)
            $title.remove()
            if (isNode) {
                this._nodesByName[title] = $el[0]
            } else {
                this._edgesByName[title] = $el[0]
            }
            // without a title we can't tell if its a user comment or not
            var previousSibling = $el[0].previousSibling
            while (previousSibling && previousSibling.nodeType != 8) {
                previousSibling = previousSibling.previousSibling
            }
            if (previousSibling != null && previousSibling.nodeType == 8) {
                var htmlDecode = function (input) {
                    var e = document.createElement('div')
                    e.innerHTML = input
                    return e.childNodes[0].nodeValue
                }
                var value = htmlDecode(previousSibling.nodeValue.trim())
                if (value != title) {
                    // user added comment
                    $el.attr('data-comment', value)
                }
            }
        }

        // remove namespace from a[xlink:title]
        $el.children('a').filter(function () { return $(this).attr('xlink:title') }).each(function () {
            var $a = $(this)
            $a.attr('title', $a.attr('xlink:title'))
            $a.removeAttr('xlink:title')
            if (options.tooltips) {
                options.tooltips.init.call(this, that.$element)
            }
        })
    }

    GraphvizSvg.prototype.setupZoom = function () {
        var that = this
        var $element = this.$element
        var $svg = this.$svg
        this.zoom = { width: $svg.attr('width'), height: $svg.attr('height'), percentage: null }
        this.scaleView(100.0)
        $element.mousewheel(function (evt) {
            if (evt.ctrlKey) {
                var percentage = that.zoom.percentage
                percentage += evt.deltaY * evt.deltaFactor
                if (percentage < 100.0) {
                    percentage = 100.0
                }
                // get pointer offset in view
                // ratio offset within svg
                var dx = evt.pageX - $svg.offset().left
                var dy = evt.pageY - $svg.offset().top
                var rx = dx / $svg.width()
                var ry = dy / $svg.height()

                // offset within frame ($element)
                var px = evt.pageX - $element.offset().left
                var py = evt.pageY - $element.offset().top

                that.scaleView(percentage)
                // scroll so pointer is still in same place
                $element.scrollLeft((rx * $svg.width()) + 0.5 - px)
                $element.scrollTop((ry * $svg.height()) + 0.5 - py)
                return false // stop propogation
            }
        })

        $element.on("keydown", function (evt) {
            console.log(evt)
            if (evt.shiftKey) {
                $element.css('cursor', 'move')
            }
        })

        $element.on("keyup", function (evt) {
            console.log(evt)
            if (evt.shiftKey) {
                $element.css('cursor', 'auto')
            }
        })
        $element
            .on('mousemove', function (e) {
                var $svg = this.$svg
                $element.css({ 'transform-origin': ((e.pageX - $(this).offset().left) / $(this).width()) * 100 + '% ' + ((e.pageY - $(this).offset().top) / $(this).height()) * 100 + '%' });
            })
    }

    GraphvizSvg.prototype.scaleView = function (percentage) {
        var that = this
        var $svg = this.$svg
        $svg.attr('width', percentage + '%')
        $svg.attr('height', percentage + '%')
        this.zoom.percentage = percentage
        // now callback to update tooltip position
        var $everything = this.$nodes.add(this.$edges)
        $everything.children('a[title]').each(function () {
            that.options.tooltips.update.call(this)
        })
    }

    GraphvizSvg.prototype.scaleInView = function (percentage) {
        var that = this
        var $svg = this.$svg
        var $element = this.$element

        // get pointer offset in view
        // ratio offset within svg
        var dx = this.mousePosition.x - $svg.offset().left
        var dy = this.mousePosition.y - $svg.offset().top
        var rx = dx / $svg.width()
        var ry = dy / $svg.height()

        // offset within frame ($element)
        var px = this.mousePosition.x - $element.offset().left
        var py = this.mousePosition.y - $element.offset().top

        $svg.attr('width', percentage + '%')
        $svg.attr('height', percentage + '%')
        this.zoom.percentage = percentage
        // now callback to update tooltip position
        var $everything = this.$nodes.add(this.$edges)
        $everything.children('a[title]').each(function () {
            that.options.tooltips.update.call(this)
        })

        // scroll so pointer is still in same place
        $element.scrollLeft((rx * $svg.width()) + 0.5 - px)
        $element.scrollTop((ry * $svg.height()) + 0.5 - py)
    }

    GraphvizSvg.prototype.scaleToNode = function ($node) {
        var $svg = this.$svg
        var $element = this.$element

        // Get the bounding box of the node
        var shape = $node.first()[0]
        if (!shape) return

        var bbox = shape.getBoundingClientRect()

        // Calculate percentage to fit the node with some padding
        var padding = 300 // pixels of padding around the node
        var widthScale = ($element.width() - 2 * padding) / bbox.width * 100
        var heightScale = ($element.height() - 2 * padding) / bbox.height * 100
        var targetPercentage = Math.min(widthScale, heightScale)

        // If already at target zoom, zoom out to 100%
        if (this.zoom.percentage == targetPercentage) {
            $svg.attr('width', '100%')
            $svg.attr('height', '100%')
            this.zoom.percentage = 100
            return
        }

        if (targetPercentage < 100) {
            targetPercentage = 100
        }

        // Set the new zoom level
        $svg.attr('width', targetPercentage + '%')
        $svg.attr('height', targetPercentage + '%')
        this.zoom.percentage = targetPercentage

        // Get updated bounding box after zoom
        var newBbox = shape.getBoundingClientRect()
        var newCenterX = newBbox.left + newBbox.width / 2
        var newCenterY = newBbox.top + newBbox.height / 2

        // Center the node in the viewport
        var viewportCenterX = $element.offset().left + $element.width() / 2
        var viewportCenterY = $element.offset().top + $element.height() / 2

        $element.scrollLeft($element.scrollLeft() + (newCenterX - viewportCenterX))
        $element.scrollTop($element.scrollTop() + (newCenterY - viewportCenterY))
    }

    GraphvizSvg.prototype.scaleNode = function ($node) {
        var dx = this.options.shrink.x
        var dy = this.options.shrink.y
        var tagName = $node.prop('tagName')
        if (tagName == 'ellipse') {
            $node.attr('rx', parseFloat($node.attr('rx')) - dx)
            $node.attr('ry', parseFloat($node.attr('ry')) - dy)
        } else if (tagName == 'polygon') {
            // this is more complex - we need to scale it manually
            var bbox = $node[0].getBBox()
            var cx = bbox.x + (bbox.width / 2)
            var cy = bbox.y + (bbox.height / 2)
            var pts = $node.attr('points').split(' ')
            var points = '' // new value
            for (var i in pts) {
                var xy = pts[i].split(',')
                var ox = parseFloat(xy[0])
                var oy = parseFloat(xy[1])
                points += (((cx - ox) / (bbox.width / 2) * dx) + ox) +
                    ',' +
                    (((cy - oy) / (bbox.height / 2) * dy) + oy) +
                    ' '
            }
            $node.attr('points', points)
        }
    }

    GraphvizSvg.prototype.convertToPx = function (val) {
        var retval = val
        if (typeof val == 'string') {
            var end = val.length
            var factor = 1.0
            if (val.endsWith('px')) {
                end -= 2
            } else if (val.endsWith('pt')) {
                end -= 2
                factor = GraphvizSvg.GVPT_2_PX
            }
            retval = parseFloat(val.substring(0, end)) * factor
        }
        return retval
    }

    GraphvizSvg.prototype.findEdge = function (nodeName, testEdge, $retval) {
        var retval = []
        for (var name in this._edgesByName) {
            var match = testEdge(nodeName, name)
            if (match) {
                if ($retval) {
                    $retval.push(this._edgesByName[name])
                }
                retval.push(match)
            }
        }
        return retval
    }

    GraphvizSvg.prototype.findLinked = function (node, includeEdges, testEdge, $retval) {
        var that = this
        var $node = $(node)
        var $edges = null
        if (includeEdges) {
            $edges = $retval
        }
        var names = this.findEdge($node.attr('data-name'), testEdge, $edges)
        for (var i in names) {
            var n = this._nodesByName[names[i]]
            if (!$retval.is(n)) {
                $retval.push(n)
                that.findLinked(n, includeEdges, testEdge, $retval)
            }
        }
    }

    GraphvizSvg.prototype.colorElement = function ($el, getColor) {
        var bg = this.$element.css('background')
        $el.find('polygon, ellipse, path').each(function () {
            var $this = $(this)
            var color = $this.data('graphviz.svg.color')
            if (color.fill && $this.prop('tagName') != 'path') {
                $this.attr('fill', getColor(color.fill, bg)) // don't set  fill if it's a path
            }
            if (color.stroke) {
                $this.attr('stroke', getColor(color.stroke, bg))
            }
        })
    }

    GraphvizSvg.prototype.restoreElement = function ($el) {
        $el.find('polygon, ellipse, path').each(function () {
            var $this = $(this)
            var color = $this.data('graphviz.svg.color')
            if (color.fill) {
                $this.attr('fill', color.fill) // don't set  fill if it's a path
            }
            if (color.stroke) {
                $this.attr('stroke', color.stroke)
            }
        })
    }


    // methods users can actually call
    GraphvizSvg.prototype.nodes = function () {
        return this.$nodes
    }

    GraphvizSvg.prototype.edges = function () {
        return this.$edges
    }

    GraphvizSvg.prototype.clusters = function () {
        return this.$clusters
    }

    GraphvizSvg.prototype.nodesByName = function () {
        return this._nodesByName
    }

    GraphvizSvg.prototype.edgesByName = function () {
        return this._edgesByName
    }

    GraphvizSvg.prototype.linkedTo = function (node, includeEdges) {
        var $retval = $()
        this.findLinked(node, includeEdges, function (nodeName, edgeName) {
            var other = null;
            var match = '->' + nodeName
            if (edgeName.endsWith(match)) {
                other = edgeName.substring(0, edgeName.length - match.length);
            }
            return other;
        }, $retval)
        return $retval
    }

    GraphvizSvg.prototype.linkedFrom = function (node, includeEdges) {
        var $retval = $()
        this.findLinked(node, includeEdges, function (nodeName, edgeName) {
            var other = null;
            var match = nodeName + '->'
            if (edgeName.startsWith(match)) {
                other = edgeName.substring(match.length);
            }
            return other;
        }, $retval)
        return $retval
    }


    GraphvizSvg.prototype.tooltip = function ($elements, show) {
        var options = this.options
        $elements.each(function () {
            $(this).children('a[title]').each(function () {
                if (show) {
                    options.tooltips.show.call(this)
                } else {
                    options.tooltips.hide.call(this)
                }
            })
        })
    }

    GraphvizSvg.prototype.bringToFront = function ($elements) {
        $elements.detach().appendTo(this.$graph)
    }

    GraphvizSvg.prototype.sendToBack = function ($elements) {
        if (this.$background.length) {
            $elements.insertAfter(this.$background)
        } else {
            $elements.detach().prependTo(this.$graph)
        }
    }

    GraphvizSvg.prototype.highlight = function ($nodesEdges, tooltips) {
        var that = this
        var options = this.options
        var $everything = this.$nodes.add(this.$edges)
        if ($nodesEdges && $nodesEdges.length > 0) {
            // create set of all other elements and dim them
            $everything.not($nodesEdges).each(function () {
                that.colorElement($(this), options.highlight.unselected)
                that.tooltip($(this))
            })
            $nodesEdges.each(function () {
                that.colorElement($(this), options.highlight.selected)
            })
            if (tooltips) {
                this.tooltip($nodesEdges, true)
            }
        } else {
            $everything.each(function () {
                that.restoreElement($(this))
            })
            this.tooltip($everything)
        }
    }

    GraphvizSvg.prototype.destroy = function () {
        var that = this
        this.hide(function () {
            that.$element.off('.' + that.type).removeData(that.type)
        })
    }


    // GRAPHVIZSVG PLUGIN DEFINITION
    // =============================

    function Plugin(option) {
        return this.each(function () {
            var $this = $(this)
            var data = $this.data('graphviz.svg')
            var options = typeof option == 'object' && option

            if (!data && /destroy/.test(option)) return
            if (!data) $this.data('graphviz.svg', (data = new GraphvizSvg(this, options)))
            if (typeof option == 'string') data[option]()
        })
    }

    var old = $.fn.graphviz

    $.fn.graphviz = Plugin
    $.fn.graphviz.Constructor = GraphvizSvg


    // GRAPHVIZ NO CONFLICT
    // ====================

    $.fn.graphviz.noConflict = function () {
        $.fn.graphviz = old
        return this
    }

}(jQuery)
