dojo.provide("bl.dnd");
dojo.require("dojo.dom");
dojo.require("dojo.event");
dojo.require("dojo.event.browser");
dojo.require("dojo.html");
dojo.require("dojo.lang.array");
dojo.require("dojo.lang.extras");
dojo.require("dojo.style");
dojo.require("bl.event");

dojo.declare("bl.dnd.Source", null, 
  function(node, type, options) {
    node = dojo.byId(node);
    if(!node) return;

    this.domNode = node;
    this.dragNode = node;
    
    if(bl.dnd.manager) bl.dnd.manager.registerSource(this);

    this.type = type||this.domNode.nodeName.toLowerCase();

    bl.lang.setOptions(this, { dragClass: null }, options);
  }, 
  {
    onDragStart: function() {
      var dragObj = new bl.dnd.DragObject(this.dragNode, this.type);
      if(this.dragClass) { dragObj.dragClass = this.dragClass; }
      return dragObj;
    },

    onDragEnd: function() {},
    
    unregister: function() { bl.dnd.manager.unregisterSource(this); },

    reregister: function() { bl.dnd.manager.registerSource(this); },

    setDragHandle: function(node){
      this.unregister();
      node = dojo.byId(node);
      this.domNode = node;
      this.reregister();
    },

    setDragTarget: function(node){
      this.domNode = node;
    }
  });

dojo.declare("bl.dnd.DragObject", null,
  function(node, type, options) {
    this.domNode = dojo.byId(node);
    if(!this.domNode) return;
    this.type = type;
    bl.lang.setOptions(this, {
      attachPoint: document.body,
      dragClass: null,
      opacity: 0.5,	
      createIframe: true,	
      disableX: false,
      disableY: false}, options);
  },
  {
    createDragNode: function() {
      var node = this.domNode.cloneNode(true);
      if(this.dragClass) { dojo.html.addClass(node, this.dragClass); }
      if(this.opacity < 1) { dojo.style.setOpacity(node, this.opacity); }
      if(dojo.render.html.ie && this.createIframe){
        var outer = document.createElement("div");
        outer.appendChild(node);
        this.bgIframe = new dojo.html.BackgroundIframe();
        this.bgIframe.size([0,0,dojo.style.getOuterWidth(node),dojo.style.getOuterHeight(node)]);
        outer.appendChild(this.bgIframe.iframe);
        node = outer;
      }
      node.style.zIndex = 500;
      return node;
    },

    onDragStart: function(e){
      dojo.html.clearSelection();

      this.scrollOffset = {
        top: dojo.html.getScrollTop(), // document.documentElement.scrollTop,
        left: dojo.html.getScrollLeft() // document.documentElement.scrollLeft
      };

      this.dragOffset = this.getDragOffset(e);
      this.dragClone = this.createDragNode();
      this.dragStartPosition = dojo.html.getAbsolutePosition(this.domNode, true);
      if ((this.domNode.parentNode.nodeName.toLowerCase() == 'body') || (dojo.style.getComputedStyle(this.domNode.parentNode,"position") == "static")) {
        this.parentPosition = {top: 0, left: 0};
      } else {
        this.parentPosition = {top: dojo.style.getAbsoluteY(this.domNode.parentNode, true),
          left: dojo.style.getAbsoluteX(this.domNode.parentNode,true)};
      }

      // set up for dragging
      with(this.dragClone.style){
        position = "absolute";
        top = this.dragOffset.top + e.pageY + "px";
        left = this.dragOffset.left + e.pageX + "px";
      }
      this.attachPoint.appendChild(this.dragClone);
      this.cloneWidth = dojo.html.getMarginBoxWidth(this.dragClone);
      this.viewportWidth = dojo.html.getViewportWidth();

      $bl.log("cloneWidth: %o, vp: %o", this.cloneWidth, this.viewportWidth);
    },

    getDragOffset: function(e) {
      return { top: -3, left: -3 };
    },

    /** Moves the node to follow the mouse */
    onDragMove: function(e){
      var x = this.dragOffset.left + e.pageX;
      var y = this.dragOffset.top + e.pageY;
      if(x < 0) x = 0;
      if(y < 0) y = 0;
      if(x + this.cloneWidth > this.viewportWidth) x = this.viewportWidth - this.cloneWidth;
      if(!this.disableY) { this.dragClone.style.top = y + "px"; }
      if(!this.disableX) { this.dragClone.style.left = x + "px"; }
    },

    /**
     * If the drag operation returned a success we reomve the clone of
     * ourself from the original position. If the drag operation returned
     * failure we slide back over to where we came from and end the operation
     * with a little grace.
     */
    onDragEnd: function(e){
      var self = this;
      switch(e.dragStatus) {
        case "dropSuccess":
          $bl.log("dropSuccess %o, evt: %o", this, e);
          var anim = dojo.lfx.fadeOut(this.dragClone, 200, null, function() {
            dojo.html.removeNode(self.dragClone);
            self.dragClone = null;
          });
          anim.play();
          break;
        case "dropFailure":
          // animate
          $bl.log("dropFailure: %o, evt: %o", this, e);
          var end = [ this.dragStartPosition.y, this.dragStartPosition.x ];
          var anim = dojo.lfx.slideTo(this.dragClone, end, 250, dojo.lfx.easeOut);
          dojo.event.connect(anim, "onEnd", function (e) {
            // pause for a second (not literally) and disappear
            dojo.lang.setTimeout(function() {
                dojo.html.removeNode(self.dragClone);
                // Allow drag clone to be gc'ed
                self.dragClone = null;
              },
              200);
          });
          anim.play();
          break;

      }

      // shortly the browser will fire an onClick() event,
      // but since this was really a drag, just squelch it
      dojo.event.connect(this.domNode, "onclick", this, "squelchOnClick");
    },

    squelchOnClick: function(e){
      // squelch this onClick() event because it's the result of a drag (it's not a real click)
      e.preventDefault();

      // but if a real click comes along, allow it
      dojo.event.disconnect(this.domNode, "onclick", this, "squelchOnClick");
    }
  });


