using System;
using System.ComponentModel;
using System.Text;
using System.Threading;

/*
    Copyright (c) 2011 flixmonk. (http://DVDSwarm.codeplex.com)
    All Rights Reserved.
        
    Redistribution and use in source and binary forms, with or without modification,
    are permitted provided that the following conditions are met:
        
    * Redistributions of source code must retain the above copyright notice, this list
      of conditions and the following disclaimer.
        
    * Redistributions in binary form must reproduce the above copyright notice, this
    list of conditions and the following disclaimer in the documentation and/or other
    materials provided with the distribution. 
        
    * Neither the name of The Flixmonk nor the names of its contributors may be used to
    endorse or promote products derived from this software without specific prior written
    permission.
        
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
    EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
    OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
    SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
    OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
    HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
    TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
namespace DVDSwarm.Tools.Matroska

{
    /// <summary>
    /// Utilizes a BackgroundWorker to run MKVMerge in a seperate thread.
    /// </summary>
    sealed public class MkvmergeBackgroundWorker
    {
        #region Fields

        private string _appFilename = string.Empty;
        private StringBuilder _arguments = new StringBuilder();
        private Int32[] _audioDelays = null;
        private string[] _audioFilenames = null;
        private DateTime _lastRaisedDateTime = DateTime.Now;
        private string _outputFilename = string.Empty;
        private System.Diagnostics.Process _process = null;
        private Int32 _progressPercent = 0;
        private string _sourceFilename = string.Empty;
        private VideoFramerate _sourceFramerate = VideoFramerate.Fps25;
        private Int32 _sourceHeight = 0;
        private Int32 _sourceWidth = 0;
        private JobStatus _status = JobStatus.Inactive;
        private BackgroundWorker _worker = null;

        #endregion

        #region Properties

        /// <summary>
        /// Gets the CLI arguments.
        /// </summary>
        public string Arguments
        {
            get
            {
                return _arguments.ToString();
            }
        }
        /// <summary>
        /// Gets the percentage of the operation completed. The value range is 0 to 100.
        /// </summary>
        public Int32 ProgressPercent
        {
            get
            {
                return _progressPercent;
            }
        }
        /// <summary>
        /// Gets the source video filename.
        /// </summary>
        public string SourceFilename
        {
            get
            {
                return _sourceFilename;
            }
        }
        /// <summary>
        /// Gets the status of the worker.
        /// </summary>
        public JobStatus Status
        {
            get
            {
                return _status;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Creates a new instance of the MkvmergeBackgroundWorker class to package the video
        /// and audio tracks.
        /// </summary>
        /// <param name="appFilename">The application path including the filename.</param>
        /// <param name="source">The source video filename.</param>
        /// <param name="output">The output package filename.</param>
        /// <param name="width">The width of the source video in pixels.</param>
        /// <param name="height">The height of the source video in pixels.</param>
        /// <param name="framerate">The frame rate of the source video.</param>
        /// <param name="audioFilenames">A string array containing audio filenames.</param>
        /// <param name="audioDelays">An Int32 array containing audio delay values in milliseconds.</param>
        /// <exception cref="System.ArgumentException">Thrown when appFilename is null or empty.</exception>
        /// <exception cref="System.ArgumentException">Thrown when source is null or empty.</exception>
        /// <exception cref="System.IO.FileNotFoundException">Thrown when appFilename is not found.</exception>
        /// <exception cref="System.IO.FileNotFoundException">Thrown when source is not found.</exception>
        /// <exception cref="System.ArgumentException">Thrown when output is null or empty.</exception>
        /// <exception cref="System.ArgumentException">Thrown when appFilename is null or empty.</exception>
        /// <exception cref="System.ArgumentException">Thrown when audioFilenames and audioDelays do not have identical lengths.</exception>
        /// <exception cref="System.IO.FileNotFoundException">Thrown when any member of audioFilenames is null or empty.</exception>
        /// <exception cref="System.IO.FileNotFoundException">Thrown when any member of audioFilenames is not found.</exception>
        public MkvmergeBackgroundWorker(string appFilename, string source, string output, Int32 width, Int32 height,
            VideoFramerate framerate, string[] audioFilenames, Int32[] audioDelays)
        {
            if (string.IsNullOrEmpty(appFilename))
            {
                throw new ArgumentException("appFilename is null or empty.");
            }
            if (string.IsNullOrEmpty(source))
            {
                throw new ArgumentException("source is null or empty.");
            }
            if (!System.IO.File.Exists(appFilename))
            {
                throw new System.IO.FileNotFoundException("'" + appFilename + "' not found.");
            }
            if (!System.IO.File.Exists(source))
            {
                throw new System.IO.FileNotFoundException("'" + source + "' not found.");
            }
            if (string.IsNullOrEmpty(output))
            {
                throw new ArgumentException("output is null or empty.");
            }
            if (audioFilenames.Length != audioDelays.Length)
            {
                throw new ArgumentException("audioFilenames and audioDelays must have identical lengths.");
            }

            // Verify audio tracks.
            if (audioFilenames.Length > 0)
            {
                for (Int32 i = 0; i < audioFilenames.Length; i++)
                {
                    if (string.IsNullOrEmpty(audioFilenames[i]))
                    {
                        throw new ArgumentException("audioFilenames[" + i.ToString() + "] is null or empty.");
                    }
                    if (!System.IO.File.Exists(audioFilenames[i]))
                    {
                        throw new System.IO.FileNotFoundException("audioFilenames[" + i.ToString() + "] not found.");
                    }
                }

                _audioDelays = new Int32[audioDelays.Length];
                audioDelays.CopyTo(_audioDelays, 0);

                _audioFilenames = new string[audioFilenames.Length];
                audioFilenames.CopyTo(_audioFilenames, 0);
            }

            _appFilename = appFilename;
            _outputFilename = output;
            _sourceFilename = source;
            _sourceFramerate = framerate;
            _sourceHeight = height;
            _sourceWidth = width;

            // Build arguments. Video track number is one. Audio tracks star at zero.
            _arguments.Append("-o \"" + _outputFilename + "\" --default-duration 1:");
            _arguments.Append(Utility.ConvertVideoFramerateToValue(_sourceFramerate, true).ToString("0.000") + "fps");

            // Add video resolution.
            _arguments.Append(" --display-dimensions 1:" + _sourceWidth.ToString() + "x" + _sourceHeight.ToString());

            // Add video source.
            _arguments.Append(" \"" + _sourceFilename + "\"");

            // Add audio tracks with delays.
            if (audioFilenames.Length > 0)
            {
                for (Int32 i = 0; i < _audioFilenames.Length; i++)
                {
                    _arguments.Append(" --sync 0:" + _audioDelays[i].ToString() + "ms \"" + _audioFilenames[i] + "\"");
                }
            }
        }

        /// <summary>
        /// Cancels the worker.
        /// </summary>
        public void Cancel()
        {
            if (_worker != null && _process != null)
            {
                _status = JobStatus.Cancelled;

                _worker.CancelAsync();

                // Unhook events.
                _process.CancelErrorRead();
                _process.OutputDataReceived -= new System.Diagnostics.DataReceivedEventHandler(process_OutputDataReceived);

                // End process.
                _process.Kill();
                _process.Dispose();
                _process = null;
            }
        }

        /// <summary>
        /// Starts the worker.
        /// </summary>
        public void Start()
        {
            _worker = new BackgroundWorker();
            _worker.WorkerSupportsCancellation = true;
            _worker.DoWork += new DoWorkEventHandler(worker_DoWork);
            _worker.RunWorkerAsync();
        }

        #endregion

        #region Published Events

        /// <summary>
        /// Raised when the worker has exited.
        /// </summary>
        public event EventHandler Completed;

        /// <summary>
        /// Raises the OnCompleted event.
        /// </summary>
        public void OnCompleted()
        {
            // Event assigned?
            if (Completed != null && _worker != null)
            {
                // Throttle event if it has been less than 50 milliseconds since we last raised an event.
                if (DateTime.Now.Subtract(_lastRaisedDateTime).Milliseconds < 50)
                {
                    Thread.Sleep(50);
                }

                _lastRaisedDateTime = DateTime.Now;
                Completed(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Raised when the progress percent has updated.
        /// </summary>
        public event EventHandler Progress;

        /// <summary>
        /// Raises the OnProgress event.
        /// </summary>
        public void OnProgress()
        {
            // Event assigned?
            if (Progress != null && _worker != null)
            {
                // Throttle event if it has been less than 50 milliseconds since we last raised an event.
                if (DateTime.Now.Subtract(_lastRaisedDateTime).Milliseconds < 50)
                {
                    Thread.Sleep(50);
                }

                _lastRaisedDateTime = DateTime.Now;
                Progress(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Raised when the worker has started.
        /// </summary>
        public event EventHandler Started;

        /// <summary>
        /// Raises the OnStarted event.
        /// </summary>
        public void OnStarted()
        {
            // Event assigned?
            if (Started != null && _worker != null)
            {
                // Throttle event if it has been less than 50 milliseconds since we last raised an event.
                if (DateTime.Now.Subtract(_lastRaisedDateTime).Milliseconds < 50)
                {
                    Thread.Sleep(50);
                }

                _lastRaisedDateTime = DateTime.Now;
                Started(this, EventArgs.Empty);
            }
        }

        #endregion

        #region Subscribed Events

        /// <summary>
        /// Reports the worker progress.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The event arguments.</param>
        private void process_OutputDataReceived(object sender, System.Diagnostics.DataReceivedEventArgs e)
        {
            if (string.IsNullOrEmpty(e.Data) || _process == null || _status == JobStatus.Cancelled)
            {
                return;
            }

            if (e.Data.StartsWith("Progress:") && e.Data.Contains("%"))
            {
                // Parse "Progress: nn%"
                Int32 index = e.Data.IndexOf(' ') + 1;
                Int32.TryParse(e.Data.Substring(index, e.Data.IndexOf('%') - index), out _progressPercent);

                this.OnProgress();
            }
        }

        /// <summary>
        /// Starts the worker.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The event arguments.</param>
        private void worker_DoWork(object sender, DoWorkEventArgs e)
        {
            // Remove output file if it already exists.
            if (System.IO.File.Exists(_outputFilename))
            {
                System.IO.File.Delete(_outputFilename);
            }

            // Start the process.
            _process = new System.Diagnostics.Process();
            _process.StartInfo.CreateNoWindow = true;
            _process.StartInfo.FileName = _appFilename;
            _process.StartInfo.Arguments = _arguments.ToString();
            _process.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
            _process.StartInfo.UseShellExecute = false;
            _process.StartInfo.RedirectStandardOutput = true;
            _process.OutputDataReceived += new System.Diagnostics.DataReceivedEventHandler(process_OutputDataReceived);

            _process.Start();
            _process.BeginOutputReadLine();

            _status = JobStatus.Active;
            OnStarted();

            _process.WaitForExit();

            if (_status == JobStatus.Active)
            {
                // Output file not found? Error occurred?
                if (!System.IO.File.Exists(_outputFilename) || _process.ExitCode > 1)
                {
                    _status = JobStatus.Failed;
                }
                else
                {
                    _status = JobStatus.Completed;
                }
            }
            
            OnCompleted();
        }

        #endregion
    }
}
