(function (scope) {
    var Class = function (param1, param2) {

        var extend, mixins, definition;
        if (param2) {     //two parameters passed, first is extends, second definition object
            extend = Array.isArray(param1) ? param1[0] : param1;
            mixins = Array.isArray(param1) ? param1.slice(1) : null;
            definition = param2;
        } else {      //only one parameter passed => no extend, only definition
            extend = null;
            definition = param1;
        }


        var Definition = definition.hasOwnProperty("constructor") ? definition.constructor : function () {
        };

        Definition.prototype = Object.create(extend ? extend.prototype : null);
        var propertiesObject = definition.propertiesObject ? definition.propertiesObject : {};
        if (mixins) {
            var i, i2;
            for (i in mixins) {
                for (i2 in mixins[i].prototype) {
                    Definition.prototype[i2] = mixins[i].prototype[i2];
                }
                for (var i2 in mixins[i].prototype.propertiesObject) {
                    propertiesObject[i2] = mixins[i].prototype.propertiesObject[i2];
                }
            }
        }

        Definition.prototype.propertiesObject = propertiesObject;

        Object.defineProperties(Definition.prototype, propertiesObject);

        for (var key in definition) {
            if (definition.hasOwnProperty(key)) {
                Definition.prototype[key] = definition[key];
            }
        }

        Definition.prototype.constructor = Definition;

        return Definition;
    };


    var Interface = function (properties) {
        this.properties = properties;
    };

    var InterfaceException = function (message) {
        this.name = "InterfaceException";
        this.message = message || "";
    };

    InterfaceException.prototype = new Error();

    Interface.prototype.implements = function (target) {
        for (var i in this.properties) {
            if (target[this.properties[i]] == undefined) {
                throw new InterfaceException("Missing property " + this.properties[i]);
            }
        }
        return true;
    };

    Interface.prototype.doesImplement = function (target) {
        for (var i in this.properties) {
            if (target[this.properties[i]] === undefined) {
                return false;
            }
        }
        return true;
    };

    var VectorMath = {
        distance: function (vector1, vector2) {
            return Math.sqrt(Math.pow(vector1.x - vector2.x, 2) + Math.pow(vector1.y - vector2.y, 2));
        }
    };

    var EventDispatcher = Class({
        constructor: function () {
            this.events = {};
        },
        on: function (name, listener, context) {
            this.events[name] = this.events[name] ? this.events[name] : [];
            this.events[name].push({
                listener: listener,
                context: context
            })
        },
        once: function (name, listener, context) {
            this.off(name, listener, context);
            this.on(name, listener, context);
        },
        off: function (name, listener, context) {
            //no event with this name registered? => finish
            if (!this.events[name]) {
                return;
            }
            if (listener) {		//searching only for certains listeners
                for (var i in this.events[name]) {
                    if (this.events[name][i].listener === listener) {
                        if (!context || this.events[name][i].context === context) {
                            this.events[name].splice(i, 1);
                        }
                    }
                }
            } else {
                delete this.events[name];
            }
        },
        trigger: function (name) {
            var listeners = this.events[name];

            for (var i in listeners) {
                listeners[i].listener.apply(listeners[i].context, Array.prototype.slice.call(arguments, 1));
            }
        }
    });

    exports.CytoscapeEdgeEditation = Class({

        init: function (cy, handleSize) {
            this.DOUBLE_CLICK_INTERVAL = 300;
            this.HANDLE_SIZE = handleSize ? handleSize : 5;
            this.ARROW_END_ID = "ARROW_END_ID";

            this._handles = {};
            this._dragging = false;
            this._hover = null;


            this._cy = cy;
            this._$container = $(cy.container());

            this._cy.on('mouseover tap', 'node', this._mouseOver.bind(this));
            this._cy.on('mouseout', 'node', this._mouseOut.bind(this));

            this._$container.on('mouseout', function (e) {
                if (this.permanentHandle) {
                    return;
                }

                this._clear();
            }.bind(this));

            this._$container.on('mouseover', function (e) {
                if (this._hover) {
                    this._mouseOver({cyTarget: this._hover});
                }
            }.bind(this));

            this._cy.on("select", "node", this._redraw.bind(this))

            this._cy.on("mousedown", "node", function () {
                this._nodeClicked = true;
            }.bind(this));

            this._cy.on("mouseup", "node", function () {
                this._nodeClicked = false;
            }.bind(this));

            this._cy.on("remove", "node", function () {
                this._hover = false;
                this._clear();
            }.bind(this));

            this._cy.on('showhandle', function (cy, target) {
                this.permanentHandle = true;
                this._showHandles(target);
            }.bind(this));

            this._cy.on('hidehandles', this._hideHandles.bind(this));

            this._cy.bind('zoom pan', this._redraw.bind(this));


            this._$canvas = $('<canvas></canvas>');
            this._$canvas.css("top", 0);
            this._$canvas.on("mousedown", this._mouseDown.bind(this));
            this._$canvas.on("mousemove", this._mouseMove.bind(this));

            this._ctx = this._$canvas[0].getContext('2d');
            this._$container.children("div").append(this._$canvas);

            $(window).bind('mouseup', this._mouseUp.bind(this));

            /*$(window).bind('resize', this._resizeCanvas.bind(this));
             $(window).bind('resize', this._resizeCanvas.bind(this));*/

            this._cy.on("resize", this._resizeCanvas.bind(this));

            this._$container.bind('resize', function () {
                this._resizeCanvas();
            }.bind(this));

            this._resizeCanvas();



        },
        registerHandle: function (handle) {
            if (handle.nodeTypeNames) {
                for (var i in handle.nodeTypeNames) {
                    var nodeTypeName = handle.nodeTypeNames[i];
                    this._handles[nodeTypeName] = this._handles[nodeTypeName] || [];
                    this._handles[nodeTypeName].push(handle);
                }
            } else {
                this._handles["*"] = this._handles["*"] || [];
                this._handles["*"].push(handle);
            }

        },
        _showHandles: function (target) {
            var nodeTypeName = target.data().type;
            if (nodeTypeName) {

                var handles = this._handles[nodeTypeName] ? this._handles[nodeTypeName] : this._handles["*"];

                for (var i in handles) {
                    if (handles[i].type != null) {
                        this._drawHandle(handles[i], target);
                    }
                }
            }

        },
        _clear: function () {

            var w = this._$container.width();
            var h = this._$container.height();
            this._ctx.clearRect(0, 0, w, h);
        },
        _drawHandle: function (handle, target) {

            var position = this._getHandlePosition(handle, target);
            var handleSize = this.HANDLE_SIZE * this._cy.zoom();
            
            this._ctx.beginPath();

            if (handle.imageUrl) {
                var base_image = new Image();
                base_image.src = handle.imageUrl;
                this._ctx.drawImage(base_image, position.x, position.y, handleSize, handleSize);
            } else {
                this._ctx.arc(position.x, position.y, this.HANDLE_SIZE, 0, 2 * Math.PI, false);
                this._ctx.fillStyle = handle.color;
                this._ctx.strokeStyle = "white";
                this._ctx.lineWidth = 0;
                this._ctx.fill();
                this._ctx.stroke();
            }

        },
        _drawArrow: function (fromNode, toPosition, handle) {
            var toNode;
            if (this._hover) {
                toNode = this._hover;
            } else {
                if (!this._arrowEnd) {
                    this._arrowEnd = this._cy.add({
                        group: "nodes",
                        data: {
                            "id": this.ARROW_END_ID,
                            "position": { x: 150, y: 150 }
                        }
                    });

                    this._arrowEnd.css({
                        "opacity": 0,
                        'width': 0.0001,
                        'height': 0.0001
                    });                   
                }

                this._arrowEnd.renderedPosition(toPosition);
                toNode = this._arrowEnd;
            }


            if (this._edge) {
                this._edge.remove();
            }

            this._edge = this._cy.add({
                group: "edges",
                data: {
                    id: "edge",
                    source: fromNode.id(),
                    target: toNode.id(),
                    type: 'temporary-link'
                },
                css: $.extend(
                    this._getEdgeCSSByHandle(handle),
                    {opacity: 0.5}
                )
            });

        },
        _clearArrow: function () {
            if (this._edge) {
                this._edge.remove();
                this._edge = null;
            }

            if (this._arrowEnd) {
                this._arrowEnd.remove();
                this._arrowEnd = null;
            }
        },
        _resizeCanvas: function () {
            this._$canvas
                .attr('height', this._$container.height())
                .attr('width', this._$container.width())
                .css({
                    'position': 'absolute',
                    'z-index': '999'
                });
        },
        _mouseDown: function (e) {
            this._hit = this._hitTestHandles(e);

            if (this._hit) {
                this._lastClick = Date.now();
                this._dragging = this._hover;
                this._hover = null;
                e.stopImmediatePropagation();
            }
        },
        _hideHandles: function () {
            this.permanentHandle = false;
            this._clear();

            if(this._hover){
                this._showHandles(this._hover);
            }
        },
        _mouseUp: function () {
            if (this._hover) {
                if (this._hit) {
                    //check if custom listener was passed, if so trigger it and do not add edge
                    var listeners = this._cy._private.listeners;
                    for (var i = 0; i < listeners.length; i++) {
                        if (listeners[i].type === 'addedgemouseup') {
                            this._cy.trigger('addedgemouseup', {
                                source: this._dragging,
                                target: this._hover,
                                edge: this._edge
                            });
                            var that = this;
                            setTimeout(function () {
                                that._dragging = false;
                                that._clearArrow();
                                that._hit = null;
                            }, 0);


                            return;
                        }
                    }

                    var edgeToRemove = this._checkSingleEdge(this._hit.handle, this._dragging);
                    if (edgeToRemove) {
                        this._cy.remove("#" + edgeToRemove.id());
                    }
                    var edge = this._cy.add({
                        data: {
                            source: this._dragging.id(),
                            target: this._hover.id(),
                            type: this._hit.handle.type
                        }
                    });
                    this._initEdgeEvents(edge);
                }
            }
            this._cy.trigger('handlemouseout', {
                node: this._hover
            });
            $("body").css("cursor", "inherit");
            this._dragging = false;
            this._clearArrow();
        },
        _mouseMove: function (e) {
            if (this._hover) {
                if (!this._dragging) {
                    var hit = this._hitTestHandles(e);
                    if (hit) {
                        this._cy.trigger('handlemouseover', {
                            node: this._hover
                        });
                        $("body").css("cursor", "pointer");

                    } else {
                        this._cy.trigger('handlemouseout', {
                            node: this._hover
                        });
                        $("body").css("cursor", "inherit");
                    }
                }
            } else {
                $("body").css("cursor", "inherit");
            }

            if (this._dragging && this._hit.handle) {
                this._drawArrow(this._dragging, this._getRelativePosition(e), this._hit.handle);
            }

            if (this._nodeClicked) {
                this._clear();
            }
        },
        _mouseOver: function (e) {

            if (this._dragging) {
                if ( (e.cyTarget.id() != this._dragging.id()) && e.cyTarget.data().allowConnection || this._hit.handle.allowLoop) {
                    this._hover = e.cyTarget;
                }
            } else {
                this._hover = e.cyTarget;
                this._showHandles(this._hover);
            }
        },
        _mouseOut: function (e) {
            if(!this._dragging) {
                if (this.permanentHandle) {
                    return;
                }

                this._clear();
            }
            this._hover = null;
        },
        _removeEdge: function (edge) {
            edge.off("mousedown");
            this._cy.remove("#" + edge.id());
        },
        _initEdgeEvents: function (edge) {
            var self = this;
            edge.on("mousedown", function () {
                if (self.__lastClick && Date.now() - self.__lastClick < self.DOUBLE_CLICK_INTERVAL) {
                    self._removeEdge(this);
                }
                self.__lastClick = Date.now();
            })
        },
        _hitTestHandles: function (e) {
            var mousePoisition = this._getRelativePosition(e);

            if (this._hover) {
                var nodeTypeName = this._hover.data().type;
                if (nodeTypeName) {
                    var handles = this._handles[nodeTypeName] ? this._handles[nodeTypeName] : this._handles["*"];

                    for (var i in handles) {
                        var handle = handles[i];

                        var position = this._getHandlePosition(handle, this._hover);
                        var renderedHandleSize = this.HANDLE_SIZE * this._cy.zoom(); //actual number of pixels that handle uses.
                        if (VectorMath.distance(position, mousePoisition) < renderedHandleSize) {
                            return {
                                handle: handle,
                                position: position
                            };
                        }
                    }
                }
            }
        },
        _getHandlePosition: function (handle, target) { //returns the upper left point at which to begin drawing the handle
            var position = target.renderedPosition();
            var width = target.renderedWidth();
            var height = target.renderedHeight();
            var renderedHandleSize = this.HANDLE_SIZE * this._cy.zoom(); //actual number of pixels that handle will use.
            var xpos = null;
            var ypos = null;

            switch (handle.positionX) {
                case "left":
                    xpos = position.x - width / 2;
                    break;
                case "right": //position.x is the exact center of the node. Need to add half the width to get to the right edge. Then, subtract renderedHandleSize to get handle position
                    xpos = position.x + width / 2 - renderedHandleSize;
                    break;
                case "center":
                    xpos = position.x;
                    break;
            }

            switch (handle.positionY) {
                case "top":
                    ypos = position.y - height / 2;
                    break;
                case "center":
                    ypos = position.y;
                    break;
                case "bottom":
                    ypos = position.y + height / 2;
                    break;
            }

            //Determine if handle will be too big and require offset to prevent it from covering too much of the node icon (in which case, move it over by 1/2 the renderedHandleSize, so half the handle overlaps).
            //Need to use target.width(), which is the size of the node, unrelated to rendered size/zoom
            var offsetX = (target.width() < 30) ? renderedHandleSize / 2 : 0; 
            var offsetY = (target.height() < 30) ? renderedHandleSize /2 : 0;
            return {x: xpos + offsetX, y: ypos - offsetY};
        },
        _getEdgeCSSByHandle: function (handle) {
            var color = handle.lineColor ? handle.lineColor : handle.color;
            return {
                "line-color": color,
                "target-arrow-color": color,
                "line-style": handle.lineStyle? handle.lineStyle: 'solid',
                "width": handle.width? handle.width : 3
            };
        },
        _getHandleByType: function (type) {
            for (var i in this._handles) {
                var byNodeType = this._handles[i];
                for (var i2 in byNodeType) {
                    var handle = byNodeType[i2];
                    if (handle.type == type) {
                        return handle;
                    }
                }
            }
        },
        _getRelativePosition: function (e) {
            var containerPosition = this._$container.offset();
            return {
                x: e.pageX - containerPosition.left,
                y: e.pageY - containerPosition.top
            }
        },
        _checkSingleEdge: function (handle, node) {

            if (handle.noMultigraph) {
                var edges = this._cy.edges("[source='" + this._hover.id() + "'][target='" + node.id() + "'],[source='" + node.id() + "'][target='" + this._hover.id() + "']");

                for (var i = 0; i < edges.length; i++) {
                    return edges[i];
                }
            } else {

                if (handle.single == false) {
                    return;
                }
                var edges = this._cy.edges("[source='" + node.id() + "']");

                for (var i = 0; i < edges.length; i++) {
                    if (edges[i].data()["type"] == handle.type) {
                        return edges[i];
                    }
                }
            }
        },
        _redraw: function () {
            this._clear();
            if (this._hover) {
                this._showHandles(this._hover);
            }
        }
    });

})(this);