//Copyright (C) Ben Parbury - All Rights Reserved. Unauthorized copying of this file, via any medium is strictly prohibited. Written by Ben Parbury <ben@gridlockinteractive.com> 2014
var gUndoSystem = new UndoSystem();

function Undo()
{
	this.time = gLevelSystem.time;
}

function UndoPlayerPush(player, x, y, block)
{
	Undo.call(this);
	
	this.player = player;
	this.block = block;
	
	this.Undo = function()
	{
		player.LoadToPosition(x, y);
	}
	
	this.DebugString = function()
	{
		return "Push " + x + ", " + y;
	}
}

function UndoPlayerMovement(player, x, y)
{
	Undo.call(this);
	
	this.Undo = function()
	{
		player.LoadToPosition(x, y);
	}
	
	this.DebugString = function()
	{
		return "Player " + x + ", " + y;
	}
}

function UndoBlockMovement(block, x, y)
{
	Undo.call(this);
	
	this.Undo = function()
	{
		block.LoadToPosition(x, y);
	}
	
	this.DebugString = function()
	{
		return "Block " + x + ", " + y;
	}
}

function UndoBlockSlide(block, x, y, moveObject)
{
	Undo.call(this);
	
	this.block = block;
	this.moveObject = moveObject;
	
	this.Undo = function()
	{
		block.LoadToPosition(x, y);
	}
	
	this.Requirement = function()
	{
		return new RequirementBlockSlide(block, moveObject);
	}
	
	this.DebugString = function()
	{
		return "Slide " + x + ", " + y;
	}
}


function RequirementBlockSlide(block, moveObject)
{
	this.IsMet = function(record)
	{
		if(moveObject == null)
		{
			return (record instanceof UndoBlockSlide && record.moveObject != null && record.block == block);
		}
		else if(moveObject instanceof Player)
		{
			return (record instanceof UndoPlayerPush && moveObject == record.player && block == record.block);
		}
		else	//block pushed by another block should be handled by above...
		{
			return true;
		}
		
	}
}

function UndoBlockDelete(block)
{
	Undo.call(this);
	
	this.Undo = function()
	{
		block.deleting = false;
		block.deleteTime = 0;
		block.deletingId = undefined;
		
		gBlockSystem.UndoInsertBlock(block);
	}
	
	this.Requirement = function()
	{
		return new RequirementBlockDelete();
	}
	
	this.DebugString = function()
	{
		return "Delete";
	}
}

function RequirementBlockDelete()
{
	this.IsMet = function(record)
	{
		return record instanceof UndoBlockMovement || record instanceof UndoBlockSlide;
	}
}

function UndoStatsChange(stat, previous)
{
	Undo.call(this);
	
	this.Undo = function()
	{
		gStatsSystem.SetStat(stat, previous);
	}
	
	this.DebugString = function()
	{
		return "Stat " + stat + " = " + previous;
	}
}

function UndoPushTracker(player, block, dx, dy)
{
	Undo.call(this);
	
	this.Undo = function()
	{
		player.pushTracker.Undo(block, dx, dy);
	}
	
	this.DebugString = function()
	{
		return "PushTracker";
	}
}

function UndoHintTile(prevTile, currentTile)
{
	Undo.call(this);
	
	this.Undo = function()
	{
		if(currentTile)
		{
			currentTile.HideHint();
		}
		
		if(prevTile)
		{
			prevTile.ShowHint();
		}
	}
	
	this.DebugString = function()
	{
		return "HintTile";
	}
}

function UndoScoreTable(deletingId, previousValue)
{
	Undo.call(this);
	
	this.Undo = function()
	{
		gBlockSystem.scoreTable[deletingId] = previousValue;
	}
	
	this.DebugString = function()
	{
		return "ScoreTable";
	}
}