dojo.declare("bl.dnd.Target", null, 
  function(node, types, priority) {
    if (arguments.length == 0) { return; }
    node = dojo.byId(node);
    if(!node) return;
    
    bl.dnd.manager.registerTarget(this);
    this.domNode = dojo.byId(node);
    if(types && dojo.lang.isString(types)) {
      types = [types];
    }
    this.acceptedTypes = types || [];
    this.priority = priority || 0;
  },
  {
	acceptsType: function(type){
		if(!dojo.lang.inArray(this.acceptedTypes, "*")){ // wildcard
			if(!dojo.lang.inArray(this.acceptedTypes, type)) { return false; }
		}
		return true;
	},

	accepts: function(dragObject){
		if(!dojo.lang.inArray(this.acceptedTypes, "*")){ // wildcard
			if (!dojo.lang.inArray(this.acceptedTypes, dragObject.type)) { return false; }
			
		}
		return true;
	},

	onDragOver: function(e){
		if(!this.accepts(e.dragObject)){ return false; }

		// cache the positions of the child nodes
		this.childBoxes = [];
		for (var i = 0, child; i < this.domNode.childNodes.length; i++) {
			child = this.domNode.childNodes[i];
			if (child.nodeType != dojo.dom.ELEMENT_NODE) { continue; }
            var dims = dojo.style.toCoordinateArray(child, true);
			this.childBoxes.push({top: dims.y, bottom: dims.y + dims.h,
				left: dims.x, right: dims.x+dims.w, node: child});
		}
		if(djConfig.vizDnD) {
			this._vizBoxes = [];
			var _this = this;
			dojo.lang.forEach(this.childBoxes, function(c) {
				var d = document.createElement("div");
				d.style.position = "absolute";
				d.style.top = c.top + "px";
				d.style.left = c.left + "px";
				var width = c.right - c.left;
				d.style.width = ((width > 1) ? (width - 2) : 0) + "px";
				var height = c.bottom - c.top;
				d.style.height = ((height > 1) ? (height - 2) : 0) + "px";
				d.style.backgroundColor = "#ff0";
				d.style.borderColor = "#000";
				d.style.borderWidth = "1px";
				d.style.borderStyle = "solid";
				dojo.style.setOpacity(d, 0.25);
				document.body.appendChild(d);
				_this._vizBoxes.push(d);
			});
		}
		// TODO: use dummy node

		return true;
	},

	onDragMove: function(e){
		var i = this._getNodeUnderMouse(e);

		if(!this.dropIndicator){
			this.createDropIndicator();
		}
		var before = null;

		if(i < 0) {
			if(this.childBoxes.length) {
				before = (dojo.html.gravity(this.childBoxes[0].node, e) & dojo.html.gravity.NORTH);
			} else {
				before = true;
			}
		} else {
			var child = this.childBoxes[i];
			before = (dojo.html.gravity(child.node, e) & dojo.html.gravity.NORTH);
		}
		this.placeIndicator(e, e.dragObject, i, before);

		if(!dojo.html.hasParent(this.dropIndicator)) {
			document.body.appendChild(this.dropIndicator);
		}
	},

	onDragOut: function(e) {
		if(this.dropIndicator) {
			dojo.dom.removeNode(this.dropIndicator);
			delete this.dropIndicator;
		}
		if(djConfig.vizDnD && this._vizBoxes) {
			dojo.lang.forEach(this._vizBoxes, function(n) { dojo.dom.removeNode(n); });
		}
	},

	onDropStart: function() {},

	/**
	 * Inserts the DragObject as a child of this node relative to the
	 * position of the mouse.
	 *
	 * @return true if the DragObject was inserted, false otherwise
	 */
	onDrop: function(e){
    //$bl.log("drop begin %o", e);
		this.onDragOut(e);

		var i = this._getNodeUnderMouse(e);

		if (i < 0) {
			if (this.childBoxes.length) {
				if (dojo.html.gravity(this.childBoxes[0].node, e) & dojo.html.gravity.NORTH) {
					return this.insert(e, this.childBoxes[0].node, "before");
				} else {
					return this.insert(e, this.childBoxes[this.childBoxes.length-1].node, "after");
				}
			}
      //$bl.log("drop append %o", e);
			return this.insert(e, this.domNode, "append");
		}

		var child = this.childBoxes[i];
		if (dojo.html.gravity(child.node, e) & dojo.html.gravity.NORTH) {
      //$bl.log("drop before %o", e);
			return this.insert(e, child.node, "before");
		} else {
      //$bl.log("drop after %o", e);
			return this.insert(e, child.node, "after");
		}
	},

	onDropEnd: function() {},


	createDropIndicator: function() {
		this.dropIndicator = document.createElement("div");
		dojo.html.addClass(this.dropIndicator, "dropIndicator");
		with (this.dropIndicator.style) {
			width = dojo.style.getContentWidth(this.domNode) + "px";
			left = dojo.style.getAbsoluteX(this.domNode) + "px";
		}
	},


	_getNodeUnderMouse: function(e){
		var offset = dojo.html.getScrollOffset();
		var mousex = e.pageX || e.clientX + offset.x;
		var mousey = e.pageY || e.clientY + offset.y;

		// find the child
		for (var i = 0, child; i < this.childBoxes.length; i++) {
			with (this.childBoxes[i]) {
				if (mousex >= left && mousex <= right &&
					mousey >= top && mousey <= bottom) { return i; }
			}
		}

		return -1;
	},


	placeIndicator: function(e, dragObject, boxIndex, before) {
		with(this.dropIndicator.style){
			if (boxIndex < 0) {
				if (this.childBoxes.length) {
					top = (before ? this.childBoxes[0].top
						: this.childBoxes[this.childBoxes.length - 1].bottom) + "px";
				} else {
					top = dojo.style.getAbsoluteY(this.domNode) + "px";
				}
			} else {
				var child = this.childBoxes[boxIndex];
				top = (before ? child.top : child.bottom) + "px";
				left = dojo.style.getAbsoluteX(child.node) + "px";
				width = dojo.style.getContentWidth(child.node) + "px";
			}
		}
	},


	insert: function(e, refNode, position) {
		var node = e.dragObject.domNode;

		if(position == "before") {
			return dojo.html.insertBefore(node, refNode);
		} else if(position == "after") {
			return dojo.html.insertAfter(node, refNode);
		} else if(position == "append") {
			refNode.appendChild(node);
			return true;
		}

		return false;
	}
  });




