/**
 * @fileoverview
 * 
 * <p>Copyright (c) 2006 Peter Michaux All rights reserved<br />
 * All rights reserved <br />
 * This file is not licensed for use or redistribution.</p>
 *
 * @author Peter Michaux
 * @version 0.1 (June 2006)
 */

/**
 * @class
 * <p>An abstract base class for adding target behavior to a collection of
 * HTML elements. A PM.Target instance is sensitive to mouseevents
 * above any of it's collection of PM.Dual instances. You will likely
 * prime one or more
 * targets
 * when a drag operation begins so the target's interesting moment hook
 * methods are called when the cursor enters, moves over, or leaves the target
 * or a mouseup (ie. drop) occurs over the target.</p>
 *
 * <p>PM.Targets uses native browser mouse events to detect mouse activity.
 * This means you cannot have any elements directly below the cursor during
 * drag operations. This allows for fast dragging operations and easy 
 * handling of overlapping and nested targets.
 * This requires creative visual behavior 
 * in your PM.Draggable subclasses during the drag operation for a pleasing
 * user interface. With enough creativity you can create a wide variety of
 * good visual user experiences. For more see my
 * <a href="http://peter.michaux.ca/articles/2006/06/16/donut-dragdrop">
 * donut dragdrop blog article.</a></p>
 *
 * <p>Like PM.Draggable, this class offers your subclasses a framework for
 * implement target behavior through the subclasses constructor and
 * interesting moment hook methods.</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>willPrime</li>
 *  <li>b4Prime</li>
 *  <li>onPrime</li>
 *  <li>b4DragEnter</li>
 *  <li>onDragEnter</li>
 *  <li>b4DragLeaveOrDrop</li>
 *  <li>onDragLeaveOrDrop</li>
 *  <li>b4DragLeave</li>
 *  <li>onDragLeave</li>
 *  <li>b4Drop</li>
 *  <li>onDrop</li>
 *  <li>b4Unprime</li>
 *  <li>onUnprime</li>
 * </ol>
 *
 * <h4>priming and the willPrime hook method</h4>
 * <p>The willPrime hook method in the above list is important and requires
 * special consideration. At the beginning of a drag operation with multiple
 * selected 
 * draggables, a target must decide if it will react to the dragged items
 * and call the dragEnter, dragOver, drop, dragLeave 
 * interesting moment hooks.
 * If the selected items belong to a variety of interaction groups there may
 * be special logic required to determine if a target will prime itself.</p>
 *
 * <p>When you call
 * <code>PM.Target.primeTargets(PM.Draggable.selected)</code> in your
 * PM.Draggable subclass' onMouseDown hook method, the PM.Target class will
 * start
 * the priming process. Only unprimed, unlocked targets with at least one group
 * in the union of groups of the selected draggables have potential for priming.
 * The willPrime hook method of these targets is called and
 * will receive the PM.Draggable.selected object which has each selected
 * draggable as a property value.
 * Inside willPrime each target can then decide if it will react to these 
 * selected draggables by returning true or if it will not react by returning
 * false. You may want to use the PM.Cluster.groupsUnion and
 * PM.Cluster.groupsIntersection methods in your sublclass' willPrime
 * hook methods. Do not include any other code in willPrime. Use onPrime instead.</p>
 *
 * <p>A stub method for willPrime. The PM.Target.prototype.willPrime must be
 * defined and must have a return value. The default return value in this stub
 * is true which will generate "union" behavior. If the target has one group
 * in common with one of the selected draggables then the target will prime itself.
 * This is likely the desired behavior when only one draggable is selected
 * and dragged.
 * Surely this will not be the desired behavior in many situations but this
 * default behavior is the most compact and potentially useful in some situations.
 * Had the default return value been set to false it would likely never 
 * have never been useful taken an extra character.</p>
 *
 * <h4>first responder and nested and overlapping targets</h4>
 * <p>Nested and overlapping targets have caused problems for other
 * dragdrop implementations. Both situations are handled well with
 * PM.Target. The user expects that the target "on top" of the visible
 * html elements will be the target that responds to the mouse event.
 * During the mouse event bubble up the first target encountered will
 * claim first responder status and will have its appropriate event handler
 * called for the mouse event. This implementation is DRY
 * (don't repeat yourself) because
 * the target order is already stored in the DOM tree. Repeating this
 * order information in the JavaScript would be slower and vulnerable
 * to buggy code. This easy handling of nested and overlapping targets
 * is thanks to the use of native browser events.</p>
 *
 * <h4>interaction groups caching</h4>
 * <p>To implement faster dragdrop behavior when priming,
 * this class caches which PM.Target
 * instances belong to which groups. However, this all happens behind the scenes
 * and your calls to addToGroup work just as they do for instances of PM.Cluster
 * and PM.Target. You do not have to do anything extra or special.</p>
 *
 * <h4>lack of geographical caching</h4>
 * <p>Because native browser events are used, there is no need to cache any
 * geographical (ie. positional or regional) target 
 * information at the beginning of 
 * drag operations. In many cases, this allows for faster dragdrop
 * implementations than looping-type dragdrop implementations. This lack
 * of caching is also
 * appealing by avoiding potential bugs in cache management code.</p>
 *
 * <h4>Dependencies</h4>
 * PM.Object
 * PM.Dual
 * PM.Cluster
 * YAHOO.util.Dom
 * 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.Target = function(ids, groups) {
  PM.Target.superclass.call(this, ids, groups);
  YAHOO.util.Event.addListener();
  // TODO don't really need to be listening for this all the time
  // only when a drag is in process. But all targets must be listening
  // not just the primed ones. This is because of nested and overlapping targets.
  for (var d in this.duals) {
    YAHOO.util.Event.addListener(this.duals[d].element, "mousemove",
                                 this.handleMousemove, this, true);
  }
};
PM.extend(PM.Target, PM.Cluster);

// group caching to be used with priming --------------------------------------

/**
 * Add a new interaction group membership to this target. This method overrides
 * PM.Cluster.prototype.addToGroup to enable additional group caching at the 
 * target level.
 * @param {String} group The group to add to this target.
 * @see PM.Cluster#addToGroup
 */
