/**
 * @fileoverview
 * 
 * <p>Copyright (c) 2006, Yahoo Inc All rights reserved<br />
 * Copyright (c) 2006 Peter Michaux All rights reserved<br />
 * The file is not licenced for use or redistribution.</p>
 *
 * Some of this code is part of the Yahoo! UI dragdrop library
 * these lines of code are 
 * Copyright (c) 2006, Yahoo! Inc. All rights reserved.                                                                                                    
 * Code licensed under the BSD License:                                                                                                                    
 * http://developer.yahoo.net/yui/license.txt                                                                                                              
 * version: 0.10.0                                                                                                                                         
 * 
 * The remainder of this code is 
 * Copyright (c) 2006 Peter Michaux. All rights reserved.
 *
 * @author Yahoo! Inc. and Peter Michaux
 * @version 0.1 (June 2006)
 */

/**
 * @class
 * <p>An abstract base class for all PM.Clusters the user can drag. This class
 * provides methods for handle management, multiple selection,
 * listens for interesting moments
 * and calls the hook methods at the appropriate times.
 * This class really is a framework for draggable behavior.
 * In the constructor and interesting moment hooks of your subclass,
 * you use the handle 
 * and multiple select management methods provided here with
 * your visual repositioning code to implement your desired behavior.</p>
 *
 * <p>This
 * is a very flexible, lightweight approach to draggable behavior. In many 
 * draggable implementations you will
 * subclass a mammoth base class that can do everything the designer imagined:
 * default visual behavior, cloning, constrained motion etc. Probably you
 * don't need all this behavior
 * and will spend time turning most of it off. This will probably lead to
 * inefficient code that may run slowly. Also you will probably need
 * different behavior
 * than the designer imagined and may need to overide library methods to
 * get what you want. With PM.Draggable your subclass will only turn
 * <em>on</em> the behavior you need in the appropriate places.</p>
 *
 * <h4>handles</h4>
 * <p>A PM.Draggable instance is a PM.Cluster and so has a collection of 
 * PM.Dual instances. All that means is you have added certain HTML elements
 * to the draggable. Some of these elements can be handles. Handles are 
 * sensitive to mousedown events and can potentially initatiate a drag 
 * operation.</p>
 * 
 * <p>By default, an instance of PM.Draggable
 * will not have any handles; however, using the handle management methods
 * provided in this class, your subclass constructor can promote certain
 * Dual instances already in the cluster to handles or you can add new 
 * elements directly as handles.
 * For many implementation you will want to have all duals as handles. For this
 * you can simply call <code>this.promoteAllToHandle():</code> after you call
 * to the superclass constructor.</p>
 *
 * <p>You may wish to make an element inside a handle insensitive to mousedown
 * events. You can use the handle management methods provided in this class
 * to add and remove invalid elements by type (ie. tag name), id, or
 * class name. You can 
 * also promote and demote any duals in a PM.Draggable instance to or from
 * handle status. Any changes you make are effective on the 
 * next mousedown event.</p>
 * 
 * <p>There is one exception to the basic philosophy of no default behavior
 * in this class.
 * By default, all anchor elements, <code>&lt;a&gt;</code> tags, are
 * invalid handles. This is because links are meant to be followed, not to be
 * draggable handles.
 * If this almost always needed default behavior is a problem or ideologically
 * offensive, you can either reverse this in your subclass
 * constructor with <code>this.removeInvalidTagName("a")</code> after the call
 * to the superclass constructor or directly remove the five characters (<code>
 * A:"A"</code>)
 * responsible for this default in PM.Draggable constructor.</p>
 *
 * <h4>interesting moment hooks</h4>
 * <p>The interesting moment hooks fire in the following order and are
 * available for use in your subclasses. The "b4" hooks are called 
 * immediately before the "on" hooks. I recommend you only use the 
 * "on" hooks in your subclasses. The "b4" hooks could be reserved 
 * to alter the behavior of all PM.Draggable instances.</p>
 *
 * <ol>
 *  <li>b4MouseDown</li>
 *  <li>onMouseDown</li>
 *  <li>b4DragStart</li>
 *  <li>onDragStart</li>
 *  <li>b4Drag</li>
 *  <li>onDrag</li>
 *  <li>b4DragEnd</li>
 *  <li>onDragEnd</li>
 *  <li>b4MouseUp</li>
 *  <li>onMouseUp</li>
 * </ol>
 *
 * <h4>drag appearance and constrained motion</h4>
 * <p>This class does not implement any of the visuals that 
 * occur during a drag. You must subclass PM.Draggable
 * and use the interesting moment hook methods to complete
 * the necessary behavior for a user interface.
 * Likely you will cache positional information in onMouseDown, update
 * the positions in onDrag, and finish with something in onDragEnd.</p>
 *
 * <p>This class knows nothing about constrained motion because there doesn't 
 * exist one implementation for all constrained motion behavior. You can use
 * the interesting moment hooks in your subclass to add constrained motion
 * behavior. Likely cache some positional information in onMouseDown and
 * make sure moves are allowed or implement "snap-to" behavior in onDrag.</p>
 *
 * <h4>multiple selection and simultaneous dragging</h4>
 * <p>This class supplies some support methods for selection management of
 * draggables since these methods will be needed by many subclasses. These
 * methods allow for multiple selection and multiple simultaneous dragging
 * through your use of them in your subclass' interesting moment hooks.
 * Likely you will do selection logic in the onMouseDown and onMouseUp
 * hooks. (Constrained motion together with multiple selection may require
 * serious consideration.)</p>
 *
 * <p>Since this class does not implement any muliple selection behavior,
 * you will use the interesting moment hooks of the "leader" to have the other
 * selected items also move during or at the end of the drag.
 * The leader is the PM.Draggable instance that was
 * mousedowned to initiate the drag. The leader is the manager of the
 * entire drag. This leader concept allows different 
 * behavior for each instance, subclass, or subclass instance of PM.Draggable.
 * The leader method was chosen for efficient drag operation in both
 * proxy and non-proxy dragging behavior. (You could override the
 * mouseDownHooks, dragStartHooks etc methods to change from leader to
 * semi-leaderless behavior. For example, in mouseDownHooks you would looping
 * through all the selected PM.Draggable
 * instances and calling each's b4MouseDown and onMouseDown methods.
 * This would introduce knowledge about selection into this class.
 * This semi-leaderless behavior may not make sense with proxy draggable
 * behavior and could be a little slower with the extra method calls for
 * each selected draggable.)</p>
 *
 * <h4>targets</h4>
 * <p>This class knows nothing about PM.Target or even the concept of targets.
 * If you are using targets, likely your PM.Draggable subclass instances
 * will communicate with your PM.Target subclass or your own target class
 * in the interesting moment hooks of the drag leader.</p>
 *
 * <p>In case you are wondering, this class knows nothing about the
 * <a href="http://peter.michaux.ca/articles/2006/06/16/donut-dragdrop">
 * donut concept</a> and can be subclassed to make traditional 
 * draggables without a donut proxy. For example, traditional draggables
 * are useful for building slider inputs or moveable and resizable windows.
 * 
 * <p>This class can also be used to make 
 * <a href="http://peter.michaux.ca/articles/2006/06/16/donut-dragdrop">
 * a more traditional looping system</a> for detecting targets under the
 * cursor.
 * To do this you would check for targets in the onDrag hook of your 
 * PM.Draggable subclass. If you do this you loose many of the advantages
 * of the donut concept.</p>
 *
 * <h4>interaction groups</h4>
 * <p>This class knows nothing about groups because it's superclass, 
 * PM.Cluster, handles all the group managment.</p>
 * 
 * <p>If you are also using PM.Target, in your PM.Draggable subclasses you will
 * likely prime targets based on the selected draggables in the onMouseDown
 * hook method. Most likely you will just sent the selected draggables to 
 * PM.Target.primeTargets. Each target will then decide whether or not it will
 * prime itself so that it can accept the selected draggables on a drop.
 * You will also likely unprime all the targets in onMouseUp.</p>
 *
 * <h4>lack of caching</h4>
 * <p>Other than the mousedown event location for detecting a dragStart event,
 * this class does not cache
 * anything. This allows you to build fast draggable implementations
 * with a small code base.</p>
 *
 * <h4>Dependencies</h4>
 * PM.Object
 * PM.Dual
 * PM.Cluster
 * YAHOO.util.Event
 *
 * @constructor
 * @param {HTMLElement|String|Array} elements the HTML element the instance will
 * represent. For convenience, this parameter can alternately be 
 * the string id of the HTMLElement. It can also be an array with a mixture of
 * HTML elements and string ids.
 * @param {String|Array} groups the interaction group string or an array
 * of group strings that to which this draggable will belong.
 * @extends PM.Cluster
 */
