using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using Microsoft.Win32;

/*
    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>
    /// Utility methods.
    /// </summary>
    public static class Utility
    {
        #region Fields

        /// <summary>
        /// Audio format extensions. The index value of the extensions correspond to an
        /// AudioFormat enumeration.
        /// </summary>
        public static string[] AudioFormatExtensions = new string[7] 
        {
            string.Empty, ".mp4", ".ac3", ".dts", ".flac", ".ogg", ".wav"
        };

        /// <summary>
        /// Audio format names. The index value of the names correspond to an
        /// AudioFormat enumeration.
        /// </summary>
        public static string[] AudioFormatNames = new string[7] 
        {
            "Unknown", "AAC", "AC3", "DTS", "FLAC", "Vorbis", "WAV"
        };

        /// <summary>
        /// Crop mode names. The index value of the names correspond to a
        /// CropMode enumeration.
        /// </summary>
        public static string[] CropModeNames = new string[4] 
        {
            "Auto", "Aspect", "Manual", "None"
        };
        
        /// <summary>
        /// Encoder quality option names. The index value of the names correspond to an
        /// EncoderQualityOption enumeration.
        /// </summary>
        public static string[] EncoderQualityOptionNames = new string[5] 
        { 
           "Archival", "High", "Medium", "Low", "Fast"
        };
        
        /// <summary>
        /// Output container format extensions. The index value of the extensions correspond to an
        /// OutputContainerFormat enumeration.
        /// </summary>
        public static string[] OutputContainerFormatExtensions = new string[3] 
        {
            string.Empty, ".mkv", ".mp4"
        };

        /// <summary>
        /// Output container format names. The index value of the names correspond to an
        /// OutputContainerFormat enumeration.
        /// </summary>
        public static string[] OutputContainerFormatNames = new string[3] 
        {
            "None", "Matroska", "MPEG-4"
        };

        /// <summary>
        /// Dialog filters for supported audio types.
        /// </summary>
        public static string SupportedAudioDialogFilters =
           "AAC Audio Files (*.mp4)|*.mp4|" +
           "AC3 Audio Files (*.ac3)|*.ac3|" +
           "DTS Audio Files (*.dts)|*.dts|" +
           "FLAC Audio Files (*.flac)|*.flac|" +
           "Vorbis Audio Files (*.ogg)|*.ogg|" +
           "WAV Audio Files (*.wav)|*.wav|" +
           "Supported Audio Files (*.aac,*.ac3,*.dts,*.flac,*.mp4,*.ogg,*.wav)|*.aac;*.ac3;*.dts;*.flac;*.mp4;*.ogg;*.wav";

        /// <summary>
        /// Dialog filters for supported video types.
        /// </summary>
        public static string SupporteVideoDialogFilters =
            "MPEG-2 Video Files (*.m2v,*.mpeg,*.mpg)|*.m2v;*.mpeg;*.mpg";


        /// <summary>
        /// Video aspect ratio names.
        /// </summary>
        public static string[] VideoAspectRatioNames = new string[24]
        {
            "Unknown", "1.00", "1.33", "1.37", "1.43", "1.50", "1.55", "1.56", "1.66", "1.75", "1.78", "1.85",
            "2.00", "2.11", "2.20", "2.21", "2.35", "2.39", "2.40", "2.55", "2.59", "2.66", "2.76",
            "4.00"
        };

        /// <summary>
        /// Video aspect ratio values.
        /// </summary>
        public static Double[] VideoAspectRatioValues = new Double[24]
        {
            0, 1.00, 1.33, 1.37, 1.43, 1.50, 1.55, 1.56, 1.66, 1.75, 1.78, 1.85,
            2.00, 2.11, 2.20, 2.21, 2.35, 2.39, 2.40, 2.55, 2.59, 2.66, 2.76,
            4.00
        };

        /// <summary>
        /// Video processing type names. The index value of the names correspond to a
        /// VideoProcessingType enumeration.
        /// </summary>
        public static string[] VideoProcessingTypeNames = new string[11] 
        { 
           "None", "Deinterlace", "Hybrid Film", "Hybrid Film (Anime)", "Hybrid Video",
           "Hybrid Video (Anime)", "IVTC", "IVTC (Anime)", "PAL Video", "PAL To Film", "PAL To NTSC"
        };

        #endregion

        #region Methods

        /// <summary>
        /// Adds the specified audio track filenames to the job. Any existing tracks are not removed.
        /// </summary>
        /// <param name="job">The job to add tracks to.</param>
        /// <param name="config">The configuration data to use.</param>
        /// <param name="files">The audio track filenames to add.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when job is null.</exception>
        /// <exception cref="System.ArgumentNullException">Thrown when config is null.</exception>
        /// <exception cref="System.ArgumentNullException">Thrown when files is null.</exception>
        /// <remarks>
        /// Any files with zero length are not added to the job.
        /// </remarks>
        public static void AddAudioTracks(JobInfo job, ConfigurationInfo config, string[] files)
        {
            if (job == null)
            {
                throw new ArgumentNullException("job");
            }
            if (config == null)
            {
                throw new ArgumentNullException("config");
            }
            if (files == null)
            {
                throw new ArgumentNullException("files");
            }

            // Add files to job if not already in the collection.
            for (Int32 i = 0; i < files.Length; i++)
            {
                // Not already in list? File length non-zero?
                if (!job.ContainsAudioTrack(files[i]) && new System.IO.FileInfo(files[i]).Length > 0)
                {
                    // Examples:
                    // VTS_02_PGC_02_1 - 0x80 - Audio - AC3 - 2ch - 48kHz - DRC - English - DELAY 0ms.AC3
                    // VTS_01_PGC_01_1 - 0x82 - Audio - AC3 - 2ch - 48kHz - DRC - English - Alternate Director's Comments - DELAY 0ms.AC3
                    // VTS_02_PGC_01_1 T80 2_0ch 192Kbps DELAY 0ms.ac3

                    AudioTrackInfo track = new AudioTrackInfo();
                    track.Downmix = config.DownmixAudio;
                    track.DynamicRangeCompression = config.AudioDynamicRangeCompression;
                    track.Filename = files[i];
                    track.Normalize = config.NormalizeAudio;
                    track.OutputFormat = config.AudioFormat;
                    string filename = files[i].ToUpperInvariant();

                    // Parse delay.
                    Int32 indexDelay = filename.IndexOf("DELAY");
                    if (indexDelay != -1)
                    {
                        Int32 indexDelayMs = filename.IndexOf("MS", indexDelay);
                        if (indexDelayMs != -1)
                        {
                            track.Delay = Convert.ToInt32(filename.Substring(indexDelay + 6, indexDelayMs - indexDelay - 6));
                        }
                    }

                    // Only added if filename is not empty.
                    if (track.Filename.Length > 0)
                    {
                        job.AudioTracks.Add(track);
                    }
                }
            }
        }

        /// <summary>
        /// Computes the cropped video width and height based on the source video width, height,
        /// and aspect ratios.
        /// </summary>
        /// <param name="width">Sets the current source video width. Returns the cropped width of
        /// the video in pixels.</param>
        /// <param name="height">Sets the current source video height. Returns the cropped height of
        /// the video in pixels.</param>
        /// <param name="aspectRatio">The aspect ratio of the source video.</param>
        /// <param name="cropAspectRatio">The aspect ratio which will used to crop the letterboxing
        /// of the source video.</param>
        /// <param name="modulus">The minimum pixel increment required by the video encoder, usually
        /// 4 but 16 is recommended.</param>
        /// <exception cref="System.ArgumentException">Thrown when width is less than 120.</exception>
        /// <exception cref="System.ArgumentException">Thrown when height is less than 120.</exception>
        /// <remarks>
        /// <para>
        /// This method always increases the resolution to match the aspect ratio, preserving pixel data. The
        /// resulting height and/or width will always be greater than the input values. 
        /// </para>
        /// <para>
        /// A modulus of 16 is recommended. Although current encoders can use mod 4 height and width values,
        /// artifacts may be introduced along the edges of the encoded video due to the way that internally
        /// generated padding is processed by the encoders. The safest strategy is to always use mod 16 height
        /// and width values.
        /// </para>
        /// </remarks>
        public static void ComputeCroppedVideoResolution(ref Int32 width, ref Int32 height, VideoAspectRatio aspectRatio,
            VideoAspectRatio cropAspectRatio, Int32 modulus)
        {
            if (width < 120)
            {
                throw new ArgumentException("width is less than 120.");
            }
            if (height < 120)
            {
                throw new ArgumentException("height is less than 120.");
            }

            double aspectRatioValue = ConvertVideoAspectRatioToValue(aspectRatio);
            double cropAspectRatioValue = ConvertVideoAspectRatioToValue(cropAspectRatio);

            // Aspect is square (1:1) pixels?
            if (cropAspectRatioValue == 1 || cropAspectRatioValue == aspectRatioValue)
            {
                return;
            }

            if (cropAspectRatioValue < aspectRatioValue)
            {
                width = System.Convert.ToInt32(System.Math.Ceiling((height * cropAspectRatioValue) / modulus) * modulus);
            }
            if (cropAspectRatioValue > aspectRatioValue)
            {
                height = Convert.ToInt32(System.Math.Ceiling((width / cropAspectRatioValue) / modulus) * modulus);
            }
        }

        /// <summary>
        /// Returns the croppped video resolution in the paramaeters 'height' and 'width'.
        /// </summary>
        /// <param name="job">The job to calculate the resolution for.</param>
        /// <param name="height">Returns the cropped height in pixels.</param>
        /// <param name="width">Returns the cropped width in pixels.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when job is null.</exception>
        public static void ComputeJobCroppedVideoResolution(JobInfo job, ref Int32 height, ref Int32 width)
        {
            if (job == null)
            {
                throw new ArgumentNullException("job");
            }

            height = job.ResizeHeight;
            width = job.ResizeWidth;

            if (height > 0 && width > 0)
            {
                Utility.ComputeCroppedVideoResolution(ref width, ref height, job.DisplayAspectRatio, job.CropAspectRatio, 16);
            }
        }

        /// <summary>
        /// Determines the best processing to be performed on the video.
        /// </summary>
        /// <param name="job">The JobInfo instance to update.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when job is null.</exception>
        public static void ComputeVideoProcessing(JobInfo job)
        {
            if (job == null)
            {
                throw new ArgumentNullException("job");
            }

            //
            // Determine video processing type.
            //
            job.Processing = VideoProcessingType.None;

            if (job.VideoType == VideoType.NtscVideo)
            {
                job.Processing = VideoProcessingType.NtscDeinterlace;

                if (job.FrameType == VideoFrameType.Progressive)
                {
                    // Video less than one hour is usually 30fps interlaced.
                    if (job.Duration.TotalMinutes <= 60)
                    {
                        job.Processing = VideoProcessingType.NtscHybridVideo;
                    }
                }

                // Assume that any video with a duration or 60 minutes or more and
                // with a file length greater than 3 GB is 24fps telecined film.
                if (job.Duration.TotalMinutes > 60 || job.FileLength > 0xc0000000)
                {
                    job.Processing = VideoProcessingType.NtscIvtc;
                }
            }

            if (job.VideoType == VideoType.NtscHybridFilm)
            {
                job.Processing =VideoProcessingType.NtscHybridFilm;

                if (job.IsAnimation)
                {
                    job.Processing = VideoProcessingType.NtscHybridFilmAnime;
                }
            }

            if (job.VideoType == VideoType.NtscHybridVideo)
            {
                job.Processing = VideoProcessingType.NtscHybridVideo;

                if (job.IsAnimation)
                {
                    job.Processing = VideoProcessingType.NtscHybridVideoAnime;
                }
            }

            // Film has to be IVTC'd regardless of percentage.
            if (job.VideoType == VideoType.NtscFilm)
            {
                job.Processing = VideoProcessingType.NtscIvtc;

                if (job.IsAnimation)
                {
                    job.Processing = VideoProcessingType.NtscIvtcAnime;
                }
            }

            if (job.VideoType == VideoType.PalVideo)
            {
                job.Processing = VideoProcessingType.Pal;

                if (job.FrameType == VideoFrameType.Progressive)
                {
                    job.Processing = VideoProcessingType.PalFilm;
                }
            }

            //
            // Set framerate based on the video processing type.
            //
            VideoProcessingType processing = job.Processing;
            if (job.ProcessingOverrideEnabled)
            {
                processing = job.ProcessingOverride;
            }

            job.OutputFramerate = job.Framerate;
            if (processing == VideoProcessingType.None)
            {
                job.OutputFramerate = job.Framerate;
            }

            if (processing == VideoProcessingType.NtscHybridFilm ||
                processing == VideoProcessingType.NtscHybridFilmAnime ||
                processing == VideoProcessingType.NtscIvtc ||
                processing == VideoProcessingType.NtscIvtcAnime ||
                processing == VideoProcessingType.PalFilm
                )
            {
                job.OutputFramerate = VideoFramerate.Fps24;
            }

            if (processing == VideoProcessingType.Pal)
            {
                job.OutputFramerate = VideoFramerate.Fps25;
            }

            if (processing == VideoProcessingType.NtscDeinterlace ||
                processing == VideoProcessingType.NtscHybridVideo ||
                processing == VideoProcessingType.NtscHybridVideoAnime)
            {
                job.OutputFramerate = VideoFramerate.Fps30;
            }

            //
            // Update output frame count.
            //
            job.OutputFrameCount = job.FrameCount;
            if (job.Framerate != job.OutputFramerate)
            {
                job.OutputFrameCount = Convert.ToInt32(System.Math.Ceiling(job.FrameCount * (
                   Utility.ConvertVideoFramerateToValue(job.OutputFramerate, true) /
                   Utility.ConvertVideoFramerateToValue(job.Framerate, true))));
            }

            // Update output trim timecodes.
            job.TrimOutputStartTimecode = new Dvd.SmpteTimecode(job.TrimStartTimecode);
            job.TrimOutputStartTimecode.ConvertFramerate(job.OutputFramerate);

            job.TrimOutputEndTimecode = new Dvd.SmpteTimecode(job.TrimEndTimecode);
            job.TrimOutputEndTimecode.ConvertFramerate(job.OutputFramerate);
        }

        /// <summary>
        /// Calculates required video resize values.
        /// </summary>
        /// <param name="job">A JobInfo instance containg information about a job.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when job is null.</exception>
        public static void ComputeVideoResizeValues(JobInfo job)
        {
            if (job == null)
            {
                throw new ArgumentNullException("job");
            }

            if (job.VideoType != VideoType.NtscFilm ||
                job.VideoType != VideoType.NtscHybridFilm ||
                job.VideoType != VideoType.NtscHybridVideo ||
                job.VideoType != VideoType.NtscVideo)
            {
                if (job.DisplayAspectRatio == VideoAspectRatio.Aspect_1_33)
                {
                    // Fullscreen NTSC
                    job.ResizeHeight = 544;
                    job.ResizeWidth = 720;
                }
                if (job.DisplayAspectRatio == VideoAspectRatio.Aspect_1_78)
                {
                    // Widescreen NTSC
                    job.ResizeHeight = 480;
                    job.ResizeWidth = 864;
                }
            }
            if (job.VideoType == VideoType.PalVideo)
            {
                if (job.DisplayAspectRatio == VideoAspectRatio.Aspect_1_33)
                {
                    // Fullscreen PAL
                    job.ResizeHeight = 576;
                    job.ResizeWidth = 768;
                }
                if (job.DisplayAspectRatio == VideoAspectRatio.Aspect_1_78)
                {
                    // Widescreen PAL
                    job.ResizeHeight = 576;
                    job.ResizeWidth = 1024;
                }
            }
        }

        /// <summary>
        /// Converts a filename with extension to an AudioFormat enumeration.
        /// </summary>
        /// <param name="value">The filename to parse.</param>
        /// <returns>An AudioFormat enumeration.</returns>
        /// <exception cref="System.ArgumentException">Thrown when value is null or empty.</exception>
        public static AudioFormat ConvertAudioFormatFromExtension(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value is null or empty.");
            }

            Int32 index = 0;
            value = System.IO.Path.GetExtension(value).ToUpperInvariant();

            for (Int32 i = 0; i < AudioFormatExtensions.Length; i++)
            {
                if (AudioFormatExtensions[i].ToUpperInvariant() == value)
                {
                    index = i;
                    break;
                }
            }

            return (AudioFormat)index;
        }

        /// <summary>
        /// Converts an AudioFormatName string to an AudioFormat enumeration.
        /// </summary>
        /// <param name="value">The AudioFormatName string to parse.</param>
        /// <returns>An AudioFormat enumeration.</returns>
        /// <exception cref="System.ArgumentException">Thrown when value is null or empty.</exception>
        public static AudioFormat ConvertAudioFormatFromName(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value is null or empty.");
            }

            Int32 index = 0;
            for (Int32 i = 0; i < AudioFormatNames.Length; i++)
            {
                if (AudioFormatNames[i] == value)
                {
                    index = i;
                    break;
                }
            }

            return (AudioFormat)index;
        }

        /// <summary>
        /// Converts an AudioFormat enumeration to an AudioFormatExtension string.
        /// </summary>
        /// <param name="value">The AudioFormat enumeration to convert.</param>
        /// <returns>An AudioFormatExtension string.</returns>
        public static string ConvertAudioFormatToExtension(AudioFormat value)
        {
            return AudioFormatExtensions[(Int32)value];
        }

        /// <summary>
        /// Converts an AudioFormat enumeration to an AudioFormatName string.
        /// </summary>
        /// <param name="value">The AudioFormat enumeration to convert.</param>
        /// <returns>An AudioFormatName string.</returns>
        public static string ConvertAudioFormatToName(AudioFormat value)
        {
            return AudioFormatNames[(Int32)value];
        }

        /// <summary>
        /// Converts an array of AudioFormat enumerations to an array of AudioFormatName strings.
        /// </summary>
        /// <param name="value">The AudioFormat enumerations to convert.</param>
        /// <returns>An array of AudioFormatName strings.</returns>
        public static string[] ConvertAudioFormatToNames(AudioFormat[] value)
        {
            string[] result = new string[value.Length];

            for (Int32 i = 0; i < value.Length; i++)
            {
                result[i] = AudioFormatNames[(Int32)value[i]];
            }

            return result;
        }

        /// <summary>
        /// Converts a Base16 hexadecimal string representation to a series of bytes.
        /// </summary>
        /// <param name="value">The string to convert.</param>
        /// <returns>A byte array if successful, otherwise null.</returns>
        /// <exception cref="System.ArgumentException">Thrown when value length is not a
        /// multiple of two.</exception>
        public static byte[] ConvertBinHexStringToBytes(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                return null;
            }

            if ((value.Length & 0x00000001) != 0)
            {
                throw new ArgumentException("value length must be a mulitple of two.");
            }

            byte[] bytes = new byte[value.Length / 2];

            for (Int32 i = 0; i < bytes.Length; i++)
            {
                bytes[i] = System.Convert.ToByte(value.Substring((i * 2), 2), 16);
            }

            return bytes;
        }

        /// <summary>
        /// Converts a series of bytes to a Base16 hexadecimal string representation.
        /// </summary>
        /// <param name="value">The byte array to convert.</param>
        /// <returns>A string.</returns>
        /// <remarks>
        /// If value length is zero, an empty string is returned.
        /// </remarks>
        /// <exception cref="System.ArgumentNullException">Thrown when value is null.</exception>
        public static string ConvertBytesToString(byte[] value)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            if (value.Length == 0)
            {
                return string.Empty;
            }

            StringBuilder sb = new StringBuilder();
            for (Int32 i = 0; i < value.Length; i++)
            {
                sb.Append(value[i].ToString("X2", CultureInfo.InvariantCulture));
            }

            return sb.ToString().ToUpperInvariant();
        }

        /// <summary>
        /// Converts a CropMode string to a CropMode enumeration.
        /// </summary>
        /// <param name="value">The CropModeName string to parse.</param>
        /// <returns>A CropMode enumeration.</returns>
        /// <exception cref="System.ArgumentException">Thrown when value is null or empty.</exception>
        public static CropMode ConvertCropModeFromName(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value is null or empty.");
            }

            Int32 index = 0;
            for (Int32 i = 0; i < CropModeNames.Length; i++)
            {
                if (CropModeNames[i] == value)
                {
                    index = i;
                    break;
                }
            }

            return (CropMode)index;
        }

        /// <summary>
        /// Converts a CropMode enumeration to an CropModetName string.
        /// </summary>
        /// <param name="value">The CropMode enumeration to convert.</param>
        /// <returns>A CropModeName string.</returns>
        public static string ConvertCropModeToName(CropMode value)
        {
            return CropModeNames[(Int32)value];
        }

        /// <summary>
        /// Converts an EncoderQualityOptionName string to an EncoderQualityOption enumeration.
        /// </summary>
        /// <param name="value">The EncoderQualityOptionName string to parse.</param>
        /// <returns>An EncoderQualityOption enumeration.</returns>
        /// <exception cref="System.ArgumentException">Thrown when value is null or empty.</exception>
        public static EncoderQualityOption ConvertEncoderQualityOptionFromName(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value is null or empty.");
            }

            Int32 index = 0;
            for (Int32 i = 0; i < EncoderQualityOptionNames.Length; i++)
            {
                if (EncoderQualityOptionNames[i] == value)
                {
                    index = i;
                    break;
                }
            }

            return (EncoderQualityOption)index;
        }

        /// <summary>
        /// Converts an EncoderQualityOption enumeration to an EncoderQualityOptionName string.
        /// </summary>
        /// <param name="value">The EncoderQualityOption enumeration to convert.</param>
        /// <returns>An EncoderQualityOptionName string.</returns>
        public static string ConvertEncoderQualityOptionToName(EncoderQualityOption value)
        {
            return EncoderQualityOptionNames[(Int32)value];
        }

        /// <summary>
        /// Converts an OutputContainerFormatName string to an OutputContainerFormat enumeration.
        /// </summary>
        /// <param name="value">The OutputContainerFormatName string to parse.</param>
        /// <returns>An OutputContainerFormat enumeration.</returns>
        /// <exception cref="System.ArgumentException">Thrown when value is null or empty.</exception>
        public static OutputContainerFormat ConvertOutputContainerFormatFromName(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value is null or empty.");
            }

            Int32 index = 0;
            for (Int32 i = 0; i < OutputContainerFormatNames.Length; i++)
            {
                if (OutputContainerFormatNames[i] == value)
                {
                    index = i;
                    break;
                }
            }

            return (OutputContainerFormat)index;
        }

        /// <summary>
        /// Converts an OutputContainerFormat enumeration to an OutputContainerFormatExtension string.
        /// </summary>
        /// <param name="value">The OutputContainerFormat enumeration to conver.</param>
        /// <returns>A string containing the container format extension without the period.
        /// If not found, an empty string is returned.</returns>
        public static string ConvertOutputContainerFormatToExtension(OutputContainerFormat value)
        {
            return OutputContainerFormatExtensions[(Int32)value];
        }

        /// <summary>
        /// Converts an OutputContainerFormat enumeration to an OutputContainerFormatName string.
        /// </summary>
        /// <param name="value">The OutputContainerFormat enumeration to convert.</param>
        /// <returns>An OutputContainerFormatName string.</returns>
        public static string ConvertOutputContainerFormatToName(OutputContainerFormat value)
        {
            return OutputContainerFormatNames[(Int32)value];
        }

        /// <summary>
        /// Converts an VideoAspectRatioName string to an VideoAspectRatio enumeration.
        /// </summary>
        /// <param name="value">The VideoAspectRatioName string to parse.</param>
        /// <returns>An VideoAspectRatio enumeration.</returns>
        /// <exception cref="System.ArgumentException">Thrown when value is null or empty.</exception>
        public static VideoAspectRatio ConvertVideoAspectRatioFromName(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value is null or empty.");
            }

            Int32 index = 0;
            for (Int32 i = 0; i < VideoAspectRatioValues.Length; i++)
            {
                if (VideoAspectRatioNames[i] == value)
                {
                    index = i;
                    break;
                }
            }

            return (VideoAspectRatio)index;
        }

        /// <summary>
        /// Converts an VideoAspectRatio enumeration to an VideoAspectRatioName string.
        /// </summary>
        /// <param name="value">The VideoAspectRatio enumeration to convert.</param>
        /// <returns>An VideoAspectRatioName string.</returns>
        public static string ConvertVideoAspectRatioToName(VideoAspectRatio value)
        {
            return VideoAspectRatioNames[(Int32)value];
        }

        /// <summary>
        /// Converts an VideoAspectRatio enumeration to a VideoAspectRatioValue double value.
        /// </summary>
        /// <param name="value">The VideoAspectRatio enumeration to convert.</param>
        /// <returns>A double representing the VideoAspectRatioValue.</returns>
        public static double ConvertVideoAspectRatioToValue(VideoAspectRatio value)
        {
            return VideoAspectRatioValues[(Int32)value];
        }

        /// <summary>
        /// Converts a double to a VideoFramerate enumeration.
        /// </summary>
        /// <param name="value">The number of frames per second.</param>
        /// <returns>AVideoFramerate enumeation.</returns>
        public static VideoFramerate ConvertVideoFramerateFromValue(double value)
        {
            VideoFramerate result = VideoFramerate.Unknown;

            if (value > 23.9 && value <= 24)
            {
                result = VideoFramerate.Fps24;
            }
            if (value == 25)
            {
                result = VideoFramerate.Fps25;
            }
            if (value > 29.9 && value <= 30)
            {
                result = VideoFramerate.Fps30;
            }
            if (value == 50)
            {
                result = VideoFramerate.Fps50;
            }
            if (value > 59.9 && value <= 60)
            {
                result = VideoFramerate.Fps60;
            }
            if (value > 119.8 && value <= 120)
            {
                result = VideoFramerate.Fps120;
            }

            return result;
        }

        /// <summary>
        /// Converts a VideoFramerate enumeration to a double.
        /// </summary>
        /// <param name="value">The VideoFramerate enumeration to convert.</param>
        /// <param name="dropFrames">Whether frames are droppped for the specified framerate.
        /// Only valid for Fps24, Fps30, and Fps60 framerates.</param>
        /// <returns>A double.</returns>
        public static double ConvertVideoFramerateToValue(VideoFramerate value, Boolean dropFrames)
        {
            // 'Unknown' value.
            double result = 0.0;

            if (value == VideoFramerate.Fps24)
            {
                if (dropFrames)
                {
                    result = 24000.0 / 1001;
                }
                else
                {
                    result = 24.0;
                }
            }
            if (value == VideoFramerate.Fps25)
            {
                result = 25.0;
            }
            if (value == VideoFramerate.Fps30)
            {
                if (dropFrames)
                {
                    result = 30000.0 / 1001;
                }
                else
                {
                    result = 30.0;
                }
            }
            if (value == VideoFramerate.Fps50)
            {
                result = 50.0;
            }
            if (value == VideoFramerate.Fps60)
            {
                if (dropFrames)
                {
                    result = 60000.0 / 1001;
                }
                else
                {
                    result = 60.0;
                }
            }
            if (value == VideoFramerate.Fps120)
            {
                if (dropFrames)
                {
                    result = 120000.0 / 1001;
                }
                else
                {
                    result = 120.0;
                }
            }

            return result;
        }

        /// <summary>
        /// Converts an VideoProcessingTypeName string to an VideoProcessingType enumeration.
        /// </summary>
        /// <param name="value">The VideoProcessingTypeName string to parse.</param>
        /// <returns>An VideoProcessingType enumeration.</returns>
        /// <exception cref="System.ArgumentException">Thrown when value is null or empty.</exception>
        public static VideoProcessingType ConvertVideoProcessingTypeFromName(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value is null or empty.");
            }

            Int32 index = 0;
            for (Int32 i = 0; i < VideoProcessingTypeNames.Length; i++)
            {
                if (VideoProcessingTypeNames[i] == value)
                {
                    index = i;
                    break;
                }
            }

            return (VideoProcessingType)index;
        }

        /// <summary>
        /// Converts an VideoProcessingType enumeration to an VideoProcessingTypeName string.
        /// </summary>
        /// <param name="value">The VideoProcessingType enumeration to convert.</param>
        /// <returns>An VideoProcessingTypeName string.</returns>
        public static string ConvertVideoProcessingTypeToName(VideoProcessingType value)
        {
            return VideoProcessingTypeNames[(Int32)value];
        }

        /// <summary>
        /// Formats the value as a string with either the B, KB, MB, or GB specifier. A
        /// space seperates the value from the specifier such as '123.456 MB'. No thousands
        /// seperator is added.
        /// </summary>
        /// <param name="value">The value to convert. Must be zero or greater.</param>
        /// <param name="showFraction">Whether to show the fractional portion of the value.</param>
        /// <returns>A string.</returns>
        public static string FormatLength(Int32 value, Boolean showFraction)
        {
            if (value < 0)
            {
                throw new ArgumentOutOfRangeException("value must be zero or greater.");
            }

            return FormatLength(Convert.ToInt64(value), showFraction);
        }

        /// <summary>
        /// Formats the value as a string with either the B, KB, MB, GB, TB, PB specifier. A
        /// space seperates the value from the specifier such as '123 MB'. No thousands
        /// seperator is added.
        /// </summary>
        /// <param name="value">The value to convert. Must be zero or greater.</param>
        /// <param name="showFraction">Whether to show the fractional portion of the value.</param>
        /// <returns>A string.</returns>
        public static string FormatLength(Int64 value, Boolean showFraction)
        {
            if (value < 0)
            {
                throw new ArgumentOutOfRangeException("value must be zero or greater.");
            }

            string result = string.Empty;
            string format = string.Empty;
            if (showFraction)
            {
                format = "F3";
            }
            else
            {
                format = "F0";
            }


            if (value < 0x400)
            {
                result = value.ToString() + " B";
            }
            if (value >= 0x400 && value < 0x100000)
            {
                result = (value / 1024.0).ToString(format) + " KB";
            }

            if (value >= 0x100000 && value < 0x40000000)
            {
                result = (value / 1048576.0).ToString(format) + " MB";
            }

            if (value >= 0x40000000 && value < 0x10000000000)
            {
                result = (value / 1073741824.0).ToString(format) + " GB";
            }

            if (value >= 0x10000000000 && value < 0x4000000000000)
            {
                result = (value / 1099511627776.0).ToString(format) + " TB";
            }

            if (value >= 0x4000000000000)
            {
                result = (value / 1125899906842624.0).ToString(format) + " PB";
            }

            return result;
        }

        /// <summary>
        /// Returns the supported output audio formats for the specified audio conversion parameters.
        /// </summary>
        /// <param name="source">The source audio format.</param>
        /// <param name="package">The output container format.</param>
        /// <returns>An array of AudioFormat enumerations if any, otherwise an empty array.</returns>
        public static AudioFormat[] GetSupportedAudioOutputFormats(AudioFormat source, OutputContainerFormat package)
        {
            AudioFormat[] result = new AudioFormat[0];

            // If package == OutputContainerFormat.None, an audio conversion is being performed instead of a job run.

            if (source == AudioFormat.Aac)
            {
                if (package == OutputContainerFormat.Matroska || package == OutputContainerFormat.Mpeg4)
                {
                    result = new AudioFormat[1] { AudioFormat.Aac };
                }
            }
            if (source == AudioFormat.Ac3)
            {
                if (package == OutputContainerFormat.Matroska)
                {
                    result = new AudioFormat[5] { AudioFormat.Aac, AudioFormat.Ac3, AudioFormat.Flac,
                        AudioFormat.Vorbis,  AudioFormat.Wav };
                }
                if (package == OutputContainerFormat.Mpeg4)
                {
                    result = new AudioFormat[2] { AudioFormat.Aac, AudioFormat.Ac3 };
                }
                if (package == OutputContainerFormat.None)
                {
                    result = new AudioFormat[4] { AudioFormat.Aac, AudioFormat.Flac, AudioFormat.Vorbis, AudioFormat.Wav };
                }
            }
            if (source == AudioFormat.Dts)
            {
                if (package == OutputContainerFormat.Matroska)
                {
                    result = new AudioFormat[5] { AudioFormat.Aac, AudioFormat.Dts, AudioFormat.Flac,
                        AudioFormat.Vorbis,  AudioFormat.Wav};
                }
                if (package == OutputContainerFormat.Mpeg4)
                {
                    result = new AudioFormat[1] { AudioFormat.Aac };
                }
                if (package == OutputContainerFormat.None)
                {
                    result = new AudioFormat[4] { AudioFormat.Aac, AudioFormat.Flac, AudioFormat.Vorbis, AudioFormat.Wav };
                }
            }
            if (source == AudioFormat.Flac)
            {
                if (package == OutputContainerFormat.Matroska)
                {
                    result = new AudioFormat[1] { AudioFormat.Flac };
                }
            }
            if (source == AudioFormat.Vorbis)
            {
                if (package == OutputContainerFormat.Matroska)
                {
                    result = new AudioFormat[1] { AudioFormat.Vorbis };
                }
            }
            if (source == AudioFormat.Wav)
            {
                if (package == OutputContainerFormat.Matroska)
                {
                    result = new AudioFormat[4] { AudioFormat.Aac, AudioFormat.Flac, AudioFormat.Vorbis, AudioFormat.Wav };
                }
                if (package == OutputContainerFormat.Mpeg4)
                {
                    result = new AudioFormat[1] { AudioFormat.Aac };
                }
                if (package == OutputContainerFormat.None)
                {
                    result = new AudioFormat[3] { AudioFormat.Aac, AudioFormat.Flac, AudioFormat.Vorbis };
                }
            }

            return result;
        }

        /// <summary>
        /// Returns the index of the specified job output filename in the jobs collection.
        /// </summary>
        /// <param name="sourceFilename">The job SourceFilename property.</param>
        /// <param name="baseFilename">The job BaseFilename property.</param>
        /// <param name="jobs">The jobs collection to search.</param>
        /// <returns>The index of the ouput filename in the jobs list if found, otherwise -1 is returned.</returns>
        /// <exception cref="System.ArgumentException">Thrown when filename is null or empty.</exception>
        /// <exception cref="System.ArgumentNullException">Thrown when jobs is null.</exception>
        public static Int32 IndexOfJobOutputFilename(string sourceFilename, string baseFilename, List<JobInfo> jobs)
        {
            if (string.IsNullOrEmpty(sourceFilename))
            {
                throw new ArgumentException("sourceFilename is null or empty.");
            }
            if (string.IsNullOrEmpty(baseFilename))
            {
                throw new ArgumentException("baseFilename is null or empty.");
            }
            if (jobs == null)
            {
                throw new ArgumentNullException("jobs");
            }

            Int32 result = -1;

            for (Int32 i = 0; i < jobs.Count; i++)
            {
                if (System.IO.Path.GetDirectoryName(jobs[i].SourceFilename) + "\\" + jobs[i].BaseFilename
                    == System.IO.Path.GetDirectoryName(sourceFilename) + "\\" + baseFilename)
                {
                    result = i;
                    break;
                }
            }

            return result;
        }

        /// <summary>
        /// Returns the index of the specified job source filename in the jobs collection.
        /// </summary>
        /// <param name="filename">The source filename to find.</param>
        /// <param name="jobs">The jobs collection to search.</param>
        /// <returns>The index of the source filename in the jobs list if found, otherwise -1 is returned.</returns>
        /// <exception cref="System.ArgumentException">Thrown when filename is null or empty.</exception>
        /// <exception cref="System.ArgumentNullException">Thrown when jobs is null.</exception>
        public static Int32 IndexOfJobSourceFilename(string filename, List<JobInfo> jobs)
        {
            if (string.IsNullOrEmpty(filename))
            {
                throw new ArgumentException("filename is null or empty.");
            }
            if (jobs == null)
            {
                throw new ArgumentNullException("jobs");
            }

            Int32 result = -1;

            for (Int32 i = 0; i < jobs.Count; i++)
            {
                if (jobs[i].SourceFilename == filename)
                {
                    result = i;
                    break;
                }
            }

            return result;
        }

        /// <summary>
        /// Returns a value indicating whether AviSynth is installed on the current system.
        /// </summary>
        /// <returns></returns>
        public static bool IsAvisynthInstalled()
        {
            bool result = false;

            // DLL found? Registry key exists?
            if (System.IO.File.Exists(Environment.SystemDirectory + "\\avisynth.dll") &&
                Registry.LocalMachine.OpenSubKey("Software\\AviSynth") != null)
            {
                result = true;
            }

            return result;
        }

        /// <summary>
        /// Scans for audio tracks and adds them to the job, replacing any existing tracks.
        /// </summary>
        /// <param name="job">The job to add tracks to.</param>
        /// <param name="config">The configuration data to use.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when job is null.</exception>
        /// <exception cref="System.ArgumentNullException">Thrown when config is null.</exception>
        /// <remarks>
        /// Any files with zero length are not added to the job.
        /// </remarks>
        public static void LoadAudioTracks(JobInfo job, ConfigurationInfo config)
        {
            if (job == null)
            {
                throw new ArgumentNullException("job");
            }
            if (config == null)
            {
                throw new ArgumentNullException("config");
            }

            // Try to auto-load audio tracks by finding all files that start with the same text
            // as the video filename, up to the first space in the name.
            job.AudioTracks.Clear();

            // Base filename used when searching for audio tracks.
            string baseFilename = string.Empty;
            if (System.IO.Path.GetFileNameWithoutExtension(job.SourceFilename).IndexOf(' ') > -1)
            {
                baseFilename = System.IO.Path.GetFileNameWithoutExtension(job.SourceFilename).Substring(
                     0, System.IO.Path.GetFileNameWithoutExtension(job.SourceFilename).IndexOf(' '));
            }
            else
            {
                baseFilename = System.IO.Path.GetFileNameWithoutExtension(job.SourceFilename);
            }

            // Find AC3 AND DTS files.
            List<string> files = new List<string>();

            files.AddRange(System.IO.Directory.GetFiles(System.IO.Path.GetDirectoryName(job.SourceFilename),
                baseFilename + "*.ac3", SearchOption.TopDirectoryOnly));

            files.AddRange(System.IO.Directory.GetFiles(System.IO.Path.GetDirectoryName(job.SourceFilename),
                baseFilename + "*.dts", SearchOption.TopDirectoryOnly));

            // Add found files to job if not already in the collection.
            for (Int32 i = 0; i < files.Count; i++)
            {
                // Not already in list? File length non-zero?
                if (!job.ContainsAudioTrack(files[i]) && new System.IO.FileInfo(files[i]).Length > 0)
                {
                    // Examples:
                    // VTS_02_PGC_02_1 - 0x80 - Audio - AC3 - 2ch - 48kHz - DRC - English - DELAY 0ms.AC3
                    // VTS_01_PGC_01_1 - 0x82 - Audio - AC3 - 2ch - 48kHz - DRC - English - Alternate Director's Comments - DELAY 0ms.AC3
                    // VTS_02_PGC_01_1 T80 2_0ch 192Kbps DELAY 0ms.ac3

                    AudioTrackInfo track = new AudioTrackInfo();
                    track.Downmix = config.DownmixAudio;
                    track.DynamicRangeCompression = config.AudioDynamicRangeCompression;
                    track.Filename = files[i];
                    track.Normalize = config.NormalizeAudio;
                    track.OutputFormat = config.AudioFormat;
                    string filename = files[i].ToUpperInvariant();

                    // Parse delay.
                    Int32 indexDelay = filename.IndexOf("DELAY");
                    if (indexDelay != -1)
                    {
                        Int32 indexDelayMs = filename.IndexOf("MS", indexDelay);
                        if (indexDelayMs != -1)
                        {
                            track.Delay = Convert.ToInt32(filename.Substring(indexDelay + 6, indexDelayMs - indexDelay - 6));
                        }
                    }

                    // Only added if filename is not empty.
                    if (track.Filename.Length > 0)
                    {
                        job.AudioTracks.Add(track);
                    }
                }
            }
        }

        /// <summary>
        /// Pluralizes the specified text based on the value of count.
        /// </summary>
        /// <param name="singular">The singular form of the text.</param>
        /// <param name="plural">The plural form of the text.</param>
        /// <param name="count">The number of items.</param>
        /// <returns>Returns a string containing the pluralized text.</returns>
        /// <exception cref="System.ArgumentException">Thrown when singular is null or empty.</exception>
        /// <exception cref="System.ArgumentException">Thrown when plural is null or empty.</exception>
        /// <remarks>
        /// The singular text is returned when count equals one. THe plural text is returned when count
        /// is less or greater than one. Does not work with most languages.
        /// </remarks>
        public static string Pluralize(string singular, string plural, Int32 count)
        {
            if (string.IsNullOrEmpty(singular))
            {
                throw new ArgumentException("singular is null or empty.");
            }
            if (string.IsNullOrEmpty(plural))
            {
                throw new ArgumentException("plural is null or empty.");
            }

            if (count == 1)
            {
                return singular;
            }
            else
            {
                return plural;
            }
        }
        /// <summary>
        /// Performs a quick index of the video to get basic information.
        /// </summary>
        /// <param name="filename">The filename of the video to index.</param>
        /// <param name="job">The job to index.</param>
        /// <param name="config">The application configuration data.</param>
        /// <exception cref="System.ArgumentException">Thrown when filename is null or empty.</exception>
        /// <exception cref="System.ArgumentNullException">Thrown when job is null.</exception>
        public static void PreviewIndexVideo(string filename, JobInfo job, ConfigurationInfo config)
        {
            if (string.IsNullOrEmpty(filename))
            {
                throw new ArgumentException("filename is null or empty.");
            }
            if (job == null)
            {
                throw new ArgumentNullException("job");
            }
            if (config == null)
            {
                throw new ArgumentNullException("config");
            }

            job.SourceFilename = filename;
            job.BaseFilename = System.IO.Path.GetFileNameWithoutExtension(job.SourceFilename);

            // Preview video.
            Tools.DgindexBackgroundWorker dgindex = new Tools.DgindexBackgroundWorker(Application.StartupPath +
                "\\Tools\\DGIndex.exe", job.SourceFilename, true);
            dgindex.Start();

            while (!dgindex.HasExited)
            {
                Thread.Sleep(100);
            }
            Thread.Sleep(250);

            // Copy data.
            job.Crop = config.Crop;
            job.Copy(dgindex);

            // Cleanup.
            dgindex = null;
            System.IO.File.Delete(System.IO.Path.ChangeExtension(job.SourceFilename, "log"));

            // Update resolution.
            Utility.ComputeVideoResizeValues(job);
            job.CropAspectRatio = job.DisplayAspectRatio;
        }

        /// <summary>
        /// Waits for the file system to update so that accessing the file does not cause a
        /// FileNotFound exception.
        /// </summary>
        /// <param name="filename">The file to wait for.</param>
        /// <exception cref="System.ArgumentException">Thrown when filename is null or empty.</exception>
        public static void WaitForFile(string filename)
        {
            if (string.IsNullOrEmpty(filename))
            {
                throw new ArgumentException("filename is null or empty.");
            }

            while (!System.IO.File.Exists(filename))
            {
                Thread.Sleep(100);
            }
        }
        
        /// <summary>
        /// Tries to convert a string to an enumeration object. If failure occurs,
        /// a default enumeration is returned.
        /// </summary>
        /// <param name="enumType">The type of the enumeration.</param>
        /// <param name="value">A string containing the text value to convert.</param>
        /// <param name="ignoreCase">Whethet to ignore the text casing.</param>
        /// <param name="defaultEnum">The enumeration object to return if the value
        /// cannot be parsed.</param>
        /// <returns>An object of type enumType whose value is represented by value.
        /// If value is not found, defaultEnum is returned.</returns>
        /// <exception cref="System.ArgumentNullException">Thrown when enumType is null.</exception>
        /// <exception cref="System.ArgumentNullException">Thrown when value is null.</exception>
        /// <exception cref="System.ArgumentException">Thrown when enumType is not an enum.</exception>
        /// <exception cref="System.ArgumentException">Thrown when value is empty.</exception>
        public static Object TryParseEnumeration(Type enumType, string value, Boolean ignoreCase, Object defaultEnum)
        {
            if (enumType == null)
            {
                throw new ArgumentNullException("enumType");
            }
            if (value == null)
            {
                throw new ArgumentNullException("value");
            }
            if (!enumType.IsEnum)
            {
                throw new ArgumentException("enumType is not an enumeration.", "enumType");
            }

            value = value.Trim();
            if (value.Length == 0)
            {
                throw new ArgumentException("value is empty.", "value");
            }

            // Get all named constants for the enumeration.
            string[] names = System.Enum.GetNames(enumType);
            if (names.Length == 0)
            {
                return defaultEnum;
            }

            // Does the named constant exist as a member of the enumeration?
            Int32 index = -1;
            if (ignoreCase)
            {
                value = value.ToUpperInvariant();
            }
            for (Int32 i = 0; i < names.Length; i++)
            {
                string s = names[i];
                if (ignoreCase)
                {
                    s = s.ToUpperInvariant();
                }

                if (s == value)
                {
                    index = i;
                    break;
                }
            }

            // Not found? Return default enum.
            if (index == -1)
            {
                return defaultEnum;
            }
            else
            {
                // Found! Return parsed enum.
                Array values = System.Enum.GetValues(enumType);

                return System.Enum.ToObject(enumType, values.GetValue(index));
            }
        }

        /// <summary>
        /// Wraps the specified text to the maximum length per line.
        /// </summary>
        /// <param name="value">The text to wrap.</param>
        /// <param name="maxLength">The maximum line length.</param>
        /// <returns>The wrapped text with each line followed by a newline ('\n') character..</returns>
        /// <exception cref="System.ArgumentException">Thrown when value is null or empty.</exception>
        /// <remarks>
        /// If a word is longer than the maximum width, it will be added as a single line.
        /// </remarks>
        public static string WrapText(string value, Int32 maxLength)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value is null or empty.");
            }

            Int32 previousIndex = -1;
            Int32 previousNewline = -1;
            StringBuilder sb = new StringBuilder();

            // Loop through chars, tracking spaces.
            for (Int32 i = 0; i < value.Length; i++)
            {
                if (value[i] == ' ' || i == value.Length - 1)
                {
                    // Current index minus previous newline index is greater than length?
                    if (i - previousNewline >= maxLength)
                    {
                        // Copy substring from the previous newline to the previous blank space.
                        sb.AppendLine(value.Substring(previousNewline + 1, previousIndex - previousNewline - 1));

                        // Previous index is now the previous newline index.
                        previousNewline = previousIndex;
                    }

                    previousIndex = i;
                }
            }

            // Append final section of string.
            sb.AppendLine(value.Substring(previousNewline + 1));

            return sb.ToString();
        }

        /// <summary>
        /// Generates an AviSynth script and writes it to the source folder. No log updates are performed.
        /// Used by the ConvertAudioDialog.
        /// </summary>
        /// <param name="filename">The audio filename.</param>
        /// <param name="source">The suadio format for the source audio.</param>
        /// <param name="drc">A value indicating whether AC3 audio should have Dynamic Range
        /// Compression (DRC) applied.</param>
        /// <param name="normalize">A value indicating whether the output audio levels should be normalized.</param>
        /// <exception cref="System.ArgumentException">Thrown when filename is null or empty.</exception>
        /// <exception cref="System.IO.FileNotFoundException">Thrown when filename is not found.</exception>
        public static void WriteAvisynthAudioScript(string filename, AudioFormat source, bool drc, bool normalize)
        {
            if (string.IsNullOrEmpty(filename))
            {
                throw new ArgumentException("filename is null or empty.");
            }
            if (!System.IO.File.Exists(filename))
            {
                throw new System.IO.FileNotFoundException("'" + filename + "' not found.");
            }

            StringBuilder sb = new StringBuilder();
            sb.AppendLine("# Load Required Plugin");
            sb.AppendLine("LoadPlugin(\"" + Application.StartupPath + "\\Tools\\NicAudio.dll\")");

            if (source == AudioFormat.Ac3)
            {
                // Apply Dynamic Range Compression (DRC) to AC3?
                if (drc)
                {
                    // Always downmix 6 channel to 2 channel.
                    sb.AppendLine("# Load Audio Track, Downmix to 2 Channels, Apply DRC");
                    sb.AppendLine("audio=NicAC3Source(\"" + filename + "\",Channels=2,DRC=1)");
                    sb.AppendLine();
                }
                else
                {
                    // Always downmix 6 channel to 2 channel.
                    sb.AppendLine("# Load Audio Track, Downmix to 2 Channels");
                    sb.AppendLine("audio=NicAC3Source(\"" + filename + "\",Channels=2)");
                    sb.AppendLine();
                }
            }
            if (source == AudioFormat.Dts)
            {
                // Apply Dynamic Range Compression (DRC) to DTS?
                if (drc)
                {
                    // Always downmix 6 channel to 2 channel.
                    sb.AppendLine("# Load Audio Track, Downmix to 2 Channels, Apply DRC");
                    sb.AppendLine("audio=NicDTSSource(\"" + filename + "\",Channels=2,DRC=1)");
                    sb.AppendLine();
                }
                else
                {
                    // Always downmix 6 channel to 2 channel.
                    sb.AppendLine("# Load Audio Track, Downmix to 2 Channels");
                    sb.AppendLine("audio=NicDTSSource(\"" + filename + "\",Channels=2)");
                    sb.AppendLine();
                }
            }

            // Normalize audio levels?
            if (normalize)
            {
                sb.AppendLine("# Normalize Audio Levels");
                sb.AppendLine("Normalize(audio)");
                sb.AppendLine();
            }


            System.IO.File.WriteAllText(System.IO.Path.ChangeExtension(filename, "avs"), sb.ToString());

            // Wait for file system to update.
            WaitForFile(System.IO.Path.ChangeExtension(filename, "avs"));
        }

        /// <summary>
        /// Generates an AviSynth script based on the AudioStreamInfo and writes it to the source folder. Log
        /// updates are performed.
        /// </summary>
        /// <param name="job">The JobInfo instance used to generate the script.</param>
        /// <param name="audioTrackID">The current audio track id.</param>
        /// <param name="log">The log file writer.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when job is null.</exception>
        /// <exception cref="System.ArgumentNullException">Thrown when log is null.</exception>
        public static void WriteAvisynthAudioScript(JobInfo job, Int32 audioTrackID, LogWriter log)
        {
            if (job == null)
            {
                throw new ArgumentNullException("job");
            }
            if (log == null)
            {
                throw new ArgumentNullException("log");
            }

            log.AppendLine("==> AviSYnth Audio Conversion Script Generated (" + DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt") + ")");
            log.AppendLine();

            StringBuilder sb = new StringBuilder();
            sb.AppendLine("# Load Required Plugin");
            sb.AppendLine("LoadPlugin(\"" + Application.StartupPath + "\\Tools\\NicAudio.dll\")");
            sb.AppendLine();

            // Create a blank video clip.
            sb.AppendLine("# Create Blank Video Clip");
            sb.Append("video=BlankClip(length=" + job.OutputFrameCount.ToString() + ", width=16, height=16, color=$000000, fps=");
            if (job.OutputFramerate == VideoFramerate.Fps24)
            {
                sb.AppendLine("24000, fps_denominator=1001)");
            }
            if (job.OutputFramerate == VideoFramerate.Fps25)
            {
                if (job.ProcessingOverride == VideoProcessingType.PalToNtsc)
                {
                    sb.AppendLine("50)");
                }
                else
                {
                    sb.AppendLine("25)");
                }
            }
            if (job.OutputFramerate == VideoFramerate.Fps30)
            {
                sb.AppendLine("30000, fps_denominator=1001)");
            }
            sb.AppendLine();

            if (job.AudioTracks[audioTrackID].Format == AudioFormat.Ac3)
            {
                // Apply Dynamic Range Compression (DRC) to AC3?
                if (job.AudioTracks[audioTrackID].DynamicRangeCompression)
                {
                    // Always downmix 6 channel to 2 channel.
                    sb.AppendLine("# Load Audio Track, Downmix to 2 Channels, Apply DRC");
                    sb.AppendLine("audio=NicAC3Source(\"" + job.AudioTracks[audioTrackID].Filename + "\",Channels=2,DRC=1)");
                    sb.AppendLine();
                }
                else
                {
                    // Always downmix 6 channel to 2 channel.
                    sb.AppendLine("# Load Audio Track, Downmix to 2 Channels");
                    sb.AppendLine("audio=NicAC3Source(\"" + job.AudioTracks[audioTrackID].Filename + "\",Channels=2)");
                    sb.AppendLine();
                }
            }
            if (job.AudioTracks[audioTrackID].Format == AudioFormat.Dts)
            {
                // Apply Dynamic Range Compression (DRC) to DTS?
                if (job.AudioTracks[audioTrackID].DynamicRangeCompression)
                {
                    // Always downmix 6 channel to 2 channel.
                    sb.AppendLine("# Load Audio Track, Downmix to 2 Channels, Apply DRC");
                    sb.AppendLine("audio=NicDTSSource(\"" + job.AudioTracks[audioTrackID].Filename + "\",Channels=2,DRC=1)");
                    sb.AppendLine();
                }
                else
                {
                    // Always downmix 6 channel to 2 channel.
                    sb.AppendLine("# Load Audio Track, Downmix to 2 Channels");
                    sb.AppendLine("audio=NicDTSSource(\"" + job.AudioTracks[audioTrackID].Filename + "\",Channels=2)");
                    sb.AppendLine();
                }
            }

            // Normalize audio levels?
            if (job.AudioTracks[audioTrackID].Normalize)
            {
                sb.AppendLine("# Normalize Audio Levels");
                sb.AppendLine("Normalize(audio)");
                sb.AppendLine();
            }

            // Mux A/V
            sb.AppendLine("# Mux A/V");
            sb.AppendLine("AudioDub(video, audio)");
            sb.AppendLine();

            // Trim?
            if (job.Trim && (job.TrimOutputStartTimecode.TotalFrames > 0 || job.TrimOutputEndTimecode.TotalFrames > 0))
            {
                Int32 startFrame = job.TrimOutputStartTimecode.TotalFrames;
                Int32 endFrame = job.TrimOutputEndTimecode.TotalFrames;

                // PAL Bob doubles framerate, so double the trim range values.
                if (job.ProcessingOverride ==VideoProcessingType.PalToNtsc)
                {
                    startFrame *= 2;
                    endFrame *= 2;
                }

                sb.AppendLine("# Trim  [" + startFrame.ToString() + "]  ---> [" + endFrame.ToString() + "], " +
                    (endFrame - startFrame + 1).ToString() + "frames");
                sb.AppendLine("Trim(" + startFrame.ToString() + ", " + endFrame.ToString() + ")");
            }

            System.IO.File.WriteAllText(System.IO.Path.ChangeExtension(job.AudioTracks[audioTrackID].Filename, "avs"), sb.ToString());

            // Update log.
            log.Append(sb);
            sb.Length = 0;

            log.AppendLine();
            log.AppendLine();
            log.UpdateLog();

            // Wait for file system to update.
            WaitForFile(System.IO.Path.ChangeExtension(job.AudioTracks[audioTrackID].Filename, "avs"));
        }

        /// <summary>
        /// Generates an AviSynth script based on the job and video info and writes it to the source folder.
        /// </summary>
        /// <param name="job">The JobInfo instance used to generate the script.</param>
        /// <param name="log">The log file writer.</param>
        /// <exception cref="System.ArgumentNullException">Thrown when job is null.</exception>
        /// <exception cref="System.ArgumentNullException">Thrown when log is null.</exception>
        public static void WriteAvisynthVideoScript(JobInfo job, LogWriter log)
        {
            if (job == null)
            {
                throw new ArgumentNullException("job");
            }
            if (log == null)
            {
                throw new ArgumentNullException("log");
            }

            // Processing
            VideoProcessingType processing = job.Processing;
            if (job.ProcessingOverrideEnabled)
            {
                processing = job.ProcessingOverride;
            }

            // Start script generation.
            log.AppendLine("==> AviSYnth Script Generated (" + DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss.fff tt") + ")");
            log.AppendLine();

            StringBuilder sb = new StringBuilder();
            sb.AppendLine("# Load Required Plugins");
            if (job.Crop == CropMode.Auto)
            {
                sb.AppendLine("LoadPlugin(\"" + Application.StartupPath + "\\Tools\\AutoCrop.dll\")");
            }
            sb.AppendLine("LoadPlugin(\"" + Application.StartupPath + "\\Tools\\DGDecode.dll\")");
            if (processing == VideoProcessingType.PalToNtsc)
            {
                sb.AppendLine("LoadPlugin(\"" + Application.StartupPath + "\\Tools\\LeakKernelDeint.dll\")");
            }
            if (processing == VideoProcessingType.NtscDeinterlace)
            {
                sb.AppendLine("LoadPlugin(\"" + Application.StartupPath + "\\Tools\\TDeint.dll\")");
            }
            sb.AppendLine("LoadPlugin(\"" + Application.StartupPath + "\\Tools\\TIVTC.dll\")");
            sb.AppendLine("LoadPlugin(\"" + Application.StartupPath + "\\Tools\\vinverse.dll\")");
            sb.AppendLine();

            sb.AppendLine("# Load Video Source");
            sb.AppendLine("video=mpeg2source(\"" + System.IO.Path.ChangeExtension(job.SourceFilename, "d2v") + "\",idct=3)");

            // Convert color space to YV12.
            if (processing == VideoProcessingType.NtscDeinterlace ||
                processing == VideoProcessingType.PalToNtsc)
            {
                sb.AppendLine("ConvertToYV12(video,interlaced=true)");
            }
            else
            {
                sb.AppendLine("ConvertToYV12(video)");
            }

            // Field Order
            if (job.FieldOrder == VideoFieldOrder.TopFieldFirst)
            {
                sb.AppendLine("AssumeTFF(video)");
            }
            else
            {
                sb.AppendLine("AssumeBFF(video)");
            }
            sb.AppendLine();

            // Processing types
            sb.AppendLine("# " + Utility.ConvertVideoProcessingTypeToName(processing));

            if (processing == VideoProcessingType.NtscDeinterlace)
            {
                if (job.IsAnimation)
                {
                    sb.AppendLine("TDeint(type=3,clip2=video,tryWeave=true,denoise=true)");
                }
                else
                {
                    sb.AppendLine("TDeint(type=5,clip2=video,tryWeave=true,denoise=true)");
                }
            }

            if (processing == VideoProcessingType.PalToNtsc)
            {
                sb.AppendLine("LeakKernelBob(Order=1)");
                sb.AppendLine("ChangeFPS(59.94)");
                sb.AppendLine("SeparateFields()");
                if (job.FieldOrder == VideoFieldOrder.TopFieldFirst)
                {
                    sb.AppendLine("SelectEvery(4,1,2)");
                }
                else
                {
                    sb.AppendLine("SelectEvery(4,0,3)");
                }
                sb.AppendLine("DoubleWeave()");
                sb.AppendLine("LeakKernelDeint(order=1)");
                sb.AppendLine("ChangeFPS(29.970)");
            }

            if (processing != VideoProcessingType.NtscDeinterlace &&
                processing != VideoProcessingType.PalToNtsc)
            {
                sb.AppendLine("TFM(d2v=\"" + System.IO.Path.ChangeExtension(job.SourceFilename, "d2v") + "\",clip2=video,cthresh=5)");
            }
            if (processing == VideoProcessingType.NtscHybridFilm)
            {
                sb.AppendLine("TDecimate(mode=1,hybrid=1) ");
            }
            if (processing == VideoProcessingType.NtscHybridFilmAnime)
            {
                sb.AppendLine("TDecimate(hybrid=1) ");
            }
            if (processing == VideoProcessingType.NtscHybridVideo)
            {
                sb.AppendLine("TDecimate(mode=1,hybrid=3) ");
            }
            if (processing == VideoProcessingType.NtscHybridVideoAnime)
            {
                sb.AppendLine("TDecimate(hybrid=3) ");
            }
            if (processing == VideoProcessingType.NtscIvtc)
            {
                sb.AppendLine("TDecimate() ");
            }
            if (processing == VideoProcessingType.NtscIvtcAnime)
            {
                sb.AppendLine("TDecimate(mode=1) ");
            }
            if (processing == VideoProcessingType.Pal)
            {
                // Field matching, no decimation.
            }
            if (processing == VideoProcessingType.PalFilm)
            {
                sb.AppendLine("TDecimate(cycle=25) ");
            }
            sb.AppendLine();

            // Decomb any stray artifacts.
            sb.AppendLine("# Decomb");
            sb.AppendLine("vinverse()");
            sb.AppendLine();

            // Resize based on the video type and whether it is animorphic.
            sb.Append("# Resize");
            if (job.VideoType == VideoType.PalVideo)
            {
                sb.Append(" PAL");
            }
            else
            {
                sb.Append(" NTSC");
            }

            if (job.DisplayAspectRatio == VideoAspectRatio.Aspect_1_33)
            {
                sb.AppendLine(" Fullscreen");
            }
            if (job.DisplayAspectRatio == VideoAspectRatio.Aspect_1_78)
            {
                sb.AppendLine(" Widescreen");
            }
            sb.AppendLine("LanczosResize(" + job.ResizeWidth + ", " + job.ResizeHeight + ")");
            sb.AppendLine();

            // Manual cropping enabled?
            if (job.Crop == CropMode.None)
            {
                sb.AppendLine("# No Cropping");
            }
            if (job.Crop == CropMode.Manual)
            {
                sb.AppendLine("# Manual Cropping");
                if (job.CropLeft > 0 || job.CropRight > 0 || job.CropTop > 0 || job.CropBottom > 0)
                {
                    sb.AppendLine("Crop(" + job.CropLeft.ToString() + "," + job.CropTop.ToString() + ",-" +
                        job.CropRight.ToString() + ",-" + job.CropBottom.ToString() + ")");
                }
                else
                {
                    sb.AppendLine("# Crop(0, 0, 0, 0)");
                }
            }
            if (job.Crop == CropMode.Auto)
            {
                // Use external AutoCrop filter. A mod of 16 is used otherwise artifacts can appear along edges.
                // To disable AutoCrop, set crop mode to 'None'.
                sb.AppendLine("# Auto Cropping");
                sb.AppendLine("# Overcropping can occur - always perform a trimmed video run first.");
                sb.AppendLine("# To Disable, Choose Manual Crop With All Values Zero");
                sb.AppendLine("AutoCrop(mode=0,wMultOf=16,hMultOf=16)");
            }
            if (job.Crop == CropMode.Aspect)
            {
                // Use internal AutoCrop based on aspect ratio. To disable AutoCrop, set crop mode to 'None'.
                sb.AppendLine("# Aspect Cropping");

                Int32 videoHeight = 0;
                Int32 videoWidth = 0;

                Utility.ComputeJobCroppedVideoResolution(job, ref videoHeight, ref videoWidth);

                Int32 horizontalMargin = (job.ResizeWidth - videoWidth) / 2;
                Int32 verticalMargin = (job.ResizeHeight - videoHeight) / 2;

                if (horizontalMargin > 0 || verticalMargin > 0)
                {
                    sb.AppendLine("# To Disable, Choose Manual Crop With All Values Zero");
                    sb.AppendLine("Crop(" + horizontalMargin + "," + verticalMargin + ",-" +
                        horizontalMargin + ",-" + verticalMargin + ")");
                }
                else
                {
                    sb.AppendLine("# Crop(0, 0, 0, 0)");
                }
            }
            sb.AppendLine();

            // Trim?
            if (job.Trim && (job.TrimOutputStartTimecode.TotalFrames > 0 || job.TrimOutputEndTimecode.TotalFrames > 0))
            {
                Int32 startFrame = job.TrimOutputStartTimecode.TotalFrames;
                Int32 endFrame = job.TrimOutputEndTimecode.TotalFrames;

                // PAL Bob doubles framerate, so double the trim range values.
                if (job.ProcessingOverride == VideoProcessingType.PalToNtsc)
                {
                    startFrame *= 2;
                    endFrame *= 2;
                }

                sb.AppendLine("# Trim, start=" + startFrame.ToString() + ", duration=" + (endFrame - startFrame).ToString() + " frames");
                sb.AppendLine("Trim(" + startFrame.ToString() + ", " + endFrame.ToString() + ")");
            }

            // Save to file.
            System.IO.File.WriteAllText(System.IO.Path.ChangeExtension(job.SourceFilename, "avs"), sb.ToString());

            // Update log.
            log.Append(sb);
            sb = null;

            log.AppendLine();
            log.AppendLine();
            log.UpdateLog();

            // Wait for file system to update.
            WaitForFile(System.IO.Path.ChangeExtension(job.SourceFilename, "avs"));
        }

        #endregion
    }
}