PM.Target.prototype.addToGroup = function(group) {
  PM.Target.superproto.addToGroup.call(this, group);
  PM.Target._addToGroup(this, group);
};

/**
 * @ignore
 */
PM.Target.prototype.removeFromGroup = function(sGroup) {
  // TODO must implement this to override removeFromGroup in Cluster
  alert("implement PM.Target.prototype.removeFromGroup");
};

/**
 * An object used as an associative array to cache the target members of
 * all groups.
 * Each property name is the name of a group.
 * Each property values is an array of targets belonging to the particular
 * group. Do
 * not modify this directly. Use PM.Target.prototype.addToGroup to manage
 * group membership of a particular target instance.
 * @type Object
 * @final
 */
PM.Target.groups = {};

/**
 * @private
 */
PM.Target._addToGroup = function(targ, sGroup) {
  if (!PM.Target.groups[sGroup]) {
    PM.Target.groups[sGroup] = {};
  }
  PM.Target.groups[sGroup][targ.internalId] = targ;
};

// interesting moments and hook calls -----------------------------------------

/**
 * @private
 */
PM.Target.prototype.addListeners = function() {
  for (var d in this.duals) {
    var dual = this.duals[d];
    YAHOO.util.Event.addListener(dual.element, "mouseup",
                                 this.handleDrop, this, true);
    YAHOO.util.Event.addListener(dual.element, "mousemove",
                                 this.handleDragOver, this, true);
  }
};

/**
 * @private
 */
PM.Target.prototype.removeListeners = function() {
  for (var d in this.duals) {
    var dual = this.duals[d];
    YAHOO.util.Event.removeListener(dual.element, "mouseup",
                                    this.handleDrop);
    YAHOO.util.Event.removeListener(dual.element, "mousemove",
                                    this.handleDragOver);
  }
};

/**
 * @private
 */
PM.Target.current = null;

/**
 * @private
 */
PM.Target.prototype.handleDragEnter = function(e) {
  if (PM.Target.current === this) {return;}
  
  if (PM.Target.current) {
    PM.Target.current.handleDragLeave(e);
  }
  PM.Target.current = this;
 
  this.dragEnterHooks(e);
  this.addListeners();
};

/**
 * @private
 */