PM.Draggable = function(elements, groups) {
  PM.Draggable.superclass.call(this, elements, groups);
  /**
   * A object used as an associative array to cache all handles
   * registered for this PM.Draggable instance. The property names are the
   * internalId's of each PM.Dual that is a handle. The property values
   * are the actual PM.Dual instances.
   * Do not modify this directly. Use the handle management methods instead.
   * @final
   */
  this.handles = {};
  /**
   * An object used as an associative array to store tag names
   * for invalid child elements of handles for this PM.Draggable instance.
   * The property names are the invalid tag names and the property values
   * are all true.
   * Do not  modify this directly use the handle management methods instead.
   * By default, anchor elements are invalid handle child elements.
   * @final
   */
  this.invalidHandleTagNames = {A:true};
  /**
   * An object used as an associative array to store the id attribute
   * for invalid child elements of handles for this PM.Draggable instance.
   * The property names are the invalid id attributes and the property values
   * are all true.
   * Do not modify this directly use the handle management methods instead.
   * @final
   */
  this.invalidHandleIds = {};
  /**
   * An object used as an associative array to store the class names
   * for invalid child elements of handles for this PM.Draggable instance.
   * The property names are the invalid class names and the property values
   * are all true.
   * Do not modify this directly use the handle management methods instead.
   * @final
   */
  this.invalidHandleClasses = {};
};
PM.extend(PM.Draggable, PM.Cluster);

