using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System;

namespace BizHawk.Client.Common
{
	public class TasMovieChangeLog
	{
		List<List<IMovieAction>> History;
		public List<string> Names;
		public int UndoIndex = -1;
		int _maxSteps = 100;
		public int MaxSteps
		{
			get { return _maxSteps; }
			set
			{
				_maxSteps = value;
				if (History.Count > value)
				{
					if (History.Count <= value)
						ClearLog();
					else
						ClearLog(History.Count - value);
				}
			}
		}

		private int _totalSteps = 0;

		private bool RecordingBatch = false;
		/// <summary>
		/// This is not intended to turn off the ChangeLog, but to disable the normal recording process.
		/// Use this to manually control the ChangeLog. (Useful for when you are making lots of 
		/// </summary>
		public bool IsRecording = true;

		public TasMovie Movie;
		public TasMovieChangeLog(TasMovie movie)
		{
			History = new List<List<IMovieAction>>();
			Names = new List<string>();
			Movie = movie;
		}

		public void ClearLog(int upTo = -1)
		{
			if (upTo == -1)
				upTo = History.Count;

			History.RemoveRange(0, upTo);
			Names.RemoveRange(0, upTo);
			UndoIndex -= upTo;
			if (UndoIndex < -1)
				UndoIndex = -1;

			if (History.Count == 0)
				RecordingBatch = false;
		}
		private void TruncateLog(int from)
		{
			History.RemoveRange(from, History.Count - from);
			Names.RemoveRange(from, Names.Count - from);

			if (UndoIndex < History.Count - 1)
				UndoIndex = History.Count - 1;

			if (RecordingBatch)
			{
				RecordingBatch = false;
				BeginNewBatch();
			}
		}

		/// <summary>
		/// All changes made between calling Begin and End will be one Undo.
		/// If already recording in a batch, calls EndBatch.
		/// </summary>
		/// <param name="keepOldBatch">If set and a batch is in progress, a new batch will not be created.</param>
		/// <returns>Returns true if a new batch was started; otherwise false.</returns>
		public bool BeginNewBatch(string name = "", bool keepOldBatch = false)
		{
			if (!IsRecording)
				return false;

			bool ret = true;
			if (RecordingBatch)
			{
				if (keepOldBatch)
					ret = false;
				else
					EndBatch();
			}

			if (ret)
			{
				ret = AddMovieAction(name);
			}
			RecordingBatch = true;

			return ret;
		}
		/// <summary>
		/// Ends the current undo batch. Future changes will be one undo each.
		/// If not already recording a batch, does nothing.
		/// </summary>
		public void EndBatch()
		{
			if (!IsRecording || !RecordingBatch)
				return;

			RecordingBatch = false;
			List<IMovieAction> last = History.Last();
			if (last.Count == 0) // Remove batch if it's empty.
			{
				History.RemoveAt(History.Count - 1);
				Names.RemoveAt(Names.Count - 1);
				UndoIndex--;
			}
			else
				last.Capacity = last.Count;
		}

		/// <summary>
		/// Undoes the most recent action batch, if any exist.
		/// </summary>
		/// <returns>Returns the frame which the movie needs to rewind to.</returns>
		public int Undo()
		{
			if (UndoIndex == -1)
				return Movie.InputLogLength;

			List<IMovieAction> batch = History[UndoIndex];
			for (int i = batch.Count - 1; i >= 0; i--)
				batch[i].Undo(Movie);
			UndoIndex--;

			RecordingBatch = false;

			if (!batch.Where(a => a.GetType() != typeof(MovieActionMarker)).Any())
				return Movie.InputLogLength;

			return PreviousUndoFrame;
		}
		/// <summary>
		/// Redoes the most recent undo, if any exist.
		/// </summary>
		/// <returns>Returns the frame which the movie needs to rewind to.</returns>
		public int Redo()
		{
			if (UndoIndex == History.Count - 1)
				return Movie.InputLogLength;

			UndoIndex++;
			List<IMovieAction> batch = History[UndoIndex];
			for (int i = 0; i < batch.Count; i++)
				batch[i].Redo(Movie);

			RecordingBatch = false;

			if (!batch.Where(a => a.GetType() != typeof(MovieActionMarker)).Any())
				return Movie.InputLogLength;

			return PreviousRedoFrame;
		}

		public bool CanUndo { get { return UndoIndex > -1; } }
		public bool CanRedo { get { return UndoIndex < History.Count - 1; } }

