/*
 * jsPlumb
 * 
 * Title:jsPlumb 1.4.1
 * 
 * Provides a way to visually connect elements on an HTML page, using either SVG, Canvas
 * elements, or VML.  
 * 
 * This file contains the YUI3 adapter.
 *
 * Copyright (c) 2010 - 2013 Simon Porritt (http://jsplumb.org)
 * 
 * http://jsplumb.org
 * http://github.com/sporritt/jsplumb
 * http://code.google.com/p/jsplumb
 * 
 * Dual licensed under the MIT and GPL2 licenses.
 */

/**
 * addClass				adds a class to the given element
 * animate				calls the underlying library's animate functionality
 * appendElement		appends a child element to a parent element.
 * bind					binds some event to an element
 * dragEvents			a dictionary of event names
 * extend				extend some js object with another.  probably not overly necessary; jsPlumb could just do this internally.
 * getAttribute			gets some attribute from an element
 * getDragObject		gets the object that is being dragged, by extracting it from the arguments passed to a drag callback
 * getDragScope			gets the drag scope for a given element.
 * getElementObject		turns an id or dom element into an element object of the underlying library's type.
 * getOffset			gets an element's offset
 * getOriginalEvent     gets the original browser event from some wrapper event.
 * getScrollLeft		gets an element's scroll left.  TODO: is this actually used?  will it be?
 * getScrollTop			gets an element's scroll top.  TODO: is this actually used?  will it be?
 * getSize				gets an element's size.
 * getUIPosition		gets the position of some element that is currently being dragged, by extracting it from the arguments passed to a drag callback.
 * initDraggable		initializes an element to be draggable 
 * initDroppable		initializes an element to be droppable
 * isDragSupported		returns whether or not drag is supported for some element.
 * isDropSupported		returns whether or not drop is supported for some element.
 * removeClass			removes a class from a given element.
 * removeElement		removes some element completely from the DOM.
 * setAttribute			sets an attribute on some element.
 * setDraggable			sets whether or not some element should be draggable.
 * setDragScope			sets the drag scope for a given element.
 * setOffset			sets the offset of some element.
 */