dojo.declare("bl.dnd.Manager", null, 
  function() {
    this.disabled = false;
    this.nestedTargets = true;
    this.dsCounter = 0;
    this.dsPrefix = "blds";
    this.dropTargetDimensions = [];
    this.currentDropTarget = null;
    this.previousDropTarget = null;
    this._dragTriggered = false;
    this.selectedSource = null;
    this.dragObject = null;
    this.currentX = null;
    this.currentY = null;
    this.lastX = null;
    this.lastY = null;
    this.mouseDownX = null;
    this.mouseDownY = null;
    this.threshold = 7;
    this.dropAcceptable = false;
    this.dropTargets = [];
    this.dragSources = [];
    this.scrollOffset = null;
  }, 
  {
	cancelEvent: function(e) { 
      e.stopPropagation(); 
      e.preventDefault();
    },

	// method over-rides
	registerSource: function(ds){
		if(ds["domNode"]){
			var dp = this.dsPrefix;
			var dpIdx = dp+"Idx_"+(this.dsCounter++);
			ds.dragSourceId = dpIdx;
			this.dragSources[dpIdx] = ds;
			ds.domNode.setAttribute(dp, dpIdx);
		}
	},

	unregisterSource: function(ds){
		if (ds["domNode"]){

			var dp = this.dsPrefix;
			var dpIdx = ds.dragSourceId;
			delete ds.dragSourceId;
			delete this.dragSources[dpIdx];
			ds.domNode.setAttribute(dp, null);
		}
	},

	registerTarget: function(dt){
		this.dropTargets.push(dt);
	},

	unregisterTarget: function(dt){
		var index = dojo.lang.find(this.dropTargets, dt, true);
		if (index>=0) {
			this.dropTargets.splice(index, 1);
		}
	},

	getDragSource: function(e){
		var tn = e.target;
		if(tn === document.body){ return; }
		var ta = dojo.html.getAttribute(tn, this.dsPrefix);
		while((!ta)&&(tn)){
			tn = tn.parentNode;
			if((!tn)||(tn === document.body)){ return; }
			ta = dojo.html.getAttribute(tn, this.dsPrefix);
		}
		return this.dragSources[ta];
	},

	onKeyDown: function(evt){
		var k = evt.keyCode || evt.charCode;		
		if(this._dragTriggered && k == evt.KEY_ESCAPE) {
			this._cancelDrag(evt);
			dojo.event.browser.stopEvent(evt);
		}
	},

	onMouseDown: function(e){
    //$bl.log("mousedown %o", e);
		if(this.disabled) { /*$bl.log("mousedown ignored, disabled");*/ return; }

		
		// only begin on left click
		if(!bl.event.isLeftMouseClick(e)) { /*$bl.log("mousedown ignored, not left click");*/ return; } 

		this.mouseDownX = e.clientX;
		this.mouseDownY = e.clientY;
		
		var target = e.target.nodeType == dojo.dom.TEXT_NODE ?
			e.target.parentNode : e.target;

		// do not start drag involvement if the user is interacting with
		// a form element.
		if(bl.alg.isInputElement(target)) { /*$bl.log("mousedown ignored, target input element");*/ return; }

		// find a selection object, if one is a parent of the source node
		var ds = this.getDragSource(e);
		if(!ds){ /*$bl.log("mousedown ignored, no source");*/ return; }
		this.selectedSource = ds;
		
		// Enable dragging of links in firefox.
		// WARNING: preventing the default action on all mousedown events
		// prevents user interaction with the contents.
		// if it's an A tag, prevent the default action from happening

		//if(dojo.html.isTag(target, "a")) e.preventDefault();
    //$bl.log("mousedown, preventing");
		this.cancelEvent(e); // pulled by blowery in favor of code above

		dojo.event.connect(document, "onmousemove", this, "onMouseMove");

	},

	onMouseUp: function(e){
    //$bl.log("mouseup %o", e);
    if(this.disabled) return;

    if(!this.selectedSource) { /*$bl.log("mouseup, ignored, no source %o", e);*/ return; }
                
		this.mouseDownX = null;
		this.mouseDownY = null;
		this._dragTriggered = false;
	  //$bl.log("mouseup, preventing");	
		//this.cancelEvent(e);
		e.dragSource = this.dragSource;
		
		if(this.currentDropTarget) this.currentDropTarget.onDropStart();
		
		var ret = null;
		
		if(this.currentDropTarget) {
			e.dragObject = this.dragObject;
			e.dropTarget = this.currentDropTarget.domNode;
						
			if(this.dropAcceptable) {
				ret = this.currentDropTarget.onDrop(e);
			} else {
				 this.currentDropTarget.onDragOut(e);
			}
		}

		e.dragStatus = (this.dropAcceptable && ret) ? "dropSuccess" : "dropFailure";
				
		if(this.dragObject) this.dragObject.onDragEnd(e);


		this.selectedSource = null;
		this.dragObject = null;
		this.dragSource = null;
		if(this.currentDropTarget) this.currentDropTarget.onDropEnd();

		dojo.event.disconnect(document, "onmousemove", this, "onMouseMove");
		this.currentDropTarget = null;
	  //$bl.log("mouseup finished");	
	},

	onMouseMove: function(e){
    ////$bl.log("mousemove %o", e);
    if(this.disabled) return;
		
		// if we've got some sources, but no drag objects, we need to send
		// onDragStart to all the right parties and get things lined up for
		// drop target detection
		if(this.selectedSource && !this.dragObject){
			if(!this._dragTriggered){
				this._dragTriggered = (this._dragStartDistance(e.clientX, e.clientY) > this.threshold);
				if(!this._dragTriggered){ return; }
			}
			var dx = e.clientX - this.mouseDownX;
			var dy = e.clientY - this.mouseDownY;
			
			this.dragSource = this.selectedSource;
			var tdo = this.selectedSource.onDragStart(e);
			if(tdo) {
				tdo.onDragStart(e);
				//tdo.dragOffset.top += dy;
				//tdo.dragOffset.left += dx;
				this.dragObject = tdo;
 			} else {
				this._cancelDrag(e);
        return;
			}

			/* clean previous drop target in dragStart */
			this.previousDropTarget = null;
			this.scrollOffset = dojo.html.getScrollOffset();
			this.cacheTargetLocations();
		}

    //this.cancelEvent(e);
	  var currentOffset = dojo.html.getScrollOffset();
    if(currentOffset[0] != this.scrollOffset[0] || currentOffset[1] != this.scrollOffset[1]) {
		 this.scrollOffset = currentOffset;
		  this.cacheTargetLocations();
    }
		
    if(this.dragObject) this.dragObject.onDragMove(e);

		// FIXME: need to fix the event object!
		// see if we can find a better drop target
		e.dragObject = this.dragObject;
		var bestBox = this.findBestTarget(e);

		if(bestBox == null){

			if(this.currentDropTarget){
				this.currentDropTarget.onDragOut(e);
				this.previousDropTarget = this.currentDropTarget;
				this.currentDropTarget = null;
			}
			this.dropAcceptable = false;
			return;
		}

		if(this.currentDropTarget !== bestBox.target){
			if(this.currentDropTarget){
				this.previousDropTarget = this.currentDropTarget;
				this.currentDropTarget.onDragOut(e);
			}
			this.currentDropTarget = bestBox.target;
			this.dropAcceptable = this.currentDropTarget.onDragOver(e);

		} else {
			if(this.dropAcceptable){
				this.currentDropTarget.onDragMove(e);
			}
		}
    //$bl.log("mousemove finished %o", e);
	},

	_cancelDrag: function(e) {
		this.mouseDownX = null;
		this.mouseDownY = null;
		if(this._dragTriggered) {
			this._dragTriggered = false;
			if(this.currentDropTarget) {
				this.currentDropTarget.onDragOut(e);
				this.currentDropTarget = null;
			}
			e.dragStatus = "dropFailure";
			if(this.dragObject) this.dragObject.onDragEnd(e);
			this.dragObject = null;
			dojo.event.disconnect(document, "onmousemove", this, "onMouseMove");
		}
		this.selectedSource = null;
		this.dragSource = null;
		

	},

	findBestTarget: function(e) {
		var _this = this;
		var boxes = [];
		
		dojo.lang.forEach(this.dropTargetDimensions, function(tmpDA) {
			if(_this.isInsideBox(e, tmpDA) && tmpDA[2].accepts(_this.dragObject)){
				boxes.push({ "target": tmpDA[2], "points": tmpDA });
				if(!_this.nestedTargets){ return "break"; }
			}
		});

		boxes.sort(function(a,b) { return b.target.priority - a.target.priority; });

		return boxes[0];
	},

	isInsideBox: function(e, coords){
		var offset = dojo.html.getScrollOffset();
		var x = e.pageX || e.clientX + offset.x;
		var y = e.pageY || e.clientY + offset.y;
		
		if(	(x > coords[0][0])&&
			(x < coords[1][0])&&
			(y > coords[0][1])&&
			(y < coords[1][1]) ){
			return true;
		}
		return false;
	},

	_dragStartDistance: function(x, y){
		if((!this.mouseDownX)||(!this.mouseDownY)){
			return;
		}
		var dx = Math.abs(x-this.mouseDownX);
		var dx2 = dx*dx;
		var dy = Math.abs(y-this.mouseDownY);
		var dy2 = dy*dy;
		return parseInt(Math.sqrt(dx2+dy2), 10);
	},

	cacheTargetLocations: function() {
		var _this = this;
                //dojo.debug("caching locations");

		this.dropTargetDimensions = [];
		dojo.lang.forEach(this.dropTargets, function(tempTarget){
			var tn = tempTarget.domNode;
			if(!tn){ return; }
            var dims = dojo.style.toCoordinateArray(tn, true);
			var ttx = dojo.style.getAbsoluteX(tn, false);
			var tty = dojo.style.getAbsoluteY(tn, false);
			var tw = ttx + dojo.style.getInnerWidth(tn);
			var th = tty + dojo.style.getInnerHeight(tn);
			_this.dropTargetDimensions.push([
				[dims.x, dims.y],	// upper-left
				// lower-right
				[dims.x+dims.w, dims.y+dims.h ],
				tempTarget
			]);
      //dojo.debug("cache loc: [" + ttx + "," + tty + "] [" + tw + "," + th + "]");
			//dojo.debug("Cached for "+tempTarget)
		});

		//dojo.debug("cached locations");
	}
  });

bl.dnd.init = function() {
	if(bl.dnd.manager != null) return;
	bl.dnd.manager = new bl.dnd.Manager();
	var dm = bl.dnd.manager;
	// set up event handlers on the document
	dojo.event.connect(document, 	"onkeydown", 	dm, "onKeyDown");
	dojo.event.connect(document, 	"onmousedown",	dm, "onMouseDown");
	dojo.event.connect(document, 	"onmouseup",	dm, "onMouseUp");
};