		public string NextUndoStepName
		{
			get
			{
				if (Names.Count == 0)
					return null;
				else
					return Names[UndoIndex];
			}
		}

		public int PreviousUndoFrame
		{
			get
			{
				if (UndoIndex == History.Count - 1)
					return Movie.InputLogLength;

				if (History[UndoIndex + 1].Count == 0)
					return Movie.InputLogLength;

				return History[UndoIndex + 1].Min(a => a.FirstFrame);
			}
		}
		public int PreviousRedoFrame
		{
			get
			{
				if (UndoIndex == -1)
					return Movie.InputLogLength;

				if (History[UndoIndex].Count == 0)
					return Movie.InputLogLength;

				return History[UndoIndex].Min(a => a.FirstFrame);
			}
		}

		#region "Change History"
		private bool AddMovieAction(string name)
		{
			if (UndoIndex + 1 != History.Count)
				TruncateLog(UndoIndex + 1);

			if (name == "")
				name = "Undo step " + _totalSteps;

			bool ret = false;
			if (!RecordingBatch)
			{
				ret = true;
				History.Add(new List<IMovieAction>(1));
				Names.Add(name);
				_totalSteps += 1;

				if (History.Count <= MaxSteps)
					UndoIndex += 1;
				else
				{
					History.RemoveAt(0);
					Names.RemoveAt(0);
					ret = false;
				}
			}

			return ret;
		}
		public void SetName(string name)
		{
			Names[Names.Count - 1] = name;
		}

		// TODO: These probably aren't the best way to handle undo/redo.
		private int lastGeneral;
		public void AddGeneralUndo(int first, int last, string name = "", bool force = false)
		{
			if (IsRecording || force)
			{
				AddMovieAction(name);
				History.Last().Add(new MovieAction(first, last, Movie));
				lastGeneral = History.Last().Count - 1;
			}
		}
		public void SetGeneralRedo(bool force = false)
		{
			if (IsRecording || force)
			{
				(History.Last()[lastGeneral] as MovieAction).SetRedoLog(Movie);
			}
		}

		public void AddBoolToggle(int frame, string button, bool oldState, string name = "", bool force = false)
		{
			if (IsRecording || force)
			{
				AddMovieAction(name);
				History.Last().Add(new MovieActionFrameEdit(frame, button, oldState, !oldState));
			}
		}

		public void AddFloatChange(int frame, string button, float oldState, float newState, string name = "", bool force = false)
		{
			if (IsRecording || force)
			{
				AddMovieAction(name);
				History.Last().Add(new MovieActionFrameEdit(frame, button, oldState, newState));
			}
		}

		public void AddMarkerChange(TasMovieMarker newMarker, int oldPosition = -1, string old_message = "", string name = "", bool force = false)
		{
			if (IsRecording || force)
			{
				AddMovieAction(name);
				History.Last().Add(new MovieActionMarker(newMarker, oldPosition, old_message));
			}
		}

		public void AddInputBind(int frame, bool isDelete, string name = "", bool force = false)
		{
			if (IsRecording || force)
			{
				AddMovieAction(name);
				History.Last().Add(new MovieActionBindInput(Movie, frame, isDelete));
			}
		}
		#endregion
	}

	#region "Classes"
	public interface IMovieAction
	{
		void Undo(TasMovie movie);
		void Redo(TasMovie movie);

		int FirstFrame { get; }
		int LastFrame { get; }
	}

	public class MovieAction : IMovieAction
	{
		public int FirstFrame { get; private set; }
		public int LastFrame { get; private set; }
		private int undoLength;
		private int redoLength;
		private int length
		{ get { return LastFrame - FirstFrame + 1; } }
		private List<string> oldLog;
		private List<string> newLog;
		private bool bindMarkers;

		public MovieAction(int firstFrame, int lastFrame, TasMovie movie)
		{
			FirstFrame = firstFrame;
			LastFrame = lastFrame;
			oldLog = new List<string>(length);

			undoLength = Math.Min(lastFrame + 1, movie.InputLogLength) - firstFrame;
			for (int i = 0; i < undoLength; i++)
				oldLog.Add(movie.GetLogEntries()[FirstFrame + i]);

			bindMarkers = movie.BindMarkersToInput;
		}
		public void SetRedoLog(TasMovie movie)
		{
			redoLength = Math.Min(LastFrame + 1, movie.InputLogLength) - FirstFrame;
			newLog = new List<string>();
			for (int i = 0; i < redoLength; i++)
				newLog.Add(movie.GetLogEntries()[FirstFrame + i]);
		}