function UndoSystem()
{
	this.enabled = false;
	this.undoing = false;
	this.undoRequested = false;
	this.lastUndoTime = 0;
	this.kUndoTime = 150;
	this.kUndoLength = 1024;
	this.undoSound = gSounds.Get("undo");
	
	this.Enable = function()
	{
		this.enabled = !gLevelSystem.IsArcade() && !gEditor;
		this.undoRecords = new Array();
		this.nextUndoRecordIndex = 0;
		this.lastUndoTime = 0;
		this.Resize();
	}
	
	this.Resize = function()
	{
		this.interferenceOffsets = new Array();
		var interferenceLines = 3;
		var randomMax = gDraw.canvas.height/interferenceLines;
		for(var i = 0; i < interferenceLines; i++)
		{
			var offset = (randomMax * i) + Math.randomRange(randomMax * 0.5, randomMax)
			this.interferenceOffsets.push(offset);
		}
	}
	
	this.Disable = function()
	{
		this.enabled = false;
	}
	
	this.AddRecord = function(record)
	{
		if(this.enabled)
		{
			this.undoRecords[this.nextUndoRecordIndex] = record;
			this.nextUndoRecordIndex = this.GetPosOffset(1);
		}
	}
	
	this.GetPosOffset = function(offset)
	{
		var pos = this.nextUndoRecordIndex + offset;
		while(pos >= this.kUndoLength)
		{
			pos -= this.kUndoLength;
		}
		
		while(pos < 0)
		{
			pos += this.kUndoLength;
		}
		
		return pos;
	}
	
	this.KeyDown = function(key)
	{
		if(this.enabled)
		{
			switch(key)
			{
				case EKeyCode.z:
				case EKeyCode.delete:
				{
					this.StartUndoing();
				}
				break;
			}
		}
	}
	
	this.KeyUp = function(key)
	{
		if(this.enabled)
		{
			switch(key)
			{
				case EKeyCode.z:
				case EKeyCode.delete:
				{
					this.EndUndoing();
				}
				break;
			}
		}
	}
	
	this.CanUndo = function()
	{	
		var requirements = new Array();
		var continueUndoing = true;
		for(var index = 1; index < this.kUndoLength; index++)
		{
			var pos = this.GetPosOffset(-index);
			var record = this.undoRecords[pos];
			if(record)
			{
				for(var i = 0; i < requirements.length; i++)
				{
					if(requirements[i].IsMet(record))
					{
						requirements.splice(i--, 1);
					}
				}
				
				if(record.Requirement)
				{
					requirements.push(record.Requirement());
				}

				var canExitUndoing = this.CanExitUndoing(requirements, record);
				if(canExitUndoing)
				{
					return true;
				}
			}
			else
			{
				return false;
			}		
		}
	}
	
	this.StartUndoing = function()
	{
		if(this.enabled && this.CanUndo() && this.undoing == false && gPause.IsPaused() == false)
		{
			this.undoing = true;
			this.undoRequested = true;
			this.undoExitPoint = false;
			this.undoRequirements = new Array();
			gParticleSystem.Clear();
			for(var i = 0; i < this.interferenceOffsets.length; i++)
			{
				gParticleSystem.Spawn("UndoParticle", {offset:this.interferenceOffsets[i]});
			}
			this.undoSound.Play(null, true);	//play during undo
		}
	}
	
	this.EndUndoing = function()
	{
		//request to stop at next suitable point...
		this.undoRequested = false;
	}
	
	this.StopUndoing = function()
	{
		gParticleSystem.Clear();
		this.undoing = false;
	}
	
	this.IsUndoing = function()
	{
		return this.undoing;
	}
	
	this.Update = function()
	{
		if(this.enabled && this.undoing)
		{
			//call Undo every fixed step
			var time = gStatsSystem.GetCurrentTimeMs();
			if(time - this.lastUndoTime > this.kUndoTime)
			{
				this.Undo();
				this.lastUndoTime = time;
			}
		}
	}

	this.Undo = function()
	{
		var continueUndoing = true;
		while(continueUndoing)
		{
			var pos = this.GetPosOffset(-1);
			var record = this.undoRecords[pos];
			if(record)
			{
				if(this.undoRequested == false && this.undoExitPoint)
				{
					this.StopUndoing();
					return;
				}

				for(var i = 0; i < this.undoRequirements.length; i++)
				{
					if(this.undoRequirements[i].IsMet(record))
					{
						this.undoRequirements.splice(i--, 1);
					}
				}
				
				if(record.Requirement)
				{
					this.undoRequirements.push(record.Requirement());
				}

				record.Undo();
				gBlockSystem.SetFadingTimersForTime(record.time);
				gLevelSystem.time = record.time;
				this.undoRecords[pos] = null;
				this.nextUndoRecordIndex = pos;
				
				//Keep on undoing if next event recorded is less than 5 frames away
				var nextRecord = this.undoRecords[this.GetPosOffset(-1)];
				continueUndoing = !nextRecord || nextRecord && (record.time - nextRecord.time < 5);
				
				var canExitUndoing = this.CanExitUndoing(this.undoRequirements, record);
				this.undoExitPoint = canExitUndoing;
				if(this.undoRequested == false && this.undoExitPoint || this.CanUndo() == false)
				{
					this.StopUndoing();
					return;
				}
			}
			else
			{
				this.StopUndoing();
				return;
			}
		}
	}
	
	this.CanExitUndoing = function(requirements, record)
	{
		return requirements.length == 0 && (record instanceof UndoPlayerMovement || record instanceof UndoPlayerPush);
	}
	
	this.Debug = function()
	{
		if(this.enabled)
		{
			gDraw.Watch("UndoSystem " + gLevelSystem.time)
			gDraw.Watch(this.nextUndoRecordIndex + " in " + this.kUndoLength);
			for(var i = 1; i <= this.kUndoLength; i++)
			{
				var pos = this.GetPosOffset(-i);
				var record = this.undoRecords[pos];
				if(record)
				{
					gDraw.Watch(" [" + pos + "] @" + record.time + " - " + record.DebugString());
				}
			}
		}
		else
		{
			gDraw.Watch("UndoSystem Disabled")
		}
	}
}