// Multiple Select Management -------------------------------------------------

/**
 * An object used as an associative array to cache all currently selected
 * PM.Draggable instances. Do not modify this directly. Use the selection
 * management methods instead.
 * @final
 */
PM.Draggable.selected = {};

/**
 * Call this method to select this PM.Draggable instance.
 */
PM.Draggable.prototype.select = function() {
  this.selected = true;
  PM.Draggable.selected[this.internalId] = this;
};

/**
 * Call this method to unselect this PM.Draggable instance.
 */
PM.Draggable.prototype.unselect = function() {
  delete this.selected;
  delete PM.Draggable.selected[this.internalId];
};

/**
 * Call this method to unselect all currently selected PM.Draggable instances.
 */
PM.Draggable.unselectAll = function() {
  for (var i in PM.Draggable.selected) {
    PM.Draggable.selected[i].unselect(); 
  }
};

// Handle Management ----------------------------------------------------------

/**
 * Promote a PM.Dual instance that is already part of this PM.Draggable
 * instance to a handle. This dual element will have an mousedown listener
 * added.
 * @param {Object} dual The PM.Dual instance to promote to a handle. 
 */
PM.Draggable.prototype.promoteToHandle = function(dual) {
  if (!this.handles[dual.internalId]) {
    YAHOO.util.Event.addListener(dual.element, "mousedown",
                                 this.handleMouseDown, this, true);
    this.handles[dual.internalId] = dual;
  }
};

/**
 * Calls PM.Draggable.prototype.promoteToHandle for each PM.Dual
 * already a part of this PM.Draggable instance.
 */
PM.Draggable.prototype.promoteAllToHandle = function() {
  for (var d in this.duals) {
    this.promoteToHandle(this.duals[d]);
  }
};

/**
 * Adds a PM.Dual instance as a handle to this PM.Draggable instance.
 * @param {PM.Dual|HTMLElement|String} dual A PM.Dual instance,
 * to be added as a handle to this PM.Draggable instance. The dual
 * parameter can alternately be an HTMLElement or the string id
 * of an element.
 */
PM.Draggable.prototype.addHandle = function(dual) {
  // TODO this could take an array and rename this method
  // addHandles or addHandlesById
  dual = this.addDual(dual, true);
  this.promoteToHandle(dual);
};
// TODO PM.Draggable.prototype.addHandlesByClassName
// TODO PM.Draggable.prototype.addHandlesByTagName

/**
 * Demotes a handle already part of this PM.Draggable instance
 * to a plain PM.Dual by removing the mousedown listener.
 * @param {Object} The PM.Dual instance that is currently a handle. 
 */