PM.Target.firstResponder = null;
// TODO should only do this listening when something is dragged
YAHOO.util.Event.addListener(document, "mousemove", 
  function(e){
    // TODO could move some of the following to handleMouseMove. Good idea for maintainability?
    if (PM.Target.firstResponder === null || !PM.Target.firstResponder.primed){
      if (PM.Target.current) {
        PM.Target.current.handleDragLeave(e);
      }
    } else if (PM.Target.firstResponder.primed) {
      if (PM.Target.current !== PM.Target.firstResponder) {
        PM.Target.firstResponder.handleDragEnter(e);        
      }
    }
    PM.Target.firstResponder = null;
  }
);

/**
 * @private
 */
PM.Target.prototype.handleMousemove = function(e) {
  if (PM.Target.firstResponder === null){
    PM.Target.firstResponder = this;    
  }
};

/**
 * @private
 */
PM.Target.prototype.handleDrop = function(e) {
  this.dropHooks(e);
  this.removeListeners();
  PM.Target.current = null;
};

/**
 * @private
 */
PM.Target.prototype.handleDragLeave = function(e) {
  this.dragLeaveHooks(e);
  this.removeListeners();
  PM.Target.current = null;
  logMessage("doing drag leave");
};

/**
 * @private
 */
PM.Target.prototype.handleDragOver = function(e) {
  this.dragOverHooks(e);
};

// priming --------------------------------------------------------------------

/**
 * An object used as an associative array to cache all currently primed
 * PM.Target instances.
 * @private
 */
PM.Target.primed = {};

/**
 * Primes this PM.Target instance.
 */
PM.Target.prototype.prime = function() {
  //logMessage("priming");
  this.primed = true;
  this.primeHooks();
  PM.Target.primed[this.internalId] = this;
};

/**
 * @private
 */
PM.Target.prototype.tryPrime = function(clusters) {
  if (this.primed || this.isLocked() === true || !this.willPrime(clusters)){
    return;
  }
  this.prime();
};

/**
 * Unprimes this PM.Target instance.
 */
PM.Target.prototype.unprime = function() {
  delete this.primed;
  this.unprimeHooks();
  delete PM.Target.primed[this.internalId];
};

/**
 * A call to this class method initiates the priming process. This method
 * tries to
 * prime all PM.Target instances with a group in common with the union
 * of groups stored in the clusters parameter. Likely you will call this
 * from an onMouseDown hook method in a PM.Draggable subclass.
 * @param {Object} clusters An object used as an associative array. The 
 * property values are PM.Cluster instances. Very likely these will be 
 * the instances of PM.Draggable currently being dragged.
 */
PM.Target.primeTargets = function(clusters) {
  var groups = PM.Cluster.groupsUnion(clusters);
  for (var g in groups) {
    for (var targId in PM.Target.groups[g]) {
      PM.Target.groups[g][targId].tryPrime(clusters);
    }
  }
};

/**
 * Unprimes all currently primed PM.Target instances.
 */
PM.Target.unprimeAllTargets = function() {
  for (var p in PM.Target.primed) {
    PM.Target.primed[p].unprime();
  }
};


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

/**
 * Interesting moment hook method called to determine whether or not to
 * prime this target. This method is important for multiple drag operations
 * and you will almost definitely need to override it in this case.
 * See the PM.Target preamble for more information about
 * using this function.
 * @param {Object} clusters An object used as an associative array. Each
 * property value is a PM.Cluster instance.
 * @type boolean
 * @return If true the target will be primed. If false the target will not
 * be primed.
 */
PM.Target.prototype.willPrime = function(clusters) {
  /* override this */ 
  return true;
};

/**
 * Interesting moment hook method called immediately before onPrime.
 * Don't use this method. Instead use the onPrime hook in your subclasses.
 */
PM.Target.prototype.b4Prime = function() { /* override this */ };
/**
 * Interesting moment hook method called when a this target is primed.
 */
PM.Target.prototype.onPrime = function() { /* override this */ };
/**
 * @private
 */
PM.Target.prototype.primeHooks = function() {
  this.b4Prime();
  this.onPrime();
};


/**
 * Interesting moment hook method called immediately before onDragEnter.
 * Don't use this method. Instead use the onDragEnter hook in your subclasses.
 * @param {Object} e The event object.
 */
