(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) {
            this.DOUBLE_CLICK_INTERVAL = 300;
            this.HANDLE_SIZE = 18;
            this.ARROW_END_ID = "ARROW_END_ID";

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

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

            this._$canvas = $('<canvas></canvas>');
            this._$canvas.css("top", 0);

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

            this._resizeCanvas();

            this.initContainerEvents();

        },
        initContainerEvents: function () {
            this._cy.on("resize", this._resizeCanvas.bind(this));
            /*$(window).bind('resize', this._resizeCanvas.bind(this));
             $(window).bind('resize', this._resizeCanvas.bind(this));*/

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

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

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

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

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

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


        },
        initNodeEvents: function (){

            this._$canvas.on("mousedown", this._mouseDown.bind(this));
            this._$canvas.on("mousemove", this._mouseMove.bind(this));
            this._$canvas.on('mouseup', this._mouseUp.bind(this));

            this._cy.on('tapdragover', 'node', this._mouseOver.bind(this));
            this._cy.on('tapdragout', 'node', this._mouseOut.bind(this));

            

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

            this._cy.on("mousedown", "node", function () {
                if(!this._tagMode) {
                    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._$container.on('mouseover', function (e) {
            //     if (this._hover) {
            //         this._mouseOver({cyTarget: this._hover});
            //     }
            // }.bind(this));

            this._cy.on('tagstart', function(){
                this._tagMode = true;
            }.bind(this));

            this._cy.on('tagend', function(){
                this._tagMode = false;
            }.bind(this))

        },
        registerHandle: function (handle) {
            
            if (handle.imageUrl) {

                var base_image = new Image();
                base_image.src = handle.imageUrl;
                base_image.onload = function() {
                    handle.image = base_image;
                  };
            }
            
            this._handles[handle.type] = this._handles[handle.type] || [];
            this._handles[handle.type] = handle;


        },
        _showHandles: function (target, handleType) {

            if(!handleType){
                handleType = 'add-edge'; //ie, CanvasHandleTypes.ADD_EDGE, which is the default
            }
            this._drawHandle(this._handles[handleType], target);

        },
        _clear: function () {

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

            target.data().handleType = handle.type;
            var position = this._getHandlePosition(target);
            var handleSize = this.HANDLE_SIZE * this._cy.zoom();
            this._ctx.clearRect(position.x, position.y, handleSize, handleSize);
            
            if (handle.image) {
                this._ctx.drawImage(handle.image, position.x, position.y, handleSize, handleSize);
            }
        },
        _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) {
            if(this._tagMode){
                return;
            }
            //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();

        },
        _mouseUp: function (e) {
            if (this._hover) {
                if(this._tagMode){
                    if(this._hitTestHandles(e))
                    this._cy.trigger('handletagclick', {
                        nodeId: this._hover.data().id
                    });
                    //this._hover = null;
                } else if (this._hit && this._dragging) {
                    //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: "default"
                        }
                    });
                    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) {
                    this._hit = this._hitTestHandles(e);
                    if (this._hit) {
                        this._cy.trigger('handlemouseover', {
                            node: this._hover
                        });
                        $("body").css("cursor", "pointer");
                    } else {
                        this._cy.trigger('handlemouseout', {
                            node: this._hover
                        });
                        if(!this._tagMode){
                            this._showHandles(this._hover);
                        }
                        $("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._hover = e.cyTarget;
                }
            } else {
                this._hover = e.cyTarget;
                if (!this._tagMode) {
                    this._showHandles(this._hover);
                }
            }
        },
        _mouseOut: function (e) {
            if(!this._dragging) {
                if (!this.permanentHandle) {
                    this._clear();
                }
                this._cy.trigger('handlemouseout', {
                    node: this._hover
                });
            }
            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 position = this._getHandlePosition(this._hover);
                var renderedHandleSize = this.HANDLE_SIZE * this._cy.zoom(); //actual number of pixels that handle uses.
                if (VectorMath.distance(position, mousePoisition) < renderedHandleSize) {
                    var handleType = this._hover.data().handleType;
                    return {
                        handle: this._handles[handleType]
                    };
                }
            //}
        },
        _getHandlePosition: function (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 = position.x + width / 2 - renderedHandleSize;
            var ypos = position.y - height / 2;

            return {x: xpos, y: ypos};
        },
        _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
            };
        },
        _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._tagMode) {
                this._cy.trigger('canvasredraw');
            }
        }
    });

})(this);