//
// This work is licensed under the Creative Commons Attribution 2.5 License. To 
// view a copy of this license, visit
// http://creativecommons.org/licenses/by/2.5/
// or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San
// Francisco, California, 94105, USA.
//
// All copies and derivatives of this source must contain the license statement 
// above and the following attribution:
//
// Author: Kyle Scholz      http://kylescholz.com/
// Copyright: 2006
//

/**
 * TreeNodeModel:
 * 
 * Represents a node in a Tree Graph.
 */
var TreeNode = function( id, radius, fanAngle, startX, startY ) {

	// Represents the angle of entry of this node's parent edge ...
	// Defaults to random so edges are randomly distributed from a root node.
	this['rootAngle'] = parseInt(Math.random()*360);

	// Each node knows it's parent so we can simplify calcuation of "branch weight".
	this['parent'];

	// indicates that this nodes is selected	
	this['selected'] = false;
	
	// indicates whether this node can move
	this['isStatic'] = true;

	// Branch weight represents the weight of all of this node's children. 
	this['branchWeight']=0;

	// A unique identifier
	this['id'] = id;
	
	// Indicates the radius (or length of edges rooted in this node)
	this['radius'] = radius;

	// Indicates the angle that can be filled with this nodes children.
    this['fanAngle'] = fanAngle;

	// Target Angle and Radius
	this['target']=new Object();
	this['target']['t'] = 0;
	this['target']['r'] = 0;

	// Current Position
	this['position']=new Object();
	this['position']['x'] = startX;
	this['position']['y'] = startY;
	this['position']['t'] = 0;
	this['position']['r'] = 0;

	// This node's children
	this['children'] = new Array();

	// TODO: Replace "children"; This is exactly the same thing.
	this['edges'] = new Array();
	
	// Indicates if this node has reached it's target
	this['done'] = false;

	/* 
	 * Increments this branch's weight.
	 */
	this.incrementBranchWeight = function() {
		this['branchWeight']++;
		if ( this['parent'] ) {
			this['parent'].incrementBranchWeight();
		}
	};

	/*
	 * Adds a child to this node. 
	 */	
	this.addEdge = function( node ) {
		this['children'].push(node);
		node['parent'] = this;
		node['isStatic'] = false;
		node['edges'].push(this);
		node['position']['t'] = this['rootAngle'];
		this.incrementBranchWeight();
		this.updateChildren();
		return node;
	};

	/*
	 * Updates target positions for a set of child nodes any time the set
	 * of child nodes changes.
	 */
	this.updateChildren = function() {

		// for each child node, calculate a new target
		for ( var i=0; i<this['children'].length; i++ ) {
			var angle;

			// If there's only one child, it's target angle is the angle of entry of
			// it's parent's "parent edge".
			if ( this['children'].length == 1 ) {
				angle=this['rootAngle'];
			// Otherwise, it's a function of the number of child nodes and the
			// fanAngle that has been specified that they must fit in.
			} else {
				angle=(this['fanAngle']/(this['children'].length)*i) + 
					this['rootAngle'] - (this['fanAngle']/(this['children'].length)*(this['children'].length-1))/2;
			}
			var node = this['children'][i];

			node['rootAngle'] = parseInt(angle);

			node['target']['t'] = angle;
			node['target']['r'] = this['radius'];

			node['done'] = false;

			node.updateChildren();
		}
	};
}

/**
 * Distance:
 * 
 * Calculates distance in for vectors and 2D space.
 */
var Distance = function( pointA, pointB ) {
	// X Distance
	this['dx'] = pointA['x'] - pointB['x'];
	// Y Distance
	this['dy'] = pointA['y'] - pointB['y'];
	this['d2'] = (this['dx']*this['dx']+this['dy']*this['dy']);
	// Distance
	this['d'] = Math.sqrt(this['d2']);

	return this;
};

