using System;
using System.ComponentModel;
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 DGIndex in a seperate thread.
    /// </summary>
    sealed public class DgindexBackgroundWorker
    {
        #region Fields

        private string _appFilename = string.Empty;
        private string _arguments = string.Empty;
        private Int32 _bFrameCount = 0;
        private Boolean _canceled = false;
        private FicSprout.Bacicworx.Media.VideoAspectRatio _displayAspectRatio = FicSprout.Bacicworx.Media.VideoAspectRatio.Aspect_1_78;
        private FicSprout.Bacicworx.Media.VideoFieldOrder _fieldOrder = FicSprout.Bacicworx.Media.VideoFieldOrder.TopFieldFirst;
        private Int64 _fileLength = 0;
        private Int32 _frameCount = 0;
        private FicSprout.Bacicworx.Media.VideoFramerate _framerate = FicSprout.Bacicworx.Media.VideoFramerate.Fps30;
        private FicSprout.Bacicworx.Media.VideoFrameType _frameType = FicSprout.Bacicworx.Media.VideoFrameType.Interlaced;
        private Boolean _hasExited = false;
        private Int32 _iFrameCount = 0;
        private Boolean _isPreview = false;
        private DateTime _lastRaisedDateTime = DateTime.Now;
        private Int32 _pFrameCount = 0;
        private System.Diagnostics.Process _process = null;
        private Int32 _progressPercent = 0;
        private string _sourceFilename = string.Empty;
        private Int32 _sourceHeight = 0;
        private Int32 _sourceWidth = 0;
        private FicSprout.Bacicworx.IO.JobStatus _status = FicSprout.Bacicworx.IO.JobStatus.Inactive;
        private FicSprout.Bacicworx.Media.VideoType _videoType = FicSprout.Bacicworx.Media.VideoType.NtscVideo;
        private double _videoTypePercentage = 0;
        private BackgroundWorker _worker = null;

        #endregion

        #region Properties

        /// <summary>
        /// Gets the CLI arguments.
        /// </summary>
        public string Arguments
        {
            get
            {
                return _arguments;
            }
        }
        /// <summary>
        /// Gets the number of B-frames for the video.
        /// </summary>
        public Int32 BFrameCount
        {
            get
            {
                return _bFrameCount;
            }
        }
        /// <summary>
        /// Gets the display aspect ratio for the source video as specified in the
        /// transport stream. This value is independent of the JobInfo.AspectRatio.
        /// </summary>
        /// <remarks>
        /// DisplayAspectRatio is the value specified by the video stream. JobInfo.AspectRatio is the 
        /// value for the video when letterboxing is removed. For example: a 2.35 widescreen video stored
        /// in a fullscreeen video stream would have a DisplayAspectRatio of 1.33 and an AspectRatio of 2.35
        /// after cropping of black borders.
        /// </remarks>
        public FicSprout.Bacicworx.Media.VideoAspectRatio DisplayAspectRatio
        {
            get
            {
                return _displayAspectRatio;
            }
        }
        /// <summary>
        /// Gets the video field order.
        /// </summary>
        public FicSprout.Bacicworx.Media.VideoFieldOrder FieldOrder
        {
            get
            {
                return _fieldOrder;
            }
        }
        /// <summary>
        /// Gets the video file length in bytes.
        /// </summary>
        public Int64 FileLength
        {
            get
            {
                return _fileLength;
            }
        }
        /// <summary>
        /// Gets the total number of frames for the video.
        /// </summary>
        public Int32 FrameCount
        {
            get
            {
                return _frameCount;
            }
        }
        /// <summary>
        /// Gets the video frame rate.
        /// </summary>
        public FicSprout.Bacicworx.Media.VideoFramerate Framerate
        {
            get
            {
                return _framerate;
            }
        }
        /// <summary>
        /// Gets the video frame type.
        /// </summary>
        public FicSprout.Bacicworx.Media.VideoFrameType FrameType
        {
            get
            {
                return _frameType;
            }
        }
        /// <summary>
        /// Gets whether the worker has exited. To determine whether the worker completed or failed
        /// the operation check the Status property.
        /// </summary>
        public Boolean HasExited
        {
            get
            {
                return _hasExited;
            }
        }
        /// <summary>
        /// Gets the number of I-frames for the video.
        /// </summary>
        public Int32 IFrameCount
        {
            get
            {
                return _iFrameCount;
            }
        }
        /// <summary>
        /// Gets whether the worker will perform a quick preview index instead of a
        /// full indexing of the source video.
        /// </summary>
        public Boolean IsPreview
        {
            get
            {
                return _isPreview;
            }
        }
        /// <summary>
        /// Gets the number of P-frames for the video.
        /// </summary>
        public Int32 PFrameCount
        {
            get
            {
                return _pFrameCount;
            }
        }
        /// <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 source video height.
        /// </summary>
        public Int32 SourceHeight
        {
            get
            {
                return _sourceHeight;
            }
        }
        /// <summary>
        /// Gets the source video width.
        /// </summary>
        public Int32 SourceWidth
        {
            get
            {
                return _sourceWidth;
            }
        }
        /// <summary>
        /// Gets the status of the worker.
        /// </summary>
        public FicSprout.Bacicworx.IO.JobStatus Status
        {
            get
            {
                return _status;
            }
        }
        /// <summary>
        /// Gets the video type.
        /// </summary>
        public FicSprout.Bacicworx.Media.VideoType VideoType
        {
            get
            {
                return _videoType;
            }
        }
        /// <summary>
        /// Gets the VideoType percentage.
        /// </summary>
        public double VideoTypePercentage
        {
            get
            {
                return _videoTypePercentage;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Creates a new instance of the DgindexBackgroundWorker class to index the specified source video.
        /// </summary>
        /// <param name="appFilename">The application path including the filename.</param>
        /// <param name="source">The source video filename.</param>
        /// <param name="preview">Whether to perform a quick indexing instead of a full indexing
        /// of the video source.</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>
        public DgindexBackgroundWorker(string appFilename, string source, bool preview)
        {
            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.");
            }

            _appFilename = appFilename;
            _sourceFilename = source;
            _isPreview = preview;
            _fileLength = new System.IO.FileInfo(_sourceFilename).Length;

            if (preview)
            {
                _arguments = "-i \"" + _sourceFilename + "\" -o \"" + _sourceFilename.Substring(0,
                    _sourceFilename.LastIndexOf('.')) + "\" -ia 3 -fo 0 -om 0 -at -hide -preview -minimize -exit -exit";
            }
            else
            {
                _arguments = "-i \"" + _sourceFilename + "\" -o \"" + _sourceFilename.Substring(0,
                    _sourceFilename.LastIndexOf('.')) + "\" -ia 3 -fo 0 -om 0 -at -hide -minimize -exit -exit";
            }
        }

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

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

                _canceled = true;
            }
        }

        /// <summary>
        /// Returns whether the log and d2v files exist and are finished.
        /// </summary>
        /// <returns>True if the files exists and are finished, otherwsie false.</returns>
        private Boolean IsFinished()
        {
            // Files exist?
            if (!System.IO.File.Exists(System.IO.Path.ChangeExtension(_sourceFilename, "log")))
            {
                return false;
            }

            if (!_isPreview && !System.IO.File.Exists(System.IO.Path.ChangeExtension(_sourceFilename, "d2v")))
            {
                return false;
            }

            Boolean d2vFinished = false;
            Boolean logFinished = false;
            Boolean result = false;
            
            try
            {
                // Load the log file.
                string[] lines = System.IO.File.ReadAllLines(System.IO.Path.ChangeExtension(_sourceFilename, "log"));

                // Parse each line.
                for (int i = 0; i < lines.Length; i++)
                {
                    // Finished?
                    if (lines[i] == "Remain: FINISH")
                    {
                        logFinished = true;
                        break;
                    }
                }
            }
            catch (System.IO.IOException)
            {
                // Dummy catch to handle file lock errors.
                logFinished = false;
            }

            try
            {
                // Load the d2v file.
                string[] lines = System.IO.File.ReadAllLines(System.IO.Path.ChangeExtension(_sourceFilename, "d2v"));

                // Parse each line.
                for (int i = 0; i < lines.Length; i++)
                {
                    // Finished?
                    if (lines[i].StartsWith("FINISHED"))
                    {
                        d2vFinished = true;
                        break;
                    }
                }
            }
            catch (System.IO.IOException)
            {
                // Dummy catch to handle file lock errors.
                d2vFinished = false;
            }

            if (_isPreview && logFinished)
            {
                result = true;
            }

            if (!_isPreview && logFinished && d2vFinished)
            {
                result = true;
            }

            return result;
        }

        /// <summary>
        /// Parses the d2v file to get frame counts.
        /// </summary>
        private void ParseD2v()
        {
            if (!System.IO.File.Exists(System.IO.Path.ChangeExtension(_sourceFilename, "d2v")))
            {
                return;
            }

            // Load the d2v.
            string[] lines = System.IO.File.ReadAllLines(System.IO.Path.ChangeExtension(_sourceFilename, "d2v"));

            // Parse each line.
            bool isDataSection = false;
            Int32 sectionsCount = 0;

            for (int i = 0; i < lines.Length; i++)
            {
                // The second blank line means the next lines are part of the Data section.
                if (lines[i].Trim() == string.Empty)
                {
                    sectionsCount++;

                    if (sectionsCount == 2)
                    {
                        isDataSection = true;
                    }
                }

                // Data section?
                if (isDataSection && lines[i].Trim() != string.Empty)
                {
                    string[] values = lines[i].Split(new char[] { ' ' }, StringSplitOptions.None);

                    for (int j = 7; j < values.Length; j++)
                    {
                        Int32 value = Convert.ToInt32(values[j], 16);
                        Int32 frameType = (value >> 4) & 0x3;

                        // End of frames.
                        if (value == 0xff)
                        {
                            break;
                        }

                        if (frameType == 1)
                        {
                            _iFrameCount++;
                        }
                        if (frameType == 2)
                        {
                            _pFrameCount++;
                        }
                        if (frameType == 3)
                        {
                            _bFrameCount++;
                        }
                    }
                }
            }

            _frameCount = _iFrameCount + _pFrameCount + _bFrameCount;
        }

        /// <summary>
        /// Parses the DGIndex log file.
        /// </summary>
        private void ParseLog()
        {
            if (!System.IO.File.Exists(System.IO.Path.ChangeExtension(_sourceFilename, "log")))
            {
                return;
            }

            // Load the log.
            string[] lines = System.IO.File.ReadAllLines(System.IO.Path.ChangeExtension(_sourceFilename, "log"));

            // Parse each line.
            for (int i = 0; i < lines.Length; i++)
            {
                // Aspect Ratio
                if (lines[i].StartsWith("Aspect Ratio:"))
                {
                    if (lines[i].Contains("[1]"))
                    {
                        // 1:1
                        _displayAspectRatio = FicSprout.Bacicworx.Media.VideoAspectRatio.Aspect_1_00;
                    }
                    if (lines[i].Contains("[2]"))
                    {
                        // 4:3
                        _displayAspectRatio = FicSprout.Bacicworx.Media.VideoAspectRatio.Aspect_1_33;
                    }
                    if (lines[i].Contains("[3]"))
                    {
                        // 16:9
                        _displayAspectRatio = FicSprout.Bacicworx.Media.VideoAspectRatio.Aspect_1_78;
                    }
                    if (lines[i].Contains("[4]"))
                    {
                        // 2.21:1
                        _displayAspectRatio = FicSprout.Bacicworx.Media.VideoAspectRatio.Aspect_2_21;
                    }
                }

                // Field Order
                if (lines[i].StartsWith("Field Order:"))
                {
                    if (lines[i].Contains("Bottom"))
                    {
                        _fieldOrder = FicSprout.Bacicworx.Media.VideoFieldOrder.BottomFieldFirst;
                    }
                    if (lines[i].Contains("Top"))
                    {
                        _fieldOrder = FicSprout.Bacicworx.Media.VideoFieldOrder.TopFieldFirst;
                    }
                }

                // Frame Count
                if (lines[i].StartsWith("Coded Number:"))
                {
                    string[] values = lines[i].Split(new string[] { ": " }, StringSplitOptions.RemoveEmptyEntries);

                    if (values.Length == 2)
                    {
                        Int32.TryParse(values[1].Trim(), out _frameCount);
                    }
                }

                // Frame Rate
                if (lines[i].StartsWith("Frame Rate:"))
                {
                    if (lines[i].Contains("23.976"))
                    {
                        _framerate = FicSprout.Bacicworx.Media.VideoFramerate.Fps24;
                    }
                    if (lines[i].Contains("25"))
                    {
                        _framerate = FicSprout.Bacicworx.Media.VideoFramerate.Fps25;
                    }
                    if (lines[i].Contains("29.970"))
                    {
                        _framerate = FicSprout.Bacicworx.Media.VideoFramerate.Fps30;
                    }
                }

                // Frame Type
                if (lines[i].StartsWith("Frame Type:"))
                {
                    if (lines[i].Contains("Interlaced"))
                    {
                        _frameType = FicSprout.Bacicworx.Media.VideoFrameType.Interlaced;
                    }
                    if (lines[i].Contains("Progressive"))
                    {
                        _frameType = FicSprout.Bacicworx.Media.VideoFrameType.Progressive;
                    }
                }

                // Resolution
                if (lines[i].StartsWith("Frame Size:"))
                {
                    string[] values = lines[i].Split(new string[] { ": ", "x" }, StringSplitOptions.RemoveEmptyEntries);

                    if (values.Length == 3)
                    {
                        Int32.TryParse(values[1].Trim(), out _sourceWidth);
                        Int32.TryParse(values[2], out _sourceHeight);
                    }
                }

                // Video Type
                if (lines[i].StartsWith("Video Type:"))
                {
                    _videoTypePercentage = 100;
                    string[] values = lines[i].Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

                    // Percentage found? This is hybrid video.
                    if (values.Length == 4)
                    {
                        // Determine video type for hybrid video/film based on percentage.
                        // Pure video has no pulldown, film and hybrids do. DGindex value
                        // ranges are:
                        //
                        // Video|-----HybridVideo-----||-----HybridFilm-----|-Film
                        //  100%      99.99-50.01%    50     50.01-94.99%     95-100%
                        //
                        Double.TryParse(values[3].Substring(0, values[3].IndexOf('%') - 1), out _videoTypePercentage);
                    }

                    if (values.Length == 3)
                    {
                        // Video types.
                        if (values[2] == "PAL")
                        {
                            _videoType = FicSprout.Bacicworx.Media.VideoType.PalVideo;
                        }
                        if (values[2] == "NTSC")
                        {
                            _videoType = FicSprout.Bacicworx.Media.VideoType.NtscVideo;
                        }
                        if (values[2] == "Film")
                        {
                            _videoType = FicSprout.Bacicworx.Media.VideoType.NtscFilm;
                            if (_videoTypePercentage < 95)
                            {
                                _videoType = FicSprout.Bacicworx.Media.VideoType.NtscHybridFilm;
                            }
                        }
                        if (values[2] == "Video")
                        {
                            _videoType = FicSprout.Bacicworx.Media.VideoType.NtscHybridVideo;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Starts the worker.
        /// </summary>
        public void Start()
        {
            _worker = new BackgroundWorker();

            if (System.IO.File.Exists(System.IO.Path.ChangeExtension(_sourceFilename, "d2v")) &&
                System.IO.File.Exists(System.IO.Path.ChangeExtension(_sourceFilename, "log")))
            {
                OnStarted();
                Thread.Sleep(100);

                ParseLog();
                ParseD2v();
                _status = FicSprout.Bacicworx.IO.JobStatus.Completed;

                _hasExited = true;
                OnCompleted();
            }
            else
            {
                _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.Canceled)
            {
                return;
            }

            // Raise event with progress percent.
            Int32.TryParse(e.Data, 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();

            // Start the process.
            _process = new System.Diagnostics.Process();
            _process.StartInfo.CreateNoWindow = true;
            _process.StartInfo.FileName = _appFilename;

            _process.StartInfo.Arguments = _arguments;
            _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();

            while (!_process.HasExited)
            {
                // Process canceled OR files written and process has not exited?
                // Condition 2 can occur if the video has caused Dgindex to hang.
                if (_canceled || (IsFinished() && !_process.HasExited))
                {
                    // The process may have exited by the time that IsFinished returns, check for that situation here.
                    if (!_process.HasExited)
                    {
                        _process.Kill();
                    }
                }

                Thread.Sleep(100);
            }

            // Default status.
            _status = FicSprout.Bacicworx.IO.JobStatus.Completed;

            // Process canceled?
            if (_canceled)
            {
                _status = FicSprout.Bacicworx.IO.JobStatus.Canceled;
            }

            // Process exit code is ignored. It occasionally outputs -1 even when the work
            // files are generated successfully and the source video is playable.

            // Files not found?
            if (!System.IO.File.Exists(System.IO.Path.ChangeExtension(_sourceFilename, "log")) ||
                (!_isPreview && !System.IO.File.Exists(System.IO.Path.ChangeExtension(_sourceFilename, "d2v"))))
            {
                _status = FicSprout.Bacicworx.IO.JobStatus.Failed;
            }

            // Parse files.
            if (_status != FicSprout.Bacicworx.IO.JobStatus.Canceled &&
                _status != FicSprout.Bacicworx.IO.JobStatus.Failed)
            {
                ParseLog();
                ParseD2v();
            }

            _hasExited = true;
            OnCompleted();
        }

        #endregion
    }
}
