using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;

namespace m3uTool
{
	/// <summary>
	/// A generic encoder.  Creates an external process, sends it argument options,
	/// and acts as a wrapper for streaming reads and writes
	/// </summary>
	public abstract class Encoder : IDisposable
	{
		protected ProcessArguments mEncOpt;
		protected Process mEncoderProcess;
		protected const int mBufferLength = 16384;
		protected StreamWriter mOutputStreamWriter = null;
		protected IProgressCallback mProgressCallback = new DoNothingProgressCallback();
		protected const byte mEndOfStream = 26;

		/// <summary>
		/// Gets or sets the progress callback.
		/// </summary>
		/// <value>The progress callback.</value>
		public IProgressCallback ProgressCallback
		{
			get { return mProgressCallback; }
			set { mProgressCallback = value; }
		}

		/// <summary>
		/// Gets the process executable filename, such as "LAME.EXE", "FAAC.EXE", etc.
		/// </summary>
		/// <value>The process executable filename.</value>
		protected abstract string ProcessExecutableFilename
		{
			get;
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="encOpt"></param>
		public Encoder(ProcessArguments encOpt) : this (encOpt, null)
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="Encoder"/> class.
		/// </summary>
		/// <param name="encOpt">The enc opt.</param>
		/// <param name="outputStream">Output to this stream</param>
		public Encoder(ProcessArguments encOpt, Stream outputStream)
		{
			//if (!outputStream.CanWrite)
			//    throw new ArgumentException("Can't write to output stream");
			//if (!string.IsNullOrEmpty(encOpt.OutputFilename) && encOpt.OutputFilename.Trim() != "-")
			//    throw new ArgumentException("Trying to output to both a stream and a file " + encOpt.OutputFilename);
			//encOpt.OutputFilename = "-";
			
			mEncOpt = encOpt;
			
			mEncoderProcess = CreateEncoderProcess(mEncOpt, ProcessExecutableFilename);
			if (outputStream != null)
			{
				mOutputStreamWriter = new StreamWriter(outputStream, Encoding.Default);
				mEncoderProcess.StartInfo.RedirectStandardOutput = true;
			}
			mEncoderProcess.StartInfo.RedirectStandardError = true;

			mEncoderProcess.Start();
			if (mOutputStreamWriter != null)
			{
				Thread thread = new Thread(new ThreadStart(GetStandardOutput));
				thread.Start();
			}

			Thread standardErrorThread = new Thread(new ThreadStart(GetStandardError));
			standardErrorThread.Start();
		}
		
		private Process CreateEncoderProcess(ProcessArguments encOpt, string encoderFilename)
		{
			Process encoderProcess = new Process();
			encoderProcess.StartInfo.RedirectStandardInput = true;
			encoderProcess.StartInfo.UseShellExecute = false;
			encoderProcess.StartInfo.FileName = encoderFilename;
			encoderProcess.StartInfo.Arguments = encOpt.GetCommandLineArguments();
			encoderProcess.StartInfo.WorkingDirectory = Environment.CurrentDirectory;
			encoderProcess.StartInfo.CreateNoWindow = true;
			
			encoderProcess.EnableRaisingEvents = true;
			encoderProcess.Exited += new EventHandler(encoderProcess_Exited);
			return encoderProcess;
		}

		void encoderProcess_Exited(object sender, EventArgs e)
		{
			mProgressCallback.SetText("encoderProcess_Exited");
		}
		
		/// <summary>
		/// Sends the data to the underlying process through its input stream.
		/// </summary>
		/// <param name="buffer">The data to encode</param>
		/// <param name="count">How many characters to write</param>
		/// <param name="startIndex">The index to begin writing from</param>
		public void Encode(char[] buffer, int startIndex, int count)
		{
			// Debug.WriteLine("-");
			mEncoderProcess.StandardInput.Write(buffer, startIndex, count);
			if (count != mBufferLength)
				mProgressCallback.SetText("Encode : " + count);
		}

		public void WriteEndOfStream()
		{
			if (!mEncoderProcess.HasExited)
			{
				mEncoderProcess.StandardInput.BaseStream.Write(new byte[] { mEndOfStream }, 0, 1);
				mEncoderProcess.StandardInput.BaseStream.Flush();
			}
		}

		private static int GetPercentDone(long previousLeft, long currentLeft, long currentDone, int lastDone)
		{
			// if currentLeft > previousLeft - lastDone,
			//		addedLeft = previousLeft - lastDone - currentLeft
			// totalLength = previousLeft + currentDone + addedLeft

			long totalLength = currentLeft;
			if (currentLeft != previousLeft)
			{
				long leftDelta = previousLeft - lastDone - currentLeft;
				totalLength = previousLeft + currentDone + leftDelta;
			}
			try
			{
				return Convert.ToInt32((currentDone / (double)totalLength) * 100);
			}
			catch(Exception ex)
			{
				Debug.WriteLine(ex.ToString());
			}
			return 0;
		}
		
		/// <summary>
		/// Encodes all of the data in the input stream 
		/// </summary>
		/// <param name="inputStream"></param>
		public void Encode(Stream inputStream)
		{
			if (!inputStream.CanRead)
				throw new ArgumentException("Can't read from inputStream");

			mProgressCallback.SetText("Encode: StreamReader sr");

			long inputCount = inputStream.Length;
			long previousInputCount = inputStream.Length;
			long processCount = 0;
			int percentageComplete = 0;

			using (StreamReader sr = new StreamReader(inputStream, Encoding.Default))
			{
				mProgressCallback.Begin(0, 100);
				mProgressCallback.SetText(mEncOpt.GetCommandLineArguments());

				char[] buffer = new char[mBufferLength];
				
				while (!sr.EndOfStream)
				{
					int readLength = sr.Read(buffer, 0, mBufferLength);
					if (buffer[readLength - 1] == mEndOfStream)
						readLength--;
					Encode(buffer, 0, readLength);

					// update progress
					processCount += readLength;
					previousInputCount = inputCount;
					inputCount = inputStream.Length;
					int currentPercentageComplete = GetPercentDone(previousInputCount, inputCount, processCount, readLength);
					if (currentPercentageComplete != percentageComplete)
					{
						percentageComplete = currentPercentageComplete;
						mProgressCallback.StepTo(percentageComplete);
					}
				}
			}
			mProgressCallback.SetText("Encode: Done!");
		}
		
		/// <summary>
		/// Open this file and encode it.
		/// </summary>
		/// <param name="filename"></param>
		public void Encode(string filename)
		{
			using (FileStream inputStream = File.OpenRead(filename))
			{
				Encode(inputStream);
			}
		}
		private void GetStandardOutput()
		{
			char[] writeBuffer = new char[mBufferLength];
			try
			{
				while (!mEncoderProcess.StandardOutput.EndOfStream)
				{
					int readLength = mEncoderProcess.StandardOutput.Read(writeBuffer, 0, writeBuffer.Length);
					mOutputStreamWriter.Write(writeBuffer, 0, readLength);
					//Debug.WriteLine("GetStandardOutput() " + "mOutputStreamWriter.Write : " + readLength);
				}
			}
			catch(Exception ex)
			{
				mProgressCallback.SetText(ex.ToString());
			}
			mProgressCallback.SetText("GetStandardOutput() " + "Done!");
		}

		private void GetStandardError()
		{
			// char[] writeBuffer = new char[10];
			try
			{
				while (!mEncoderProcess.StandardError.EndOfStream)
				{
					string line = mEncoderProcess.StandardError.ReadLine();
					//int readLength = mEncoderProcess.StandardError.Read(writeBuffer, 0, writeBuffer.Length);
					Debug.WriteLine("GetStandardError() " + line);
				}
			}
			catch (Exception ex)
			{
				mProgressCallback.SetText(ex.ToString());
			}
			mProgressCallback.SetText("GetStandardError() " + "Done!");
		}
		#region IDisposable Members

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			int count = 0;
			mProgressCallback.SetText("Dispose()");
			while (mEncoderProcess != null && !mEncoderProcess.HasExited)
			{
				//WriteEndOfStream();
				//Debug.Write(".");
				if (count++ > 50)
					mEncoderProcess.Kill();
				Thread.Sleep(10);
			}
			mEncoderProcess.WaitForExit();
			if (mOutputStreamWriter != null)
				mOutputStreamWriter.Flush();
			//if (mEncoderProcess.ExitCode != 0)
			//    throw new ApplicationException("Exit code " + mEncoderProcess.ExitCode);
			mProgressCallback.SetText("Dispose() with exit code " + mEncoderProcess.ExitCode);


			//if (mEncoderProcess != null && mEncoderProcess.Responding)
			//    mEncoderProcess.Close();
		}

		#endregion
	}
}