PM.Draggable.prototype.demoteToDual = function(handle) {
  YAHOO.util.Event.removeListener(handle.element, "mousedown",
                                  this.handleMouseDown);
  delete this.handles[handle.internalId];
};

/**
 * Calls PM.Draggable.prototype.demoteToDual for each handle
 * in this PM.Draggalbe instance.
 */
PM.Draggable.prototype.demoteAllToDual = function() {
  for (var h in this.handles) {
    this.demoteToDual(this.handles[h]);
  }
};

/**
 * Add a tag name to the list of invalid child elements types.
 * All child elements of all handles of this PM.Draggable instance
 * with this tag name will not behave as handles.
 * @param {String} tagName The tag name of the child elements that 
 * should not behave as handles. 
 */
PM.Draggable.prototype.addInvalidHandleTagName = function(tagName) {
  this.invalidHandleTagNames[tagName.toUpperCase()] = true;
};

/**
 * Add the id to the list of invalid child element ids. The children
 * of handles with these ids will not behave as handles.
 * @param {String} The id of the element that should not behave
 * as a handle. 
 */
PM.Draggable.prototype.addInvalidHandleId = function(id) {
  this.invalidHandleIds[id] = true;
};

/**
 * Add a class name to the list of invalid child element class names.
 * The children of handles with any of these class names will not behave
 * as handles.
 * @param {String} cssClass The class name of the elements that should
 * not behave as handles. 
 */
PM.Draggable.prototype.addInvalidHandleClass = function(cssClass) {
  this.invalidHandleClasses[cssClass] = true;
};

/**
 * Reverses the action of PM.Draggable.prototype.addInvalidHandleTagName
 * @param {String} tagName 
 */
PM.Draggable.prototype.removeInvalidHandleTagName = function(tagName) {
  delete this.invalidHandleTagNames[tagName.toUpperCase()];
};

/**
 * Reverses the action of PM.Draggable.prototype.addInvalidHandleId
 * @param {String} id 
 */
PM.Draggable.prototype.removeInvalidHandleId = function(id) {
  delete this.invalidHandleIds[id];
};

/**
 * Reverses the action of PM.Draggable.prototype.addInvalidHandleClass
 * @param {String} cssClass
 */
PM.Draggable.prototype.removeInvalidHandleClass = function(cssClass) {
  delete this.invalidHandleClasses[cssClass];
};

/**
 * Checks if a DOM node is a valid handle child element.
 * @param {Node} node The DOM node to be checked.
 * @type boolean
 * @return Returns true if the node is a valid handle child. Returns
 * false if the node is not a valid handle child.
 */
PM.Draggable.prototype.isValidHandleChild = function(node) {
  if (node.nodeName === "#text") {node = node.parentNode;}
  if (this.invalidHandleTagNames[node.tagName.toUpperCase()] || 
      this.invalidHandleIds[node.id]) {
        return false;
  }
  for (var c in this.invalidHandleClasses) {
    if (YAHOO.util.Dom.hasClass(node, c)){
      return false;
    }
  }
  return true;
};

// Automatic scrolling support methods ----------------------------------------

// TODO docs
PM.Draggable.prototype.setScrollBox = function(t,l,b,r) {
  this.scrollBox = {top:t, left:l, bottom:b, right:r};
};

// Listen for interesting moments and call to hooks ---------------------------

/**
 * @private
 */
PM.Draggable.prototype.buttonOk = function(e) {
  var button = e.which || e.button;
  if (button > 1) {
    return false;
  }
  return true;
};

/**
 * @private
 */
PM.Draggable.prototype.isLegitMouseDown = function(e) {
  if (!this.buttonOk(e) ||
       this.isLocked() ||
      !this.isValidHandleChild(YAHOO.util.Event.getTarget(e))) {
    return false;
  }
  return true;
};

/**
 * The number of milliseconds that must expire after a mousedown
 * event when the mouse button remains down
 * before a dragStart event is fired. Default value is 2000 ms or 2 s.
 * If either
 * PM.Draggable.clickTimeThresh or PM.Draggable.clickPixelThresh
 * is surpassed then a dragStart event is fired.
 *
 * @type {integer}
 */
