// Chris Pyper 2006
// Undo Redo functionality

// Variable to hold undoRedo Container object
var undoRedo = false;

// This class will hold a stack of cloned DOM nodes in order to provide Undo/Redo functionality.
// In order to provide a smooth feel to the functionality pointers that overrun the end of the buffer
// will be pointed back to the beginning.  Other pointers will track the end and begining elements
// and falgs will track if certain conditions have been met.  This will give the appearance of having 
// a buffer size as defined, but with out the overhead and loss of redo functionality associated with 
// shifting arrays.  Gives a really nie undo/redo effect.
function UndoRedo()
{
	// The actual stack itself, just an array of objects 
	this.undoStack = new Array(UNDOBUFFERSIZE);
	
	// Pointers to the current element(top), the bottom of the stack, and the highest element you can travel too(ceiling)
	this.top     = 0;
	this.last    = 0;
	this.ceiling = 0;
	
	// Some flags to track certain events 
	this.initialized    = false;
	this.redoCeilingHit = true;
	this.undoFloorHit   = false;

	// Counter to hold unique id for undo/redo
	this.undoId = 0;

	// Return, if any, the previous element on the undo stack and return it
	this.undo = function()
	{
		debug('undo()', 'info');

		// If we have reached bottom of undo buffer then we can't return anything
		if(this.undoFloorHit)
		{
			debug('undo() cannot undo hit end of buffer, last = ' + this.last, 'warn');
			return false;
		}

		debug('undo() using element number ' + this.top, 'info');
		
		// Get container currently pointed too
		var container;
		if(container = this.undoStack[this.top])
			this.redoCeilingHit = false;
		else
			container = false;

		// If we have hit bottom set flag to indicate so
		if(this.top == this.last)
			this.undoFloorHit = true;
		// Otherwise decrement pointer
		else
		{
			--this.top;
			if(this.top < 0)
				this.top = UNDOBUFFERSIZE - 1;
		}

		debug('undo() top = ' + this.top + ' last = ' + this.last, 'info');

		return container;
	}

	// Return, if any, the next element in the undo stack, if an undo has been performed already
	this.redo = function()
	{
		// Check if there is room to do a redo
		if(this.redoCeilingHit)
		{
			debug('redo() cannot redo hit top of buffer, top = ' + this.top, 'warn');
			return false;
		}
	
		// Incrment pointer to top of undo buffer
		++this.top;
		if(this.top > UNDOBUFFERSIZE - 1)
			this.top = 0;

		// If we hit the top element set flag to indicate so
		if(this.top == this.ceiling)
			this.redoCeilingHit = true;

		debug('redo() using element number ' + this.top, 'info');
		debug('redo() top = ' + this.top + ' last = ' + this.last, 'info');

		// Fetch and return container
		var container;
		if(container = this.undoStack[this.top])
		{
			this.undoFloorHit = false; // Reset flag since doing a redo moves you off undo floor
	        return container;
		}
	    return false;
	}

	this.snapShot = function(snapShotNode, type)
	{
		debug('snapShot()', 'info');

		var clone = snapShotNode.cloneNode(true);

		// Clean up any spell check highlighting
		removeSpellCheckHighlighting(clone);

		// Check if element passed over matches previous, this is neecessary to smooth out funcitonality
		// sometimes identical snapshots may be taken, the user undo experince should be based on page change
		// events not internal editor events, this helps that out a lot
		if(this.initialized && this.last != this.top && !this.undoFloorHit)
		{
			var tempContainer = this.undoStack[this.top];

			// We have to match container type for a match.  But some times a new element is added and then
			// during a edit not changed, or duplicated, this cuases another identical copy of the node to 
			// be pushed onto the undo/redo buffer with diffrent type, the first snapshot taken of an element
			// in edit mode has a type of move and the added element has a type of add, so check for this
			// and if it occurs, omit it as a qualifying paramter.
			var typePass = false;
			if(tempContainer.type == type || (tempContainer.type == 'add' && type == 'move'))
				typePass = true;

			//debug("TC=" + tempContainer.type + " C=" + type, 'error');

			if(tempContainer && this.areEqual(tempContainer.clone, clone) && typePass)
			{
				debug('snapShot() Previous buffer entry matches node, ignoring.... ', 'error');
				return false;
			}
		}
		
		// Build container
		var container = new UndoRedoDOMContainer(clone, type, this.undoId);
		++this.undoId;
		
		// Increment top
		if(this.initialized)
			++this.top;
		if(this.top > UNDOBUFFERSIZE - 1)
			this.top = 0;
		
		// Any time a element is added it becomes the top thus overriding elements you have undone
		this.ceiling = this.top;

		// Add container
		this.undoStack[this.top] = container;

		// Move up pointer to bottom of buffer if we hit it
		if(this.top == this.last && this.initialized)
			++this.last;
		if(this.last > UNDOBUFFERSIZE - 1)
			this.last = 0;

		// Set flags to current buffer state
		this.initialized    = true;
		this.undoFloorHit   = false;
		this.redoCeilingHit = true;
		
		debug('snapShot() element pushed to undo/redo stack element number ' + this.top, 'warn');
		debug('snapShot() top = ' + this.top + ' last = ' + this.last, 'info');

		return true;
	}

	// Replace a snaphost.  This is used when a redo is performed becuase the exisintg
	// node needs to be captured as we are reverting to the previous
	this.replaceSnapShot = function(container)
	{
		debug('replaceSnapShot() using undoId ' + container.id, 'info');

		for(var i = 0; i < UNDOBUFFERSIZE; ++i)
		{
			var localContainer = this.undoStack[i];
			if(localContainer && localContainer.id == container.id)
			{
				debug('replaceSnapShot() undo container found id' + container.id, 'info');
				this.undoStack[i] = container;
				return true;
			}
		}
		debug('replaceSnapShot() undo container NOT found id' + container.id, 'error');
		return false;
	}

	// Function used by editor to determine if a final snapshot to grab the last should be taken if undo is used
	// when pointer is at top of undo stack
	this.atTopOfUndoStack = function()
	{
		return this.redoCeilingHit;
	}

	// Since there is no current way to check for equality of DOM nodes(i.e. there identical), I will have to
	// make my own implementation for measuring equality within the context of the editor
	this.areEqual = function(node1, node2)
	{
		if(getId(node1) != getId(node2))
			return false;
		if(node1.className != node2.className)
			return false;
		if(node1.innerHTML != node2.innerHTML)
			return false;
		// There are ugly and expensive ways to match styles but there are the only ones that matter
		// in the context of the editor so do it fast and cheap
		if(node1.style.top != node2.style.top)
			return false;
		if(node1.style.left != node2.style.left)
			return false;
		if(node1.style.width != node2.style.width)
			return false;
		if(node1.style.height != node2.style.height)
			return false;
		if(node1.style.position != node2.style.position)
			return false;
		if(getStyle(node1, 'zIndex') != getStyle(node2, 'zIndex'))
            return false;

		return true;
	}
}

// Container to bind together Original Node Reference and a Cloned snapshot of that node in a earlier state.
function UndoRedoDOMContainer(clone, type, id)
{
	this.clone = clone;
	this.type  = type;
	this.id	   = id;
}

// Factory method to UndoRedo object
function fetchUndoRedo()
{
	if(!undoRedo)
		undoRedo = new UndoRedo();
	return undoRedo;
}

// Grab instance of undo object and take snapshot of node
function undoRedoSnapShot(node, type)
{
	debug('undoRedoSnapShot()', 'info');

    var undoRedo = fetchUndoRedo();
    generateId(node);
    return undoRedo.snapShot(node, type);
}