PM.Target.prototype.b4DragEnter = function(e) { /* override this */ };
/**
 * Interesting moment hook method called when the cursor enters this
 * target during a drag operation. This call only happens if this target
 * is primed and is the first responder. See the PM.Target class preamble
 * for more information about priming and the first responder.
 * @param {Object} e The event object.
 */
PM.Target.prototype.onDragEnter = function(e) { /* override this */ };
/**
 * @private
 */
PM.Target.prototype.dragEnterHooks = function(e) {
  this.b4DragEnter(e);
  this.onDragEnter(e);
};

/**
 * Interesting moment hook method called immediately before onDragOver.
 * Don't use this method. Instead use the onDragOver hook in your subclasses.
 * @param {Object} e The event object.
 */
PM.Target.prototype.b4DragOver = function(e) { /* override this */ };
/**
 * Interesting moment hook method called when the cursor moves over this
 * target during a drag operation. This call only happens if this target
 * is primed and is the first responder. See the PM.Target class preamble
 * for more information about priming and the first responder.
 * @param {Object} e The event object.
 */
PM.Target.prototype.onDragOver = function(e) { /* override this */ };
/**
 * @private
 */
PM.Target.prototype.dragOverHooks = function(e) {
  this.b4DragOver(e);
  this.onDragOver(e);
};

/**
 * Interesting moment hook method called immediately before onDragLeaveOrDrop.
 * Don't use this method. Instead use the onDragLeaveOrDrop hook in your
 * subclasses.
 * @param {Object} e The event object.
 */
PM.Target.prototype.b4DragLeaveOrDrop = function(e) { /* override this */ };
/**
 * Interesting moment hook method called when the cursor leaves this
 * target or a mouseup occurs over this target during a drag operation.
 * In either case this method is called before onDragLeave or onDrop.
 * This call only happens if this target
 * is primed and is the first responder. See the PM.Target class preamble
 * for more information about priming and the first responder.
 * @param {Object} e The event object.
 */
PM.Target.prototype.onDragLeaveOrDrop = function(e) { /* override this */ };

/**
 * Interesting moment hook method called immediately before onDragLeave.
 * Don't use this method. Instead use the onDragLeave hook in your subclasses.
 * @param {Object} e The event object.
 */
PM.Target.prototype.b4DragLeave = function(e) { /* override this */ };
/**
 * Interesting moment hook method called when the cursor leaves this
 * target during a drag operation.
 * This call only happens if this target
 * was primed and was the first responder before the cursor left the target.
 * See the PM.Target class preamble
 * for more information about priming and the first responder.
 * @param {Object} e The event object.
 */
PM.Target.prototype.onDragLeave = function(e) { /* override this */ };
/**
 * @private
 */
PM.Target.prototype.dragLeaveHooks = function(e) {
  this.b4DragLeaveOrDrop(e);
  this.onDragLeaveOrDrop(e);
  this.b4DragLeave(e);
  this.onDragLeave(e);
};

/**
 * Interesting moment hook method called immediately before onDrop.
 * Don't use this method. Instead use the onDrop hook in your subclasses.
 * @param {Object} e The event object.
 */
PM.Target.prototype.b4Drop = function(e) { /* override this */ };
/**
 * Interesting moment hook method called when 
 * a mouseup occurs over this target during a drag operation.
 * This call only happens if this target
 * is primed and is the first responder. See the PM.Target class preamble
 * for more information about priming and the first responder.
 * @param {Object} e The event object.
 */
PM.Target.prototype.onDrop = function(e) { /* override this */ };
/**
 * @private
 */
PM.Target.prototype.dropHooks = function(e) {
  this.b4DragLeaveOrDrop(e);
  this.onDragLeaveOrDrop(e);
  this.b4Drop(e);
  this.onDrop(e);
};

/**
 * Interesting moment hook method called immediately before onUnprime.
 * Don't use this method. Instead use the onUnprime hook in your subclasses.
 */
PM.Target.prototype.b4Unprime = function() { /* override this */ };
/**
 * Interesting moment hook method called when a this target is unprimed.
 */
PM.Target.prototype.onUnprime = function() { /* override this */ };
/**
 * @private
 */
PM.Target.prototype.unprimeHooks = function() {
  this.b4Unprime();
  this.onUnprime();
};