/**
 * TreeGraphModel:
 * 
 * Performs positioning on nodes.
 * 
 * @extends GraphModel
 */
var TreeGraphModel = function( frameWidth, frameHeight ) {
	this.initialize( frameWidth, frameHeight );
};
TreeGraphModel.prototype = {

	// A collection of GraphModelListeners that want to receive updates from this graph
	subscribers: new Array(),
	
	// A collection  of root nodes
	updateQueue: new Array(),
	
	//
	nextUID: 0,

	// 'fluidity' is a constant that dictates the smoothness of animation. We'll
	// always make a tradeoff between smoothness and speed. For different graph
	// applications, different fluidity values will be appropriate.
	//
	// In Tree Graphs, 'fluidity' indicates the denominator in a function that dictates
	// the amount of distance between the current and target angles (ie: (TA-CA)/fluidity)
	fluidity: 3,

	// A boolean that indicates whether this graph should stagger node positions for
	// readability.
	doStagger: false,

	staggerPixels: 30,
	
	/*
	 * Initialize this instance.
	 * 
	 * @overrides GraphModel.initialize
	 */
	initialize: function( frameWidth, frameHeight ) {
		this['frameWidth'] = frameWidth;
		this['frameHeight'] = frameHeight;

		// A factor that skews the positions on the graph relative to the dimenions
		// of the enclosing frame.
		this['skew'] = frameWidth/frameHeight;
	},

	/*
	 * 
	 */
	update: function() {
		var entropy = 0;
		for ( var i=0; i<this['updateQueue'].length; i++ ) {
			entropy += this.setChildrenForces( this['updateQueue'][i] );
		}
		return entropy;
	},
		
	/*
	 * Moves child nodes closer to target.
	 * 
	 * TODO: rename ... there are no forces
	 */
	setChildrenForces: function( node ) {

		var count=0;

		for( var i=0; i<node['children'].length; i++ ) {

			// Get new angle and radius values for this node
			var dt = (node['children'][i]['position']['t'] - node['children'][i]['target']['t']);
			var dr = (node['children'][i]['position']['r'] - node['children'][i]['target']['r']);

			if ( dt<1 && dr<1 ) {
				node['children'][i]['done'] = true;
			}
													
			var newAngle = node['children'][i]['position']['t'] - (dt / this['fluidity']);
			var newRadius = node['children'][i]['position']['r'] - (dr / this['fluidity']);
	
			node['children'][i]['position']['t'] = newAngle;
			node['children'][i]['position']['r'] = newRadius;

			// Determine if we should stagger			
			var stagger = 0;
			if ( this['doStagger'] && i % 2 ) { stagger = this['staggerPixels']; }

			// Set coordinates based on angle and radius			
			node['children'][i]['position']['y'] = node['position']['y'] - 
				(Math.cos(newAngle*(Math.PI/180)) * 
				(node['children'][i]['position']['r']+stagger));
	
		    node['children'][i]['position']['x'] = node['position']['x'] - 
				((Math.sin(newAngle*(Math.PI/180)) * 
				(node['children'][i]['position']['r']+stagger)))*this['skew'];

			// Tell subscribers to draw nodes.
			for( var j=0; j<this['subscribers'].length; j++ ) {
// TODO: fix
				this['subscribers'][j].drawNode( node );
				this['subscribers'][j].drawNode( node['children'][i] );
			}
			
			if ( !node['children'][i]['done'] ) {
				count++;
			}

			// 
			this.setChildrenForces( node['children'][i] );
		}
		return( count );
	},
	
	/*
	 * Add a new TreeNode.
	 */
	addNode: function( radius, fanAngle, startX, startY ) {
		var node = new TreeNode( this['nextUID']++, radius, fanAngle, startX, startY );
		return node;
	},

	/*
	 * Add a new edge.
	 */
	addEdge: function( nodeI, nodeJ ) {

	}
}
TreeGraphModel.extend(GraphModel);