		public void Undo(TasMovie movie)
		{
			bool wasRecording = movie.ChangeLog.IsRecording;
			bool wasBinding = movie.BindMarkersToInput;
			movie.ChangeLog.IsRecording = false;
			movie.BindMarkersToInput = bindMarkers;

			if (redoLength != length)
				movie.InsertEmptyFrame(movie.InputLogLength, length - redoLength);

			for (int i = 0; i < undoLength; i++)
				movie.SetFrame(FirstFrame + i, oldLog[i]);

			if (undoLength != length)
				movie.RemoveFrames(FirstFrame + undoLength, movie.InputLogLength);

			movie.ChangeLog.IsRecording = wasRecording;
			movie.BindMarkersToInput = bindMarkers;
		}
		public void Redo(TasMovie movie)
		{
			bool wasRecording = movie.ChangeLog.IsRecording;
			bool wasBinding = movie.BindMarkersToInput;
			movie.ChangeLog.IsRecording = false;
			movie.BindMarkersToInput = bindMarkers;

			if (undoLength != length)
				movie.InsertEmptyFrame(movie.InputLogLength, length - undoLength);

			for (int i = 0; i < redoLength; i++)
				movie.SetFrame(FirstFrame + i, newLog[i]);

			if (redoLength != length)
				movie.RemoveFrames(FirstFrame + redoLength, movie.InputLogLength);

			movie.ChangeLog.IsRecording = wasRecording;
			movie.BindMarkersToInput = bindMarkers;
		}
	}

	public class MovieActionMarker : IMovieAction
	{
		public int FirstFrame { get; private set; }
		public int LastFrame { get; private set; }

		private string oldMessage;
		private string newMessage;

		public MovieActionMarker(TasMovieMarker marker, int oldPosition = -1, string old_message = "")
		{
			FirstFrame = oldPosition;
			if (marker == null)
			{
				LastFrame = -1;
				oldMessage = old_message;
			}
			else
			{
				LastFrame = marker.Frame;
				if (old_message == "")
					oldMessage = marker.Message;
				else
					oldMessage = old_message;
				newMessage = marker.Message;
			}
		}

		public void Undo(TasMovie movie)
		{
			if (FirstFrame == -1) // Action: Place marker
				movie.Markers.Remove(movie.Markers.Get(LastFrame));
			else if (LastFrame == -1) // Action: Remove marker
				movie.Markers.Add(FirstFrame, oldMessage);
			else // Action: Move/rename marker
			{
				movie.Markers.Move(LastFrame, FirstFrame);
				movie.Markers.Get(LastFrame).Message = oldMessage;
			}
		}
		public void Redo(TasMovie movie)
		{
			if (FirstFrame == -1) // Action: Place marker
				movie.Markers.Add(LastFrame, oldMessage);
			else if (LastFrame == -1) // Action: Remove marker
				movie.Markers.Remove(movie.Markers.Get(FirstFrame));
			else // Action: Move/rename marker
			{
				movie.Markers.Move(FirstFrame, LastFrame);
				movie.Markers.Get(LastFrame).Message = newMessage;
			}
		}
	}

	public class MovieActionFrameEdit : IMovieAction
	{
		public int FirstFrame { get; private set; }
		public int LastFrame { get { return FirstFrame; } }
		private float oldState, newState;
		private string buttonName;
		private bool isFloat = false;

		public MovieActionFrameEdit(int frame, string button, bool oldS, bool newS)
		{
			oldState = oldS ? 1 : 0;
			newState = newS ? 1 : 0;
			FirstFrame = frame;
			buttonName = button;
		}
		public MovieActionFrameEdit(int frame, string button, float oldS, float newS)
		{
			oldState = oldS;
			newState = newS;
			FirstFrame = frame;
			buttonName = button;
			isFloat = true;
		}

		public void Undo(TasMovie movie)
		{
			bool wasRecording = movie.ChangeLog.IsRecording;
			movie.ChangeLog.IsRecording = false;

			if (isFloat)
				movie.SetFloatState(FirstFrame, buttonName, oldState);
			else
				movie.SetBoolState(FirstFrame, buttonName, oldState == 1);

			movie.ChangeLog.IsRecording = wasRecording;
		}
		public void Redo(TasMovie movie)
		{
			bool wasRecording = movie.ChangeLog.IsRecording;
			movie.ChangeLog.IsRecording = false;

			if (isFloat)
				movie.SetFloatState(FirstFrame, buttonName, newState);
			else
				movie.SetBoolState(FirstFrame, buttonName, newState == 1);

			movie.ChangeLog.IsRecording = wasRecording;
		}
	}

