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

/*
    Copyright (c) 2010 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
{
    /// <summary>
    /// Utilizes a BackgroundWorker to run MP4Box in a seperate thread.
    /// </summary>
    sealed public class Mp4boxBackgroundWorker
    {
        #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 FicSprout.Bacicworx.Media.VideoFramerate _sourceFramerate = FicSprout.Bacicworx.Media.VideoFramerate.Fps25;
        private Int32 _sourceHeight = 0;
        private Int32 _sourceWidth = 0;
        private FicSprout.Bacicworx.IO.JobStatus _status = FicSprout.Bacicworx.IO.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 FicSprout.Bacicworx.IO.JobStatus Status
        {
            get
            {
                return _status;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Creates a new instance of the Mp4boxBackgroundWorker 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">An array of audo track filenames.</param>
        /// <param name="audioDelays">An array of audio track delays.</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 Mp4boxBackgroundWorker(string appFilename, string source, string output, Int32 width, Int32 height,
            FicSprout.Bacicworx.Media.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;

            // The only way to add negative audio delays is by adding a positive delay to the video.
            // [1] Scan all audio tracks to get the largest negative delay.
            // [2] Flip the sign and use as the positive delay for the video.
            // [3] Adjust the delay of all audio tracks based on the video delay.
            
            // [1] Scan for negative delays.
            Int32 largestDelay = 0;
            bool hasNegativeDelay = false;
            for (Int32 i = 0; i < audioDelays.Length; i++)
            {
                if (audioDelays[i] < largestDelay)
                {
                    largestDelay = audioDelays[i];
                }
            }
            // Negative delays? Do [2], [3].
            if (largestDelay < 0)
            {
                largestDelay = System.Math.Abs(largestDelay);
                hasNegativeDelay = true;

                // Update all delays based on the positive video delay.
                for (Int32 i = 0; i < audioDelays.Length; i++)
                {
                    audioDelays[i] = audioDelays[i] + largestDelay;
                }
            }
            
            // Build arguments. Track numbers start at one.
            // -add "{Exising MP4}#1:fps=29.970" -add "{Audio}" -delay [track#]=[delay ms]"{Output MP4}"

            _arguments.Append("-add \"" + _sourceFilename + "#1:fps=");
            _arguments.Append(FicSprout.Bacicworx.Media.Utility.ConvertVideoFramerateToValue(_sourceFramerate, true).ToString("0.000") + "\"");
            
            if (hasNegativeDelay)
            {
                _arguments.Append(" -delay 1=" + largestDelay.ToString());
            }

            // Add audio tracks with delays.
            if (audioFilenames.Length > 0)
            {
                for (Int32 i = 0; i < _audioFilenames.Length; i++)
                {
                    _arguments.Append(" -add \"" + _audioFilenames[i] + "\"");
                    
                    // Only add delay if greater than zero.
                    if (audioDelays[i] > 0)
                    {
                        _arguments.Append(" -delay " + (i + 2).ToString() + "=" + _audioDelays[i].ToString());
                    }
                }
            }

            // Add output filenamee.
            _arguments.Append(" \"" + _outputFilename + "\"");
        }

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

                _worker.CancelAsync();

                // Unhook events.
                _process.CancelOutputRead();
                _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 == FicSprout.Bacicworx.IO.JobStatus.Cancelled)
            {
                return;
            }

            if (e.Data.StartsWith("Importing M4V:"))
            {
                // Parse "Importing M4V: |==========| (nnn/100)"
                Int32 index = e.Data.IndexOf('(') + 1;
                Int32.TryParse(e.Data.Substring(index, e.Data.IndexOf('/') - index), out _progressPercent);

                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)
        {
            _status = FicSprout.Bacicworx.IO.JobStatus.Active;
            OnStarted();

            // Remove output files if they already exist.
            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();
            _process.WaitForExit();

            if (_status == FicSprout.Bacicworx.IO.JobStatus.Active)
            {
                // Output file not found?
                if (!System.IO.File.Exists(_outputFilename))
                {
                    _status = FicSprout.Bacicworx.IO.JobStatus.Failed;
                }
                else
                {
                    _status = FicSprout.Bacicworx.IO.JobStatus.Completed;
                }
            }

            OnCompleted();
        }

        #endregion
    }
}
