// Copyright (C) 2009 Jordan Bettis <jordanb@hafdconsulting.com>

// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use, copy,
// modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL JORDAN BETTIS BE LIABLE FOR
// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// Except as contained in this notice, the name of Jordan Bettis, and
// the mark "HAFD Consulting" shall not be used in advertising or
// otherwise to promote the sale, use or other dealings in this
// Software without prior written authorization from Jordan Bettis.


function PetGhost(playpen, ghost_name, initial_css) {

    ///////////////////////////////////////////////////////////////////////////
    // PROPERTIES
    ///////////////////////////////////////////////////////////////////////////

    // Playpen is a jquery object for the div tag that the ghost can play.
    this.playpen = playpen;

    /**
     * This is used if the browser window gets resized to map our ghost
     * to an analogous position in the new playpen (when pp size is
     * dependentent upon window size, of course; otherwise, this does
     * nothing).
     */
    this.pp_size = {w: playpen.width(), h: playpen.height()};

    // Now let's add the ghost to the playpen and set this.ghost.
    this.ghost_name = ghost_name;
    this.playpen.append('<div id="' + ghost_name + '"></div>');
    this.ghost = $("div#" + ghost_name)
 
    this.ghost.css(initial_css);

    this.div_size = 64;

    // We want to block new animation requests while the ghost is being 
    // animated, so we use this semaphore. It can be either "normal" or
    // "busy".
    this.state = "normal"

    /**
     * We'll make a mouse_position object to be maintained by our
     * event handler, so we can always know where the mouse is.
     */
    this.mouse_position = {left: null, top: null};

    /**
     * This defines the possible eye positions and their offsets in
     * the sprite image.
     */
    this.eye_pos = {up_left    : 0 * -this.div_size + "px",
		    down_left  : 1 * -this.div_size + "px",
		    down_right : 2 * -this.div_size + "px",
		    up_right   : 3 * -this.div_size + "px",
		    front      : 4 * -this.div_size + "px"};

    /**
     * This defines the possible sheet states and their offsets in the
     * sprite map.
     */
    this.sheet_pos = {bumps  : 0 * -this.div_size + "px",
		      spikes : 1 * -this.div_size + "px"};

    /**
     * This is the sprite state of our ghost. It is updated as we run
     * through our various animations. It should always be in
     * agreement with the CSS backgroundPosition attribute. We
     * reproduce the state here so we can treat the DOM as
     * write-only and thereby avoid some cross-browser ugliness.
     */
    this.sprite_state = {eye : this.eye_pos.up_right,
 			 sheet : this.sheet_pos.bumps};

    ///////////////////////////////////////////////////////////////////////////
    //  METHODS
    ///////////////////////////////////////////////////////////////////////////

    /**
     * This function is called by the constructor after having defined
     * all the methods. It sets up the ghost so that it can be
     * manipulated properly, and registers the appropriate callbacks.
     */
    this.initialize = function () {
	this_ghost = this
	this.ghost.show("slow");

	pos = this.ghost.position();

	if(this.ghost.css("right") != null)
	    this.ghost.css("right", "");
	if(this.ghost.css("bottom") != null)
	    this.ghost.css("bottom", "");

	this.ghost.css({"left": pos.left,
			"top" : pos.top});

	this.update_css_sprite();

	// This timer makes the sheet flutter, but also provides an 
        // opportunity to make some movements.
	window.setInterval(function() { 
	    this_ghost.flutter_sheet(); 
	    this_ghost.consider_movement();
	}, 750);

	// This handles window resize events, which may change the size of our
        // playpen div and require us to shift the ghost back into a legal
	// position.
	$(window).resize(function() {
	    new_size = {w: this_ghost.playpen.width(),
			h: this_ghost.playpen.height()};

	    scale = {w: new_size.w / this_ghost.pp_size.w,
		     h: new_size.h / this_ghost.pp_size.h};

	    curr_pos = this_ghost.ghost.position();
	    this_ghost.ghost.css({"left" : curr_pos.left * scale.w,
				 "top"  : curr_pos.top * scale.h});

	    this_ghost.pp_size = new_size;

	});

	// This keeps track of the current mouse position.
	$(document).mousemove(function(e) {
	    pen_pos = this_ghost.playpen.position();
	    this_ghost.mouse_position.left = e.pageX - pen_pos.left;
	    this_ghost.mouse_position.top = e.pageY - pen_pos.top;
	    this_ghost.point_eyes();
	});

    }

    /**
     * This function sets the current sprite background position to be
     * equal to the current state of this object. It should be called
     * by any method which updates the sprite state.
     */
    this.update_css_sprite = function() {
	if(this.sprite_state.eye == this.eye_pos.front) {
	    this.sprite_state.sheet = this.sheet_pos.bumps;
	    this.playpen.css({"cursor": "crosshair"});
	}
	else {
	    this.playpen.css({"cursor": ""});
	}

	this.ghost.css({backgroundPosition : 
			this.sprite_state.eye + " " + this.sprite_state.sheet});
    }

    /**
     * This is a trigger that toggles the sheet state of the css
     * sprite.  Which is to say, if it's in bump mode, it'll flip it
     * to spikes and vice versa. It should be run from a jquery timer
     * however often we want the sheet to flutter.
     */
    this.flutter_sheet = function () {
	// There's no spikey state for eyes-front, so we do nothing if
	// the sprite is in that state.
	this.point_eyes();

	if(this.sprite_state.eye == this.eye_pos.front) {
	    return;
	}
	else if(this.sprite_state.sheet == this.sheet_pos.bumps) {
	    this.sprite_state.sheet = this.sheet_pos.spikes;
	}
	else {
	    this.sprite_state.sheet = this.sheet_pos.bumps;
	};

	this.update_css_sprite();
    }

    /**
     * This should be called from a callback with some regularity. It
     * ponders the different types of movement the ghost might make,
     * based on probability functions.
     */
    this.consider_movement = function() {
	if(this.state == "busy")
	    return;

	position = this.relative_position(this.mouse_position);
	
	// Our ghost has the mouse, so there's a 10% chance he'll lose
	// interest.
	if(position.r < 40) {
	    if(Math.random() < 0.05) {
		this.creep();
	    }
	}
	else {
	    // The closer the mouse is, the greater probability that our 
	    // ghost will pounce. This reciprocal seems to support that
	    // behavior pretty well.
	    pounce_prob = 1/(position.r * 0.05)
	    if(this.inside_boundary(position) &&
	       Math.random() < pounce_prob) {
		this.pounce();
	    }
	    // If we don't pounce, there's a 10% probability that
	    // we'll creep.
	    else if(this.inside_boundary(position) && 
		    Math.random() < 0.08) {
		this.creep();
	    }
	}
    }
	    

    /**
     * This method efficiently keeps the eyes pointed at the mouse. It
     * should be called whenever some event might cause the relationship
     * between the mouse and the ghost to change.  
     */
    this.point_eyes = function() {
	rel_pos = this.relative_position(this.mouse_position);

	if(rel_pos.r < 40)
	    eye = this.eye_pos.front;

	else if(rel_pos.theta > 0 && rel_pos.theta <= Math.PI/2)
	    eye = this.eye_pos.up_right;

	else if(rel_pos.theta > Math.PI/2 && rel_pos.theta <= Math.PI)
	    eye = this.eye_pos.up_left;

	else if(rel_pos.theta > Math.PI && rel_pos.theta <= 3/2 * Math.PI)
	    eye = this.eye_pos.down_left;

	else if(rel_pos.theta > 3/2 * Math.PI) {
	    eye = this.eye_pos.down_right;
	}

	if(eye != this.sprite_state.eye) {
	    this.sprite_state.eye = eye;
	    this.update_css_sprite();
	}
    }
	    

    /**
     * This takes an absolute position (in left/top format), based
     * on the ghost's position-parent.
     *
     * It returns an object with 'x', 'y', 'r' and 'theta' set to
     * correspond to (x,y) and (r,theta) coordinates for the in_pos
     * on a cartesian and polar plane with the ghost at the origin.
     */
    this.relative_position = function(in_pos) {
	// Reflect about the x axis to behave cartesianally.
	in_pos_x = in_pos.left;
	in_pos_y = -in_pos.top;

        // Nudge the origin over to be at the center of the div.
	our_pos = this.ghost.position()
	our_pos_x = (our_pos.left + this.div_size/2);
	our_pos_y = -(our_pos.top + this.div_size/2);

	rel_pos = new Object();
	rel_pos.x = in_pos_x - our_pos_x;
	rel_pos.y = in_pos_y - our_pos_y;

	pol_pos = this.calculate_polar(rel_pos);
	
	rel_pos.r = pol_pos.r;
	rel_pos.theta = pol_pos.theta;

	return rel_pos;
    }

    /**
     * This takes a relative position, in either (x,y) or (r,theta) format,
     * with the ghost at the origin, and returns an absolute position 
     * relative the position-parent in left/top format. 
     *
     * It can be thought the inverse of the relative_position function
     * except that it doesn't undo the 32 pixel offset, so that it's
     * still referenced off the center of the div instead of the top left
     * corner.
     */
    this.absolute_position = function(in_pos) {
	if (!(("x" in in_pos) && ("y" in in_pos))) {
	    in_pos = this.calculate_cartesian(in_pos);
	}

	our_pos = this.ghost.position();
	our_pos_x = (our_pos.left);
	our_pos_y = -(our_pos.top);
	
	// Some weird browser bugs crop up with some non-whole values.
	abs_pos = {left : Math.floor(in_pos.x + our_pos_x),
		   top : Math.floor(-(in_pos.y + our_pos_y))};

	// Various rounding issues can cause the position to drift
	// outside the allowed range, even when doing boundary
	// checking.
	abs_pos.left = Math.max(abs_pos.left, 0);
	abs_pos.top = Math.max(abs_pos.top, 0);
	abs_pos.left = Math.min(abs_pos.left, 
				this.playpen.width() - this.div_size/2);
	abs_pos.top = Math.min(abs_pos.top, 
			       this.playpen.height() - this.div_size/2);

	return abs_pos;
    }

    /**
     * Given a cartesian position (an object with legal 'x' and 'y'
     * properties) it will return a polar position, with 'r' and 'theta'
     * properties.
     */
    this.calculate_polar = function(cart_pos) {
	pol_pos = new Object();

	pol_pos.r = Math.sqrt(
	    cart_pos.y * cart_pos.y + cart_pos.x * cart_pos.x);

	if(cart_pos.x == 0 && cart_pos.y == 0)
	    pol_pos.theta = 0;

	else if(cart_pos.x == 0 && cart_pos.y > 0)
	    pol_pos.theta = Math.PI/2;

	else if(cart_pos.x == 0 && cart_pos.y < 0)
	    pol_pos.theta = 3/2 * Math.PI;

	else if(cart_pos.x > 0 && cart_pos.y >= 0)
	    pol_pos.theta = Math.atan(cart_pos.y/cart_pos.x);

	else if(cart_pos.x > 0 && cart_pos.y < 0)
	    pol_pos.theta = Math.atan(cart_pos.y/cart_pos.x) + 2 * Math.PI;

	else if (cart_pos.x < 0)
	    pol_pos.theta = Math.atan(cart_pos.y/cart_pos.x) + Math.PI;

	return pol_pos;
    }	

    /**
     * Given an object with polar positioning information (an 'r' and
     * 'theta' property), this returns an object with the corresponding
     * cartesian position.
     */
    this.calculate_cartesian = function(polar_pos) {
        while (polar_pos.theta < 0)
            polar_pos.theta += 2 * Math.PI;
        while (polar_pos.theta >= 2 * Math.PI)
            polar_pos.theta -= 2 * Math.PI;

	cart_pos = new Object();

        cart_pos.x = polar_pos.r * Math.cos(polar_pos.theta);
        cart_pos.y = polar_pos.r * Math.sin(polar_pos.theta);
	
	return cart_pos;

    }

    /**
     * This produces a random number from a normal distribution about 
     * the indicated mean (mu) with the standard deviation (sigma^2)
     * stddev.
     *
     * It allows us to randomize the things the ghost decides
     * to do (where to wander, for instance) while still having him
     * 'prefer' to do something sensible (by setting the most sensible
     * option to be the mean of the distribution).
     *
     * This implements the Kinderman and Monahan method, which is
     * algorithm 3.4.1.R from Knuth's Seminumerical Algorithms
     * (p130). The notation used here is the notation used by
     * Dr. Knuth.
     */
    this.normal_random = function(mean, stddev) {
	do {
	    v = Math.random();

	    // u can not be zero
	    do
		u = Math.random();
	    while(u == 0);
	 
	    x = Math.sqrt(8/Math.E) * (v - 1/2)/u;

	} while(x*x <= -4 * Math.log(u)) ;
	
	return mean + x * stddev;
    }


    /**
     * This determines if the given relative position is within the
     * boundaries of the playpen.
     */
    this.inside_boundary = function(position) {
	abs_pos = this.absolute_position(position);
	if (abs_pos.top < this.div_size/2
	    || abs_pos.left < this.div_size/2
	    || abs_pos.left > this.playpen.width() - this.div_size
	    || abs_pos.top > this.playpen.height() - this.div_size)
	{
	    return false;
	}
	else {
	    return true;
	}
    }

    /**
     * This calculates the distance of the ghost from the given wall
     * (one of 'left', 'top', 'right', 'bottom') following a line from
     * the ghost with the angle theta.
     *
     * If the line doesn't intersect the wall, the function will return
     * Infinity.
     *
     * We include buffering to account for the size of the ghost's div.
     */
    this.wall_distance = function(wall, theta) {
	curr_pos = this.ghost.position();
	curr_pos.top += this.div_size/2;
	curr_pos.left += this.div_size/2;

	while (theta < 0)
	    theta += 2*Math.PI;
	while (theta >= 2*Math.PI)
	    theta -= 2*Math.PI;

	if(wall == "left") {
	    
	    if(theta <= Math.PI/2 || theta >= 3/2*Math.PI)
		return Infinity;
	    else {
		direct = curr_pos.left - this.div_size/2;
		d_theta = Math.abs(theta - Math.PI);
	    }
	}
	else if(wall == "top") {
	    if(theta >= Math.PI)
		return Infinity;
	    else {
		direct = curr_pos.top - this.div_size/2;
		if(theta >= Math.PI/2)
		    d_theta = theta - Math.PI/2;
		else
		    d_theta = Math.PI/2 - theta;
	    }

	}
	else if(wall == "right") {
	    if(theta < Math.PI/2 || theta > 3/2*Math.PI )	{
		direct = this.playpen.width() - this.div_size/2 - curr_pos.left;
		if(theta < Math.PI/2)
		    d_theta = theta;
		else
		    d_theta = 2*Math.PI - theta;
	    }
	    else
		return Infinity;
	}
	else if(wall == "bottom") {
	    if(theta <= Math.PI)
		return Infinity;
	    else {
		direct = this.playpen.height() - this.div_size/2 - curr_pos.top;
		if(theta <= 3/2*Math.PI)
		    d_theta = Math.abs(theta - 3/2*Math.PI);
		else
		    d_theta = theta - 3/2*Math.PI;
	    }
	}

	return direct / Math.cos(d_theta);
    }
    
    /**
     * If the position is outside of the playpen, this finds the wall
     * the ghost will hit trying to get to it, and the position of the
     * impact.
     *
     * It returns a relative position (x, y, r, theta properties) with
     * the addition of a 'wall' property.
     */
    this.find_impact = function(position) {

	left = this.wall_distance("left", position.theta);
	top = this.wall_distance("top", position.theta);
	right = this.wall_distance("right", position.theta);
	bottom = this.wall_distance("bottom", position.theta);

	impact = {wall : "left", r : left};

	if(top < impact.r)
	    impact = {wall : "top", r : top};
	if(right < impact.r)
	    impact = {wall : "right", r : right};
	if(bottom < impact.r)
	    impact = {wall : "bottom", r : bottom};

	impact.theta = position.theta;

	cart = this.calculate_cartesian(impact);
	impact.x = cart.x;
	impact.y = cart.y;

	return impact;

    }

    /**
     * If the ghost wants to go to position but there's a wall in the way,
     * the move function should call this instead to make the ghost hit
     * the wall rather than proceeding through it.
     */
    this.hit_wall = function(position, duration) {
	this_ghost = this;
	impact_point = this.find_impact(position);

	abs_impact_point = this.absolute_position(impact_point);

	this.state = "busy";
	this.ghost.animate(
	    {top: abs_impact_point.top + "px",
	     left: abs_impact_point.left + "px"},
	    duration,
	    function () {
		this_ghost.point_eyes();
		this_ghost.state = "normal";
	    });
    }

    /**
     * This makes the ghost try to 'pounce' on the current position
     * of the mouse. If it misses but the mouse is still close by,
     * it will 'lunge' at the new position in the animation callback.
     */
    this.pounce = function() {
	position = {left: this.mouse_position.left - this.div_size/2,
		    top: this.mouse_position.top - this.div_size/2};

	rel_pos = this.relative_position(this.mouse_position);
	duration = rel_pos.r;
	this_ghost = this

	if(this.inside_boundary(rel_pos)) {

	    this.state = "busy";
	    this.ghost.animate(
		{left: position.left + "px", top: position.top + "px"},
		duration,
		function () {
		    this_ghost.point_eyes();
		    new_mouse_pos = this_ghost.relative_position(
			this_ghost.mouse_position);
		    if(new_mouse_pos.r < 350) {
			this_ghost.run(new_mouse_pos);
		    }
		    this_ghost.state = "normal";
		});
	}
	else {
	    this.hit_wall(rel_pos, duration);
	}

    }

    /**
     * Slowly move a little. Since our little ghost is mouse-obsessed,
     * we want him to tend in the direction of the cursor, but only to
     * a good pouncing position, so we'll set up a normal deviate
     * around 200px away;
     */
    this.creep = function() {

	position = this.relative_position(this.mouse_position);
	
	// Our ghost is on the mouse, so we actually want him to lose
	// interest and just go off someplace else.
	if(position.r < 40) {
	    abs_pos = {left: Math.random() * (this.playpen.width() - 64),
		       top: Math.random() * (this.playpen.height() - 64)};
	    
	    creep_position = this.relative_position(abs_pos);
	}
	// In this case we just want to move the ghost around a bit,
	// as if he's getting excited.
	else if(position.r <= 200) {
	    rand_r = Math.abs(this.normal_random(0, 5));
	    rand_theta = Math.random() * 2*Math.PI;
	    cart = this.calculate_cartesian({r: rand_r, theta: rand_theta});
	    creep_position = {r: rand_r, theta: rand_theta,
			      x: cart.x, y: cart.y};
	}
	else {
	    rand_r = Math.max(
		position.r - 50,
		Math.abs(this.normal_random(position.r - 200, 10)));
	    rand_theta = this.normal_random(position.theta, 0.05); 
	    cart = this.calculate_cartesian({r: rand_r, theta: rand_theta});
	    creep_position = {x: cart.x, y: cart.y,
			      r: rand_r, theta: rand_theta};
	}
	duration = creep_position.r * 10;
	
	this.move(creep_position, duration);
    }

    /**
     * Move very quickly to the specified relative position.
     */
    this.run = function(position) {
	duration = position.r;
	this.move(position, duration);
    }

    /**
     * Move to the specified position taking the specified amount of
     * time. The position must be relative.
     */
    this.move = function(position, duration) {
	this_ghost = this
        abs_pos = this.absolute_position(position);

	if(this.inside_boundary(position)) {
	    this.state = "busy";
	    this.ghost.animate(
		{top: abs_pos.top + "px",
		 left: abs_pos.left + "px"},
		duration,
		function () {
		    this_ghost.point_eyes();
		    this_ghost.state = "normal";
		});
	}
	else {
	    this.hit_wall(position, duration)
	}
    }


    ///////////////////////////////////////////////////////////////////////////
    // INITIALIZATION
    ///////////////////////////////////////////////////////////////////////////

    this.initialize();
	
}