(function() {
	
	if (!Array.prototype.indexOf) {
		Array.prototype.indexOf = function( v, b, s ) {
			for( var i = +b || 0, l = this.length; i < l; i++ ) {
	  			if( this[i]===v || s && this[i]==v ) { return i; }
	 		}
	 		return -1;
		};
	}
	
	var Y;
	
	YUI().use('node', 'dd', 'dd-constrain', 'anim', 'node-event-simulate', function(_Y) {
		Y = _Y;	
		Y.on("domready", function() { jsPlumb.init(); });
	});
	
	/**
	 * adds the given value to the given list, with the given scope. creates the scoped list
	 * if necessary.
	 * used by initDraggable and initDroppable.
	 */
	var _add = function(list, scope, value) {
		var l = list[scope];
		if (!l) {
			l = [];
			list[scope] = l;
		}
		l.push(value);
	},	
	ddEvents = [ "drag:mouseDown", "drag:afterMouseDown", "drag:mouseup",
	     "drag:align", "drag:removeHandle", "drag:addHandle", "drag:removeInvalid", "drag:addInvalid",
	     "drag:start", "drag:end", "drag:drag", "drag:over", "drag:enter",
	     "drag:exit", "drag:drophit", "drag:dropmiss", "drop:over", "drop:enter", "drop:exit", "drop:hit"	     	               
	],	
	animEvents = [ "tween" ],	
	/**
	 * helper function to curry callbacks for some element. 
	 */
	_wrapper = function(fn) {
		return function() {
			try {
				return fn.apply(this, arguments);
			}
			catch (e) { }
		};
	},	
	/**
	 * extracts options from the given options object, leaving out event handlers.
	 */
	_getDDOptions = function(options) {
		var o = {};
		for (var i in options) if (ddEvents.indexOf(i) == -1) o[i] = options[i];
		return o;
	},	
	/**
	 * attaches all event handlers found in options to the given dragdrop object, and registering
	 * the given el as the element of interest.
	 */
	_attachListeners = function(dd, options, eventList) {	
	    for (var ev in options) {
	    	if (eventList.indexOf(ev) != -1) {
	    		var w = _wrapper(options[ev]);
	    		dd.on(ev, w);
	    	}
	    }
	},
	_droppables = {},
	_droppableOptions = {},
	_draggablesByScope = {},
	_draggablesById = {},
	_droppableScopesById = {},
	_checkHover = function(el, entering) {
		if (el) {
			var id = el.get("id");
			if (id) {
				var options = _droppableOptions[id];
				if (options) {
					if (options['hoverClass']) {
						if (entering) el.addClass(options['hoverClass']);
						else el.removeClass(options['hoverClass']);
					}
				}
			}
		}
	},
	_lastDragObject = null,
	_extend = function(o1, o2) {
		for (var i in o2)
			o1[i] = o2[i];
		return o1;
	},
	_getAttribute = function(el, attributeId) {
		return el.getAttribute(attributeId);
	},
	_getElementObject = function(el) {
		if (el == null) return null;
		var eee = null;
        eee = typeof el == 'string' ? Y.one('#' + el) : el._node ? el : Y.one(el);        
        return eee;
	};
	
	jsPlumb.CurrentLibrary = {
			
		addClass : function(el, clazz) {
			jsPlumb.CurrentLibrary.getElementObject(el).addClass(clazz);
		},	
		
		/**
		 * animates the given element.
		 */
		animate : function(el, properties, options) {
			var o = _extend({node:el, to:properties}, options),			
				id = _getAttribute(el, "id");
			o["tween"] = jsPlumb.wrap(properties["tween"], function() {
				// TODO should use a current instance.
				jsPlumb.repaint(id);
			});
			var a = new Y.Anim(o);
			_attachListeners(a, o, animEvents);
			a.run();
		},
		
		appendElement : function(child, parent) {
			_getElementObject (parent).append(child);			
		},
		
		/**
		 * event binding wrapper.  
		 */
		bind : function(el, event, callback) {
			_getElementObject(el).on(event, callback);
		},
			
		dragEvents : {
			"start":"drag:start", "stop":"drag:end", "drag":"drag:drag", "step":"step",
			"over":"drop:enter", "out":"drop:exit", "drop":"drop:hit"
		},								
			
		extend : _extend,
		
		getAttribute : _getAttribute,
		
		getClientXY : function(eventObject) {
			return [eventObject.clientX, eventObject.clientY];
		},
		
		/**
		 * takes the args passed to an event function and returns you an object representing that which is being dragged.
		 */
		getDragObject : function(eventArgs) {
			// this is a workaround for the unfortunate fact that in YUI3, the 'drop:exit' event does
			// not contain a reference to the drag that just exited.  single-threaded js to the 
			// rescue: we'll just keep it for ourselves.
			if (eventArgs[0].drag) _lastDragObject = eventArgs[0].drag.el;
			return _lastDragObject;
		},
		
		getDragScope : function(el) {
			var id = jsPlumb.getId(el),
				dd = _draggablesById[id];
			return dd.scope;
		},

		getDropEvent : function(args) {
			return args[0];
		},
		
		getDropScope : function(el) {
			var id = jsPlumb.getId(el);
			return _droppableScopesById[id];
		},
		
		getDOMElement : function(el) { 			
			if (typeof(el) == "String") 
				return document.getElementById(el);
			else if (el._node) 
				return el._node;
			else return el;
		},
		
		getElementObject : _getElementObject,
		
		getOffset : function(el) {			
			var o = Y.DOM.getXY(el._node);
			return {left:o[0], top:o[1]};
		},

		getOriginalEvent : function(e) {
			return e._event;
		},
		
		getPageXY : function(eventObject) {
			return [eventObject.pageX, eventObject.pageY];
		},
		
		getParent : function(el) {
			return jsPlumb.CurrentLibrary.getElementObject(el).get("parentNode");
		},
		
		getScrollLeft : function(el) {
			return 0; 
		},
		
		getScrollTop : function(el) {
			return 0;
		},
		
		getSelector : function(context, spec) {
			var _convert = function(s) { return s && s ._nodes ? s._nodes : []; };
            
            if (arguments.length == 2) {            
                return _convert(jsPlumb.CurrentLibrary.getElementObject(context).all(spec));
            }
            else {
			     return _convert(Y.all(context));
            }            
		},
		
		getSize : function(el) {
			return [ el._node.offsetWidth, el._node.offsetHeight ];
		},

        getTagName : function(el) {
            var e = jsPlumb.CurrentLibrary.getElementObject(el);
            return e != null && e._node != null ? e._node.tagName : null;
        },
		
		getUIPosition : function(args, zoom) {
			zoom = zoom || 1;
			var n = args[0].currentTarget.el._node,
			o = Y.DOM.getXY(n);
			return {left:o[0] / zoom, top:o[1] / zoom};
		},		
		
		hasClass : function(el, clazz) {
			return el.hasClass(clazz);
		},
				
		initDraggable : function(el, options, isPlumbedComponent, _jsPlumb) {
			var _opts = _getDDOptions(options),
				id = _jsPlumb.getId(el);
			_opts.node = "#" + id;		
			var dd = new Y.DD.Drag(_opts), 
                containment = options.constrain2node || options.containment;
			dd.el = el;	
            
            if (containment) {
                dd.plug(Y.Plugin.DDConstrained, {
                    constrain2node: containment
                });
            }
			
			if (isPlumbedComponent) {
				var scope = options['scope'] || _jsPlumb.Defaults.Scope;
				dd.scope = scope;
				_add(_draggablesByScope, scope, dd);
			}
			
			_draggablesById[id] = dd;			
			
			_attachListeners(dd, options, ddEvents);
		},
		
		initDroppable : function(el, options) {
			var _opts = _getDDOptions(options),
				id = jsPlumb.getId(el);
			_opts.node = "#" + id;			
			var dd = new Y.DD.Drop(_opts);
			
			_droppableOptions[id] = options;
			
			options = _extend({}, options);
			var scope = options['scope'] || jsPlumb.Defaults.Scope;					
			_droppableScopesById[id] = scope;
			
			options["drop:enter"] = jsPlumb.wrap(options["drop:enter"], function(e) {
				if (e.drag.scope !== scope) return true;
				_checkHover(el, true);
			}, true);
			options["drop:exit"] = jsPlumb.wrap(options["drop:exit"], function(e) {
				_checkHover(el, false);
			});
			options["drop:hit"] = jsPlumb.wrap(options["drop:hit"], function(e) {
				if (e.drag.scope !== scope) return true;
				_checkHover(el, false);
			}, true);
			
			_attachListeners(dd, options, ddEvents);
		},
		
		isAlreadyDraggable : function(el) {
			el = _getElementObject(el);
			return el.hasClass("yui3-dd-draggable");
		},
		
		isDragSupported : function(el) { return true; },		
		isDropSupported : function(el) { return true; },										
		removeClass : function(el, clazz) { 
			jsPlumb.CurrentLibrary.getElementObject(el).removeClass(clazz); 
		},		
		removeElement : function(el) { _getElementObject(el).remove(); },
		
		setAttribute : function(el, attributeName, attributeValue) {
			el.setAttribute(attributeName, attributeValue);
		},

		setDragFilter : function(el, filter) {
			jsPlumb.log("NOT IMPLEMENTED: setDragFilter")
		},
		
		/**
		 * sets the draggable state for the given element
		 */
		setDraggable : function(el, draggable) {
			var id = jsPlumb.getId(el),
				dd = _draggablesById[id];
			if (dd) dd.set("lock", !draggable);
		},
		
		setDragScope : function(el, scope) {
			var id = jsPlumb.getId(el),
				dd = _draggablesById[id];
			if (dd) dd.scope = scope;
		},
		
		setOffset : function(el, o) {
			el = _getElementObject(el);
			el.set("top", o.top);
			el.set("left", o.left);
		},

        stopDrag : function() {
            Y.DD.DDM.stopDrag();
        },
		
		trigger : function(el, event, originalEvent) {
			originalEvent.stopPropagation();
			_getElementObject(el).simulate(event, {
				pageX:originalEvent.pageX, 
				pageY:originalEvent.pageY, 
				clientX:originalEvent.clientX, 
				clientY:originalEvent.clientY
			});			
		},
		
		/**
		 * event unbinding wrapper.  
		 */
		unbind : function(el, event, callback) {
			_getElementObject(el).detach(event, callback);
		}
	};				
})();