PM.Draggable.clickTimeThresh = 2000;
/**
 * The number of pixels the cursor must move after a mousedown
 * event occurs before a dragStart event is fired. Default is 3 pixels.
 * If either
 * PM.Draggable.clickTimeThresh or PM.Draggable.clickPixelThresh
 * is surpassed then a dragStart event is fired.
 * 
 * @type {integer}
 */
PM.Draggable.clickPixelThresh = 3;

YAHOO.util.Event.addListener(document, "mousedown",
                             function(e){PM.Draggable.handled=false;});

/**
 * @private
 */
PM.Draggable.prototype.addListeners = function() {
  YAHOO.util.Event.addListener(document, "mousemove",
                           PM.Draggable.prototype.handleMouseMove, this, true);
  YAHOO.util.Event.addListener(document, "mouseup",
                           PM.Draggable.prototype.handleMouseUp, this, true);
};

/**
 * @private
 */
PM.Draggable.prototype.removeListeners = function() {
  YAHOO.util.Event.removeListener(document, "mousemove",
                                  PM.Draggable.prototype.handleMouseMove);
  YAHOO.util.Event.removeListener(document, "mouseup",
                                  PM.Draggable.prototype.handleMouseUp);
};


/**
 * A cache of the mousedown x position on the last legitimate
 * mousedown event that occured on a PM.Draggable instance.
 * @type {integer}
 * @final
 */
PM.Draggable.startPageX = null;
/**
 * A cache of the mousedown y position on the last legitimate
 * mousedown event that occured on a PM.Draggable instance.
 * @type {integer}
 * @final
 */
PM.Draggable.startPageY = null;

/**
 * @private
 */
PM.Draggable.prototype.handleMouseDown = function(e) {
  if (PM.Draggable.handled || !this.isLegitMouseDown(e)) {return;}
  /**
   * @private
   */
  PM.Draggable.handled = true;

  this.mouseDownHooks(e);

  PM.Draggable.startPageX = YAHOO.util.Event.getPageX(e);
  PM.Draggable.startPageY = YAHOO.util.Event.getPageY(e);

  delete PM.Draggable.dragLeader;
  
  this.addListeners();
  
  // Use a closure so handleDragStart executes in correct scope
  var thisC = this;
  this.clickTimeout = setTimeout(function() {thisC.handleDragStart(e);},
                                 PM.Draggable.clickTimeThresh);    
  YAHOO.util.Event.preventDefault(e);
};


/**
 * @private
 */
PM.Draggable.prototype.handleDragStart = function(e) {
  clearTimeout(this.clickTimeout);
  // TODO document dragLeader
  PM.Draggable.dragLeader = this;
  this.dragStartHooks(e);
};

/**
 * TODO document
 * @ignore
 */
PM.Draggable.wasMouseUpOutside = function(e) {
  // TODO firefox mouseup outside of page boundary doesn't work
  
  // check for IE mouseup outside of page boundary
  // TODO should be checking for which button also?
  if (YAHOO.util.Event.isIE && !e.button) {
      return true;
  }
  return false;
};

/**
 * @private
 */
PM.Draggable.prototype.handleMouseMove = function(e) {

  if (PM.Draggable.wasMouseUpOutside(e)) {
      // TODO really need preventDefault?
      YAHOO.util.Event.preventDefault(e);
      this.handleMouseUp(e);
      return;
  }

  if (!PM.Draggable.dragLeader) {
    var diffX = Math.abs(PM.Draggable.startPageX -
                         YAHOO.util.Event.getPageX(e));
    var diffY = Math.abs(PM.Draggable.startPageY -
                         YAHOO.util.Event.getPageY(e));
    if (diffX > PM.Draggable.clickPixelThresh || 
        diffY > PM.Draggable.clickPixelThresh) {
      // TODO remove this line
      //this.handleDragStart(PM.Draggable.startPageX, PM.Draggable.startPageY);
      this.handleDragStart(e);
    }
  }

  if (PM.Draggable.dragLeader) {
    this.dragHooks(e);
  }
  YAHOO.util.Event.preventDefault(e);
};

/**
 * @private
 */
PM.Draggable.prototype.handleDragEnd = function(e) {
  this.dragEndHooks(e);
};

/**
 * @private
 */
