using System;
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 ProcessStreamWrapper : IDisposable
    {
        #region Protected Variables

        /// <summary>
        /// How much data to buffer when reading and writing
        /// </summary>
        protected const int mBufferLength = 1000;

        /// <summary>
        /// The output stream writer.  If it is null, it is assumed
        /// that the process will take care of output itself.
        /// </summary>
        protected StreamWriter mOutputStreamWriter;

        /// <summary>
        /// The process
        /// </summary>
        protected Process mProcess;

        /// <summary>
        /// The arguments to use for the process
        /// </summary>
        protected ProcessArguments mProcessArguments;

        /// <summary>
        /// Progress call-back, for status updates outside this class.
        /// </summary>
        protected IProgressCallback mProgressCallback = new DoNothingProgressCallback();

        #endregion

        #region Properties

        /// <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; }

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="ProcessStreamWrapper"/> class.
        /// </summary>
        /// <param name="encOpt">The enc opt.</param>
        public ProcessStreamWrapper(ProcessArguments encOpt) : this(encOpt, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ProcessStreamWrapper"/> class.
        /// </summary>
        /// <param name="encOpt">The enc opt.</param>
        /// <param name="outputStream">Output to this stream</param>
        public ProcessStreamWrapper(ProcessArguments encOpt, Stream outputStream)
        {
            mProcessArguments = encOpt;

            mProcess = CreateProcess(mProcessArguments, ProcessExecutableFilename);
            if (outputStream != null)
            {
                mOutputStreamWriter = new StreamWriter(outputStream, Encoding.Default);
                mProcess.StartInfo.RedirectStandardOutput = true;
                mProcess.StartInfo.StandardOutputEncoding = Encoding.Default;
            }
            mProcess.StartInfo.RedirectStandardError = true;

            mProcess.Exited += Process_Exited;
            mProcess.Start();
            if (mOutputStreamWriter != null)
            {
                var thread = new Thread(GetStandardOutput);
                thread.Start();
            }

            var standardErrorThread = new Thread(GetStandardError);
            standardErrorThread.Start();
        }

        #endregion

        #region Static methods

        /// <summary>
        /// Creates a process given an executable and some process arguments
        /// </summary>
        /// <param name="processArguments">The enc opt.</param>
        /// <param name="executable">The executable filename.</param>
        /// <returns></returns>
        private static Process CreateProcess(ProcessArguments processArguments, string executable)
        {
            var process = new Process();
            process.StartInfo.RedirectStandardInput = true;
            process.StartInfo.UseShellExecute = false;
            process.StartInfo.FileName = executable;
            process.StartInfo.Arguments = processArguments.GetCommandLineArguments();
            process.StartInfo.WorkingDirectory = Environment.CurrentDirectory;
            process.StartInfo.CreateNoWindow = true;

            process.EnableRaisingEvents = true;
            return process;
        }

        #endregion

        #region Event handlers

        /// <summary>
        /// Handles the Exited event of the Process.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
        private void Process_Exited(object sender, EventArgs e)
        {
            mProgressCallback.SetText("Process_Exited");
        }

        #endregion

        #region Utility methods

        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;
        }

        #endregion

        #region Public methods

        /// <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 ProcessInput(char[] buffer, int startIndex, int count)
        {
            mProcess.StandardInput.Write(buffer, startIndex, count);
        }

        /// <summary>
        /// Sends the data from the input stream to the process
        /// </summary>
        /// <param name="inputStream"></param>
        public void ProcessInput(Stream inputStream)
        {
            if (!inputStream.CanRead)
                throw new ArgumentException("Can't read from inputStream");

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

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

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

                var buffer = new char[mBufferLength];

                while (!sr.EndOfStream)
                {
                    int readLength = sr.Read(buffer, 0, mBufferLength);
                    ProcessInput(buffer, 0, readLength);

                    // update progress
                    wroteCount += readLength;
                    previousInputCount = inputCount;
                    inputCount = inputStream.Length;
                    int currentPercentageComplete = GetPercentDone(previousInputCount, inputCount, wroteCount,
                                                                   readLength);
                    if (currentPercentageComplete != percentageComplete)
                    {
                        percentageComplete = currentPercentageComplete;
                        mProgressCallback.StepTo(percentageComplete);
                    }
                }
            }


            mInputDone = true;
            mProgressCallback.SetText("ProcessInput: Done. " + String.Format("Wrote: {0}", wroteCount));
        }


        /// <summary>
        /// Open this file and encode it.
        /// </summary>
        /// <param name="filename"></param>
        public void ProcessInput(string filename)
        {
            using (FileStream inputStream = File.OpenRead(filename))
            {
                ProcessInput(inputStream);
            }
        }

        #endregion

        #region Threaded methods

        /// <summary>
        /// TEMP: this should be removed once GetStandardOutput is working
        /// </summary>
        private bool mInputDone;

        /// <summary>
        /// Collects all the data from Process.StandardOutput and sends it to the outputstream writer
        /// </summary>
        private void GetStandardOutput()
        {
            int readTotal = 0;
            var writeBuffer = new char[mBufferLength];

            try
            {
                while (!mProcess.StandardOutput.EndOfStream)
                    //while (mProcess.StandardOutput.Peek() > -1)
                {
                    mProgressCallback.SetText("- Before read");
                    int readLength = mProcess.StandardOutput.Read(writeBuffer, 0, writeBuffer.Length);
                    mProgressCallback.SetText("- After read: " + readLength);
                    mOutputStreamWriter.Write(writeBuffer, 0, readLength);
                    if (mInputDone)
                        mProgressCallback.SetText(" StandardOutput: " + new string(writeBuffer, 0, readLength));
                    readTotal += readLength;
                    mProgressCallback.SetText("Read total = " + readTotal);

                    //mProcess.StandardInput.Flush();
                    //mProcess.StandardOutput.BaseStream.Flush();
                }
            }
            catch (Exception ex)
            {
                mProgressCallback.SetText(ex.ToString());
            }
            // mOutputStreamWriter.Flush();
            mProgressCallback.SetText("GetStandardOutput() " + "Done!" + "  StandardOutput.EndOfStream = " +
                                      mProcess.StandardOutput.EndOfStream);
        }

        /* Asynchronous implementation of GetStandardOutput
		//int mReadTotal = 0;
		//private byte[] mBuffer = new byte[mBufferLength];
		//private void GetStandardOutputAsync()
		//{
		//    mProcess.StandardOutput.BaseStream.BeginRead(mBuffer, 0, mBufferLength, GetStandardOutputCallback, 0);
		//}

		//private void GetStandardOutputCallback(IAsyncResult asyncResult)
		//{
		//    mProgressCallback.SetText("- Before read:");
		//    int readLength = mProcess.StandardOutput.BaseStream.EndRead(asyncResult);
		//    mProgressCallback.SetText("- After read:" + readLength);
		//    mOutputStreamWriter.BaseStream.Write(mBuffer, 0, readLength);
		//    mReadTotal += readLength;
		//    mProgressCallback.SetText("Read total = " + mReadTotal);
		//    mProcess.StandardOutput.BaseStream.BeginRead(mBuffer, 0, mBufferLength, GetStandardOutputCallback, 0);
		//}
		 */

        /// <summary>
        /// Collects all the data from Process.StandardError and sends it to the progress callback
        /// </summary>
        private void GetStandardError()
        {
            try
            {
                while (!mProcess.StandardError.EndOfStream)
                {
                    string line = mProcess.StandardError.ReadLine();
                    mProgressCallback.SetText("GetStandardError() " + line);
                }
            }
            catch (Exception ex)
            {
                mProgressCallback.SetText(ex.ToString());
            }
            mProgressCallback.SetText("GetStandardError() " + "Done!");
        }

        #endregion

        #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 (mProcess != null && !mProcess.HasExited)
            {
                mProcess.WaitForExit(2);
                if (count++ > mBufferLength && !mProcess.HasExited)
                {
                    mProcess.Kill();
                    mProgressCallback.SetText("!!! Process.Kill()");
                }
                else
                {
                    // mProgressCallback.SetText("Writing EOF");
                    mProcess.StandardInput.Write(new string(new[] {(char) 0x1A, '\n'}));
                }
            }

            mProgressCallback.SetText("Dispose() with exit code " + mProcess.ExitCode);
        }

        #endregion
    }
}