	public class MovieActionPaint : IMovieAction
	{
		public int FirstFrame { get; private set; }
		public int LastFrame { get; private set; }
		private List<float> oldState;
		private float newState;
		private string buttonName;
		private bool isFloat = false;

		public MovieActionPaint(int startFrame, int endFrame, string button, bool newS, TasMovie movie)
		{
			newState = newS ? 1 : 0;
			FirstFrame = startFrame;
			LastFrame = endFrame;
			buttonName = button;
			oldState = new List<float>(endFrame - startFrame + 1);

			for (int i = 0; i < endFrame - startFrame + 1; i++)
				oldState.Add(movie.BoolIsPressed(startFrame + i, button) ? 1 : 0);
		}
		public MovieActionPaint(int startFrame, int endFrame, string button, float newS, TasMovie movie)
		{
			newState = newS;
			FirstFrame = startFrame;
			LastFrame = endFrame;
			buttonName = button;
			isFloat = true;
			oldState = new List<float>(endFrame - startFrame + 1);

			for (int i = 0; i < endFrame - startFrame + 1; i++)
				oldState.Add(movie.BoolIsPressed(startFrame + i, button) ? 1 : 0);
		}

		public void Undo(TasMovie movie)
		{
			bool wasRecording = movie.ChangeLog.IsRecording;
			movie.ChangeLog.IsRecording = false;

			if (isFloat)
			{
				for (int i = 0; i < oldState.Count; i++)
					movie.SetFloatState(FirstFrame + i, buttonName, oldState[i]);
			}
			else
			{
				for (int i = 0; i < oldState.Count; i++)
					movie.SetBoolState(FirstFrame + i, buttonName, oldState[i] == 1);
			}

			movie.ChangeLog.IsRecording = wasRecording;
		}
		public void Redo(TasMovie movie)
		{
			bool wasRecording = movie.ChangeLog.IsRecording;
			movie.ChangeLog.IsRecording = false;

			if (isFloat)
				movie.SetFloatStates(FirstFrame, LastFrame - FirstFrame + 1, buttonName, newState);
			else
				movie.SetBoolStates(FirstFrame, LastFrame - FirstFrame + 1, buttonName, newState == 1);

			movie.ChangeLog.IsRecording = wasRecording;
		}
	}

	public class MovieActionBindInput : IMovieAction
	{
		public int FirstFrame { get; private set; }
		public int LastFrame { get; private set; }

		private string log;
		private bool delete;

		private bool bindMarkers;

		public MovieActionBindInput(TasMovie movie, int frame, bool isDelete)
		{
			FirstFrame = LastFrame = frame;
			log = movie.GetInputLogEntry(frame);
			delete = isDelete;
			bindMarkers = movie.BindMarkersToInput;
		}

		public void Undo(TasMovie movie)
		{
			bool wasRecording = movie.ChangeLog.IsRecording;
			bool wasBinding = movie.BindMarkersToInput;
			movie.ChangeLog.IsRecording = false;
			movie.BindMarkersToInput = bindMarkers;

			if (delete) // Insert
			{
				movie.InsertInput(FirstFrame, log);
				movie.InsertLagHistory(FirstFrame + 1, true);
			}
			else // Delete
			{
				movie.RemoveFrame(FirstFrame);
				movie.RemoveLagHistory(FirstFrame + 1);
			}

			movie.ChangeLog.IsRecording = wasRecording;
			movie.BindMarkersToInput = bindMarkers;
		}
		public void Redo(TasMovie movie)
		{
			bool wasRecording = movie.ChangeLog.IsRecording;
			bool wasBinding = movie.BindMarkersToInput;
			movie.ChangeLog.IsRecording = false;
			movie.BindMarkersToInput = bindMarkers;

			if (delete)
			{
				movie.RemoveFrame(FirstFrame);
				movie.RemoveLagHistory(FirstFrame + 1);
			}
			else
			{
				movie.InsertInput(FirstFrame, log);
				movie.InsertLagHistory(FirstFrame + 1, true);
			}

			movie.ChangeLog.IsRecording = wasRecording;
			movie.BindMarkersToInput = bindMarkers;
		}
	}
	#endregion
}