using System;
using System.Collections.Generic;
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[3] 
        {
            "Auto", "Aspect", "Manual"
        };
        /// <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";
   
        #endregion

        #region Methods

        /// <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 DetermineVideoProcessing(JobInfo job)
        {
            if (job == null)
            {
                throw new ArgumentNullException("job");
            }

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

            if (job.VideoType == FicSprout.Bacicworx.Media.VideoType.NtscVideo &&
                job.FrameType == FicSprout.Bacicworx.Media.VideoFrameType.Progressive)
            {
                // Video less than one hour (3600 seconds) is usually 30fps interlaced.
                if (job.FrameCount / FicSprout.Bacicworx.Media.Utility.GetVideoFramerateValue(job.Framerate, true) < 3600)
                {
                    job.Processing = FicSprout.Bacicworx.Media.VideoProcessingType.NtscHybridVideo;
                }
                else
                {
                    // Video one hour or more is usually 24fps telecined film.
                    job.Processing = FicSprout.Bacicworx.Media.VideoProcessingType.NtscIvtc;
                }
            }
            if (job.VideoType == FicSprout.Bacicworx.Media.VideoType.NtscVideo &&
                job.FrameType == FicSprout.Bacicworx.Media.VideoFrameType.Interlaced)
            {
                job.Processing = FicSprout.Bacicworx.Media.VideoProcessingType.NtscDeinterlace;
            }
            if (job.VideoType == FicSprout.Bacicworx.Media.VideoType.NtscHybridFilm)
            {
                job.Processing = FicSprout.Bacicworx.Media.VideoProcessingType.NtscHybridFilm;

                if (job.IsAnimation)
                {
                    job.Processing = FicSprout.Bacicworx.Media.VideoProcessingType.NtscHybridFilmAnime;
                }
            }
            if (job.VideoType == FicSprout.Bacicworx.Media.VideoType.NtscHybridVideo)
            {
                job.Processing = FicSprout.Bacicworx.Media.VideoProcessingType.NtscHybridVideo;

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

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

                if (job.IsAnimation)
                {
                    job.Processing = FicSprout.Bacicworx.Media.VideoProcessingType.NtscIvtcAnime;
                }
            }
            if (job.VideoType == FicSprout.Bacicworx.Media.VideoType.PalVideo)
            {
                job.Processing = FicSprout.Bacicworx.Media.VideoProcessingType.Pal;
            }
            if (job.VideoType == FicSprout.Bacicworx.Media.VideoType.PalVideo &&
                job.FrameType == FicSprout.Bacicworx.Media.VideoFrameType.Progressive)
            {
                job.Processing = FicSprout.Bacicworx.Media.VideoProcessingType.PalFilm;
            }

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

            job.OutputFramerate = job.Framerate;
            if (processing == FicSprout.Bacicworx.Media.VideoProcessingType.None)
            {
                job.OutputFramerate = job.Framerate;
            }
            if (processing == FicSprout.Bacicworx.Media.VideoProcessingType.NtscHybridFilm ||
                processing == FicSprout.Bacicworx.Media.VideoProcessingType.NtscHybridFilmAnime ||
                processing == FicSprout.Bacicworx.Media.VideoProcessingType.NtscIvtc ||
                processing == FicSprout.Bacicworx.Media.VideoProcessingType.NtscIvtcAnime ||
                processing == FicSprout.Bacicworx.Media.VideoProcessingType.PalFilm
                )
            {
                job.OutputFramerate = FicSprout.Bacicworx.Media.VideoFramerate.Fps24;
            }
            if (processing == FicSprout.Bacicworx.Media.VideoProcessingType.Pal)
            {
                job.OutputFramerate = FicSprout.Bacicworx.Media.VideoFramerate.Fps25;
            }
            if (processing == FicSprout.Bacicworx.Media.VideoProcessingType.NtscDeinterlace ||
                processing == FicSprout.Bacicworx.Media.VideoProcessingType.NtscHybridVideo ||
                processing == FicSprout.Bacicworx.Media.VideoProcessingType.NtscHybridVideoAnime)
            {
                job.OutputFramerate = FicSprout.Bacicworx.Media.VideoFramerate.Fps30;
            }

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

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

            job.TrimOutputEndTimecode = new FicSprout.Bacicworx.Media.SmpteTimecode(job.TrimEndTimecode);
            job.TrimOutputEndTimecode.ConvertFramerate(job.OutputFramerate);
        }

        /// <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 GetAudioFormat(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value is null or empty.");
            }

            Int32 index = 0;
            for (int 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 GetAudioFormatExtension(AudioFormat value)
        {
            return AudioFormatExtensions[(Int32)value];
        }

        /// <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 GetAudioFormatFromExtension(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 (int i = 0; i < AudioFormatExtensions.Length; i++)
            {
                if (AudioFormatExtensions[i].ToUpperInvariant() == value)
                {
                    index = i;
                    break;
                }
            }

            return (AudioFormat)index;
        }

        /// <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 GetAudioFormatName(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[] GetAudioFormatNames(AudioFormat[] value)
        {
            string[] result = new string[value.Length];

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

            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[] GetAudioOutputFormats(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>
        /// 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 GetCropMode(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value is null or empty.");
            }

            Int32 index = 0;
            for (int 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 GetCropModeName(CropMode value)
        {
            return CropModeNames[(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 GetOutputContainerFormat(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException("value is null or empty.");
            }

            Int32 index = 0;
            for (int 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 GetOutputContainerFormatExtension(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 GetOutputContainerFormatName(OutputContainerFormat value)
        {
            return OutputContainerFormatNames[(Int32)value];
        }

        /// <summary>
        /// Calculates required video 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 GetVideoResizeValues(JobInfo job)
        {
            if (job == null)
            {
                throw new ArgumentNullException("job");
            }

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

        }

        /// <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 (int 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 (int 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 (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 (int 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>
        /// 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 LoadAudioTracks(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 (int 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>
        /// 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 QuickIndexVideo(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.
            DgindexBackgroundWorker dgindex = new 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.DisplayAspectRatio = dgindex.DisplayAspectRatio;
            job.FieldOrder = dgindex.FieldOrder;
            job.FrameCount = dgindex.FrameCount;
            job.Framerate = dgindex.Framerate;
            job.FrameType = dgindex.FrameType;
            job.OutputFramerate = dgindex.Framerate;
            job.SourceFilename = dgindex.SourceFilename;
            job.SourceHeight = dgindex.SourceHeight;
            job.SourceWidth = dgindex.SourceWidth;
            job.VideoType = dgindex.VideoType;
            job.VideoTypePercentage = dgindex.VideoTypePercentage;

            // Cleanup.
            dgindex = null;
            System.IO.File.Delete(System.IO.Path.ChangeExtension(job.SourceFilename, "log"));
        
            // Update resolution.
            Utility.GetVideoResizeValues(job);
            job.CropAspectRatio = job.DisplayAspectRatio;
        }

        /// <summary>
        /// Shuts down Windows.
        /// </summary>
        public static void ShutdownWindows()
        {
            ShutdownTimerDialog dialog = new ShutdownTimerDialog();
            dialog.ShowDialog();
        }

        /// <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>
        /// 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, FicSprout.Bacicworx.IO.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 == FicSprout.Bacicworx.Media.VideoFramerate.Fps24)
            {
                sb.AppendLine("24000, fps_denominator=1001)");
            }
            if (job.OutputFramerate == FicSprout.Bacicworx.Media.VideoFramerate.Fps25)
            {
                sb.AppendLine("25)");
            }
            if (job.OutputFramerate == FicSprout.Bacicworx.Media.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))
            {
                sb.AppendLine("# Trim  [" + job.TrimOutputStartTimecode.ToString() + "]  ---> [" +
                    job.TrimOutputEndTimecode.ToString() + "], " + (job.TrimOutputEndTimecode.TotalFrames - 
                    job.TrimOutputStartTimecode.TotalFrames + 1).ToString() + "frames");
                sb.AppendLine("Trim(" + job.TrimOutputStartTimecode.TotalFrames.ToString() + ", " +
                    job.TrimOutputEndTimecode.TotalFrames.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, FicSprout.Bacicworx.IO.LogWriter log)
        {
            if (job == null)
            {
                throw new ArgumentNullException("job");
            }
            if (log == null)
            {
                throw new ArgumentNullException("log");
            }

            // Processing
            FicSprout.Bacicworx.Media.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 == FicSprout.Bacicworx.Media.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)");
            sb.AppendLine("ConvertToYV12(video)");

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

            // Processing types
            sb.AppendLine("# " + FicSprout.Bacicworx.Media.Utility.GetVideoProcessingTypeName(processing));

            if (processing == FicSprout.Bacicworx.Media.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)");
                }
            }
            else
            {
                sb.AppendLine("TFM(d2v=\"" + System.IO.Path.ChangeExtension(job.SourceFilename, "d2v") + "\",clip2=video,cthresh=5)");

                if (processing == FicSprout.Bacicworx.Media.VideoProcessingType.NtscHybridFilm)
                {
                    sb.AppendLine("TDecimate(mode=1,hybrid=1) ");
                }
                if (processing == FicSprout.Bacicworx.Media.VideoProcessingType.NtscHybridFilmAnime)
                {
                    sb.AppendLine("TDecimate(hybrid=1) ");
                }
                if (processing == FicSprout.Bacicworx.Media.VideoProcessingType.NtscHybridVideo)
                {
                    sb.AppendLine("TDecimate(mode=1,hybrid=3) ");
                }
                if (processing == FicSprout.Bacicworx.Media.VideoProcessingType.NtscHybridVideoAnime)
                {
                    sb.AppendLine("TDecimate(hybrid=3) ");
                }
                if (processing == FicSprout.Bacicworx.Media.VideoProcessingType.NtscIvtc)
                {
                    sb.AppendLine("TDecimate() ");
                }
                if (processing == FicSprout.Bacicworx.Media.VideoProcessingType.NtscIvtcAnime)
                {
                    sb.AppendLine("TDecimate(mode=1) ");
                }
                if (processing == FicSprout.Bacicworx.Media.VideoProcessingType.Pal)
                {
                    // Field matching, no decimation.
                }
                if (processing == FicSprout.Bacicworx.Media.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 != FicSprout.Bacicworx.Media.VideoType.NtscFilm ||
                job.VideoType != FicSprout.Bacicworx.Media.VideoType.NtscHybridFilm ||
                job.VideoType != FicSprout.Bacicworx.Media.VideoType.NtscHybridVideo ||
                job.VideoType != FicSprout.Bacicworx.Media.VideoType.NtscVideo)
            {
                sb.Append(" NTSC");
            }
            if (job.VideoType != FicSprout.Bacicworx.Media.VideoType.PalVideo)
            {
                sb.Append(" PAL");
            }
            if (job.DisplayAspectRatio == FicSprout.Bacicworx.Media.VideoAspectRatio.Aspect_1_33)
            {
                sb.AppendLine(" Fullscreen");
            }
            if (job.DisplayAspectRatio == FicSprout.Bacicworx.Media.VideoAspectRatio.Aspect_1_78)
            {
                sb.AppendLine(" Widescreen");
            }
            sb.AppendLine("LanczosResize(" + job.ResizeWidth + ", " + job.ResizeHeight + ")");
            sb.AppendLine();

            // Manual cropping enabled?
            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 all crop values to zero.
                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 all crop values to zero.
                sb.AppendLine("# Aspect Cropping");
                
                if (job.CropLeft > 0 || job.CropRight > 0 || job.CropTop > 0 || job.CropBottom > 0)
                {
                    Int32 horizontalMargin = (job.ResizeWidth - job.CropLeft - job.CropRight) / 2;
                    Int32 verticalMargin = (job.ResizeHeight - job.CropBottom - job.CropTop) / 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))
            {
                sb.AppendLine("# Trim, start=" + job.TrimOutputStartTimecode.TotalFrames.ToString() + ", duration=" +
                    (job.TrimOutputEndTimecode.TotalFrames - job.TrimOutputStartTimecode.TotalFrames).ToString() + " frames");
                sb.AppendLine("Trim(" + job.TrimOutputStartTimecode.TotalFrames.ToString() + ", " +
                    job.TrimOutputEndTimecode.TotalFrames.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
    }
}