PM.Draggable.prototype.handleMouseUp = function(e) {
  // I changed this next line
  // clearTimeout(PM.Draggable.clickTimeout);
  clearTimeout(this.clickTimeout);
  this.removeListeners();
  if (PM.Draggable.dragLeader) {
    this.handleDragEnd(e);
  }
  this.mouseUpHooks(e);
};


// Hook stub methods ----------------------------------------------------------

/**
 * Interesting moment hook method called immediately before onMouseDown.
 * Don't use this method. Instead use the onMouseDown hook in your subclasses.
 * @param {Object} e The event object.
 */
PM.Draggable.prototype.b4MouseDown = function(e) { /* override this */ };
/**
 * Interesting moment hook method called on a mousedown event on this instance.
 * Override this method in your subclasses.
 * @param {Object} e The event object.
 */
PM.Draggable.prototype.onMouseDown = function(e) { /* override this */ };
/**
 * @private
 */
PM.Draggable.prototype.mouseDownHooks = function(e) {
  this.b4MouseDown(e);
  this.onMouseDown(e);
};

/**
 * Interesting moment hook method called immediately before onDragStart.
 * Don't use this method. Instead use the onDragStart hook in your subclasses.
 * @param {Object} e The event object.
 */
PM.Draggable.prototype.b4DragStart = function(e) { /* override this */ };
/**
 * Interesting moment hook method called upon initiation of a drag
 * initiated by this instance.
 * A drag is initiated after PM.Draggable.clickTimeThresh milliseconds
 * elapse after a mousedown event or the cursor moves
 * PM.Draggable.clickPixelThresh pixels after a mousedown event.
 * Override this method in your subclasses.
 * @param {Object} e The event object.
 */
PM.Draggable.prototype.onDragStart = function(e) { /* override this */ };
/**
 * @private
 */
PM.Draggable.prototype.dragStartHooks = function(e) {
  this.b4DragStart(e);
  this.onDragStart(e);
};

/**
 * Interesting moment hook method called immediately before onDrag.
 * Don't use this method. Instead use the onDrag hook in your subclasses.
 * @param {Object} e The event object
 */
PM.Draggable.prototype.b4Drag = function(e) { /* override this */ };
/**
 * Interesting moment hook method called for every mousemove event
 * during a drag initiated by this instance.
 * Try to optimize the code you write for this method
 * to avoid a slow or jerky drag apperance.
 * Override this method in your subclasses.
 * @param {Object} e The event object.
 */
PM.Draggable.prototype.onDrag = function(e) { /* override this */ };
/**
 * @private
 */
PM.Draggable.prototype.dragHooks = function(e) {
  this.b4Drag(e);
  this.onDrag(e);
};

/**
 * Interesting moment hook method called immediately before onDragEnd.
 * Don't use this method. Instead use the onDragEnd hook in your subclasses.
 * @param {Object} e The event object.
 */
PM.Draggable.prototype.b4DragEnd = function(e) { /* override this */ };
/**
 * Interesting moment hook method called when a drag ends for a drag initiated
 * by this instance.
 * This method is only called on a mouseup event if a drag was
 * truly initiated. That is,
 * PM.Draggable.clickTimeThresh or PM.Draggable.clickPixelThresh were
 * surpassed and therefore a startDrag event occured.
 * Override this method in your subclasses.
 * @param {Object} e The event object.
 */
PM.Draggable.prototype.onDragEnd = function(e) { /* override this */ };
/**
 * @private
 */
PM.Draggable.prototype.dragEndHooks = function(e) {
  this.b4DragEnd(e);
  this.onDragEnd(e);
};

/**
 * Interesting moment hook method called immediately before onMouseUp.
 * Don't use this method. Instead use the onMouseUp hook in your subclasses.
 * @param {Object} e The event object.
 */
PM.Draggable.prototype.b4MouseUp = function(e) { /* override this */ };
/**
 * Interesting moment hook method called when a mouseup occurs after a mousedown
 * occured on this draggable. This method is called whether or not a drag was
 * initiated.
 * Override this method in your subclasses.
 * @param {Object} e The event object.
 */
PM.Draggable.prototype.onMouseUp = function(e) { /* override this */ };
/**
 * @private
 */
PM.Draggable.prototype.mouseUpHooks = function(e) {
  this.b4MouseUp(e);
  this.onMouseUp(e);
};