//
// PositionTracker generates a moving (X,Y) coordinate based on various configurable
// options. Requires prototype.js 1.6.1 or greater.
//
// Example usage:
//
//   var tracker = new PositionTracker( $('canvas') );
//   tracker.track('trailingMouse', {delay:16});
//   
//   function draw() {
//     tracker.update(frameCount);
//     var [x, y] = tracker.position();
//     ...
//   }
//   
// Copyright 2010 Elijah Saxon
// Copying and distribution of this file, with or without modification,
// are permitted in any medium without royalty provided the copyright
// notice and this notice are preserved.  This file is offered as-is,
// without any warranty.
//

/**
 * Class PositionTracker
 * 
 **/

var PositionTracker = Class.create({

  // 
  // PUBLIC METHODS
  //

  initialize: function(canvas) {
    this.canvas = canvas;
    this.canvas.observe('mousemove', this.mouseMove.bind(this));
    this.canvas.observe('mouseover', this.mouseOver.bind(this));
    this.canvas.observe('mouseout',  this.mouseOut.bind(this));
    this.mouse = true;
    this.trackers = [];
    this.x = -1;
    this.y = -1;
  },

  /**
   * PositionTracker#track(what[, options]) -> undefined
   * - what (String): the type of tracker to enable.
   * - options (Object): optional options for the tracker.
   * Activates an additional position tracker.
   **/
  track: function(what, options) {
    if (what == 'trailingMouse') {
      this.trackMouse(options);
    } else if (what == 'wander') {
      this.trackWander(options);
    }
  },

  /**
   * PositionTracker#position() -> Array
   * Returns the current [x,y] position.
   **/
  position: function() {
    return [this.x, this.y];
  },

  /**
   * PositionTracker#update(frameCount) -> Boolean
   * - frameCount (Integer): the sequence number of the current animation frame.
   * Called at each animation frame. Returns true unless there is no valid position yet.
   **/
  update: function(frameCount) {
    this.trackers.each(function(trackerUpdate) {
      trackerUpdate(frameCount);
    })
    return this.x != -1;
  },

  // 
  // PRIVATE METHODS
  //

  //
  // CANVAS DOM EVENT CALLBACKS
  //

  mouseMove: function(event) {
    this.mouseX = event.pointerX();
    this.mouseY = event.pointerY();
    if (this.x == -1) this.x = this.mouseX;
    if (this.y == -1) this.y = this.mouseY;
  },

  mouseOver: function(event) {
    this.mouseMove(event);
    this.mouse = true;
  },

  mouseOut: function(event) {
    this.mouse = false;
  },

  //
  // trailing mouse tracker
  //

  trackMouse: function(options) {
    this.mouseDelay = options['delay'] || 1;
    this.mouseX = this.x;
    this.mouseY = this.y;
    this.trackers.push(this.updateMouse.bind(this));
  },

  updateMouse: function(frameCount) {
    if (this.mouse) {
      this.x += (this.mouseX-this.x) / this.mouseDelay;
      this.y += (this.mouseY-this.y) / this.mouseDelay;
    }
  },

  //
  // aimless wandering tracker
  //

  trackWander: function(options) {
    this.wander = options;
    this.wander.direction = Math.random() * 360;
    this.wander.when = this.wander.when || 'always';
    this.wander.speed = this.wander.speed || 2;
    this.wander.flux = 50;
    this.wander.angleDelta = 1;
    this.trackers.push(this.updateWander.bind(this));
  },

  updateWander: function(frameCount) {
    var w = this.wander;
    if (w.when == 'nomouse' && this.mouse == true) {
      return;
    } else {
      // periodically, update the turning rate. 
      if (frameCount % w.flux == 0) {
		    // determine the urge to turn
		    var boundingBox = [[0,0], [this.canvas.width,this.canvas.height]];
		    var distance = this.distanceToEdge([this.x, this.y], w.direction, boundingBox);
		    distance /= Math.max(this.canvas.width, this.canvas.height); // crudely normalize distance
		    // the closer to the edge we get, the more we want to turn. 1 = must turn, 0 = don't turn
		    var urgeToTurn = Math.pow(1/(distance+0.5), 2);
        // update turning rate
        w.angleDelta = (Math.random() > 0.5 ? -1 : 1 ) * urgeToTurn;
      }

      // move the current vector
      w.direction += w.angleDelta;
      this.x += Math.cos(this.radians(w.direction)) * w.speed;
      this.y += Math.sin(this.radians(w.direction)) * w.speed;
      if (this.x < 0 || this.y < 0 || this.x > this.canvas.width || this.y > this.canvas.height) {
        // hit the edge, so move toward the center
        w.direction = this.angleOfVector(this.canvas.width/2 - this.x, this.canvas.height/2 - this.y);
        w.angleDelta = 0;
      }
    }
  }, 

  //
  // helper methods that should be put in a different library
  //

  // vector fun

  // returns the angle in degrees of a vector (normalized or not).
  angleOfVector: function(x, y) {
    var angle = this.degrees( Math.atan(y/x) );
    if (x < 0) angle -= 180;
    return angle;
  },

  // give a point and direction, returns the distance to a bounding box edge
  // position: [x,y]
  // direction: degrees
  // boundingbox: [[x,y], [x,y]]
  // assume that the bounding box is not more than 2000 px from corner to corner.
  distanceToEdge: function(position, direction, boundingbox) {
    var endpointx = position[0] + Math.cos(this.radians(direction))*2000;
    var endpointy = position[1] + Math.sin(this.radians(direction))*2000;
    var line = [position, [endpointx, endpointy]];
    var points = Intersect.lineWithRect(line, boundingbox);
    if (points.size() > 0) {
      var edgepoint = points[0];
      return this.distance(position, edgepoint);
    } else { return 2000; }
  },

  // copied from processing.js
  radians: function(angle) {return (angle/180) * Math.PI;},
  degrees: function(angle) {
    angle = (angle * 180) / Math.PI;  
    if (angle < 0) {angle = 360 + angle}    
    return angle;
  },
  random: function(aMin, aMax) {
    return arguments.length == 2 ? aMin+(Math.random()*(aMax-aMin)) : Math.random()*aMin;
  },
  distance: function(p1, p2) {
    return Math.sqrt( Math.pow( p2[0] - p1[0], 2 ) + Math.pow( p2[1] - p1[1], 2 ) );
  }

});

