using System;
using System.Text;

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

        /// <summary>
        /// x264 supported decoder level names. Encoder default is 'Auto'.
        /// </summary>
        public static string[] DecoderLevelNames = new string[16] {"Auto", "1.0", "1.1", "1.2", "1.3",
            "2.0", "2.1", "2.2", "3.0", "3.1", "3.2", "4.0", "4.1", "4.2", "5.0", "5.1"};
        /// <summary>
        /// x264 supported encoder efficiency names. Encoder default is 'Medium'.
        /// </summary>
        public static string[] EncoderEfficiencyNames = new string[11] {"Default", "Ultra Fast", "Super Fast",
            "Very Fast", "Faster" , "Fast" , "Medium" , "Slow" , "Slower" , "Very Slow" , "Placebo"};
        /// <summary>
        /// x264 supported input tuning names. Encoder default is 'None'.
        /// </summary>
        public static string[] InputTuningNames = new string[10] {"None", "Animation", "Fast Decode", "Film",
            "Grain Retention", "PSNR", "SSIM", "Still Image", "TouHou", "Zero Latency"};
        /// <summary>
        /// x264 supported output level names. Encoder default is 'None'.
        /// </summary>
        public static string[] OutputProfileNames = new string[4] { "None", "Baseline", "Main", "High" };

        #endregion

        #region Methods

        /// <summary>
        /// Computes the x264 subpixel estimation complexity (subme) value from an EncoderQualityOption
        /// enumeration value.
        /// </summary>
        /// <param name="quality">EncoderQualityOption enumeration to convert.</param>
        /// <returns>An Int32 representing the subme value.</returns>
        public static Int32 ComputeSubme(EncoderQualityOption quality)
        {
            Int32 result = 7;

            if (quality == EncoderQualityOption.Archival)
            {
                result = 10;
            }

            return result;
        }

        /// <summary>
        /// Computes the x264 constant rate factor (crf) value from an EncoderQualityOption enumeration.
        /// </summary>
        /// <param name="value">EncoderQualityOption enumeration to convert.</param>
        /// <param name="sourceHeight">The height of the source video in pixels.</param>
        /// <returns>An Int32 representing the constant rate factor value.</returns>
        public static Int32 ComputeConstantRateFactor(EncoderQualityOption value, Int32 sourceHeight)
        {
            // Old values here. Newer values are slightly crisper and action more
            // fluid but increases file size and encoding time by approximately ~15%.
            //Int32[] constantRateFactor_1080 = new Int32[5] { 15, 16, 18, 20, 22 };
            //Int32[] constantRateFactor_720 = new Int32[5] { 18, 19, 21, 23, 25 };
            //Int32[] constantRateFactor_576 = new Int32[5] { 20, 21, 23, 25, 27 };
            //Int32[] constantRateFactor_480 = new Int32[5] { 21, 22, 24, 26, 28 };
            //Int32[] constantRateFactor_360 = new Int32[5] { 24, 25, 27, 29, 31 };
            
            // Value indexes correspond to EncoderQualityOption enum.
            Int32[] constantRateFactor_1080 = new Int32[5] { 11, 13, 15, 17, 19 };
            Int32[] constantRateFactor_720 = new Int32[5] { 15, 17, 19, 21, 23 };
            Int32[] constantRateFactor_576 = new Int32[5] { 17, 19, 21, 23, 25 };
            Int32[] constantRateFactor_480 = new Int32[5] { 19, 21, 23, 25, 27 };
            Int32[] constantRateFactor_360 = new Int32[5] { 23, 25, 27, 29, 31 };

            Int32 result = 21;
            if (sourceHeight <= 360)
            {
                result = constantRateFactor_360[(Int32)value];
            }
            if (sourceHeight > 360 && sourceHeight <= 480)
            {
                result = constantRateFactor_480[(Int32)value];
            }
            if (sourceHeight > 480 && sourceHeight <= 575)
            {
                result = constantRateFactor_720[(Int32)value];
            }
            // Custom processing for PAL video.
            if (sourceHeight > 575 && sourceHeight <= 720)
            {
                result = constantRateFactor_576[(Int32)value];
            }
            if (sourceHeight > 720 && sourceHeight <= 1080)
            {
                result = constantRateFactor_1080[(Int32)value];
            }

            return result;
        }

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

            DecoderLevel result = DecoderLevel.Auto;
            Int32 index = Array.IndexOf(DecoderLevelNames, value);

            if (index > -1)
            {
                result = (DecoderLevel)index;
            }

            return result;
        }

        /// <summary>
        /// Converts a DecoderLevel enumeration to an DecoderLevelName string.
        /// </summary>
        /// <param name="value">The DecoderLevel enumeration to convert.</param>
        /// <returns>An DecoderLevelName string.</returns>
        public static string ConvertDecoderLevelToName(DecoderLevel value)
        {
            return DecoderLevelNames[(Int32)value];
        }

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

            if (value == "Default")
            {
                value = "Medium";
            }

            EncoderEfficiency result = EncoderEfficiency.Medium;
            Int32 index = Array.IndexOf(EncoderEfficiencyNames, value);

            if (index > -1)
            {
                result = (EncoderEfficiency)index;
            }

            return result;
        }

        /// <summary>
        /// Converts a EncoderEfficiency enumeration to an EncoderEfficiencyName string.
        /// </summary>
        /// <param name="value">The EncoderEfficiency enumeration to convert.</param>
        /// <returns>An EncoderEfficiencyName string.</returns>
        public static string ConvertEncoderEfficiencyToName(EncoderEfficiency value)
        {
            return EncoderEfficiencyNames[(Int32)value];
        }

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

            InputTuning result = InputTuning.None;
            Int32 index = Array.IndexOf(InputTuningNames, value);

            if (index > -1)
            {
                result = (InputTuning)index;
            }

            return result;
        }

        /// <summary>
        /// Converts a InputTuning enumeration to an InputTuningName string.
        /// </summary>
        /// <param name="value">The InputTuning enumeration to convert.</param>
        /// <returns>An InputTuningName string.</returns>
        public static string ConvertInputTuningToName(InputTuning value)
        {
            return InputTuningNames[(Int32)value];
        }

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

            OutputProfile result = OutputProfile.None;
            Int32 index = Array.IndexOf(OutputProfileNames, value);

            // Name index found? 
            if (index != -1)
            {
                result = (OutputProfile)index;
            }

            return result;
        }

        /// <summary>
        /// Converts a OutputProfile enumeration to an OutputProfileName string.
        /// </summary>
        /// <param name="value">The OutputProfile enumeration to convert.</param>
        /// <returns>An OutputProfileName string.</returns>
        public static string ConvertOutputProfileToName(OutputProfile value)
        {
            return OutputProfileNames[(Int32)value];
        }

        /// <summary>
        /// Generates CLI arguments based on input values.
        /// </summary>
        /// <param name="source">The filename of the source video to encode.</param>
        /// <param name="output">the filename of the output video.</param>
        /// <param name="quality">The EncoderQualityOption setting.</param>
        /// <param name="level">The DecoderLevel setting.</param>
        /// <param name="efficiency">The EncoderEfficiency settting.</param>
        /// <param name="tuning">The InputTuning setting.</param>
        /// <param name="profile">The OutputProfile setting.</param>
        /// <param name="sourceHeight">The source video height in pixels.</param>
        /// <param name="reduceFilesize">A value indicating whether to reduce filesize by increasing the
        /// trellis quantization setting. Encoding time is increased by approximately 250%.</param>
        /// <param name="isAnimation">A value indicating whether the video is animation such as anime
        /// or cartoons.</param>
        /// <returns>A string containing the command line arguments.</returns>
        public static string GenerateArguments(string source, string output, EncoderQualityOption quality,
            DecoderLevel level, EncoderEfficiency efficiency, InputTuning tuning, OutputProfile profile,
            Int32 sourceHeight, bool reduceFilesize, bool isAnimation)
        {
            return GenerateArguments(source, output, quality, level, efficiency, tuning, profile, sourceHeight,
                reduceFilesize, isAnimation, 0, 0);
        }

        /// <summary>
        /// Generates CLI arguments based on input values.
        /// </summary>
        /// <param name="source">The filename of the source video to encode.</param>
        /// <param name="output">the filename of the output video.</param>
        /// <param name="quality">The EncoderQualityOption setting.</param>
        /// <param name="level">The DecoderLevel setting.</param>
        /// <param name="efficiency">The EncoderEfficiency settting.</param>
        /// <param name="tuning">The InputTuning setting.</param>
        /// <param name="profile">The OutputProfile setting.</param>
        /// <param name="sourceHeight">The source video height in pixels.</param>
        /// <param name="reduceFilesize">A value indicating whether to reduce filesize by increasing the
        /// trellis quantization setting. Encoding time is increased by approximately 250%.</param>
        /// <param name="startFrame">The frame number at which to begin encoding.</param>
        /// <param name="frameCount">The number of frames to encode.</param>
        /// <param name="isAnimation">A value indicating whether the video is animation such as anime
        /// or cartoons.</param>
        /// <returns>A string containing the command line arguments.</returns>
        public static string GenerateArguments(string source, string output, EncoderQualityOption quality,
            DecoderLevel level, EncoderEfficiency efficiency, InputTuning tuning, OutputProfile profile,
            Int32 sourceHeight, bool reduceFilesize, bool isAnimation, Int32 startFrame, Int32 frameCount)
        {
            StringBuilder result = new StringBuilder();

            result.Append(" --crf " + ComputeConstantRateFactor(quality, sourceHeight).ToString());

            if (tuning != InputTuning.None)
            {
                if (isAnimation)
                {
                    tuning = InputTuning.Animation;
                }

                result.Append(" --tune " + ConvertInputTuningToName(tuning));
            }
            if (profile != OutputProfile.None)
            {
                result.Append(" --profile " + ConvertOutputProfileToName(profile));
            }
            if (level != DecoderLevel.Auto)
            {
                result.Append(" --level " + ConvertDecoderLevelToName(level));
            }
            if (efficiency != EncoderEfficiency.Medium)
            {
                result.Append(" --preset " + ConvertEncoderEfficiencyToName(efficiency));
            }

            Int32 subme = ComputeSubme(quality);
            if (subme != 7)
            {
                result.Append(" --subme " + subme.ToString());
            }
               
            if (subme == 10 || reduceFilesize)
            {
                result.Append(" --trellis 2");
            }

            if (startFrame > 0)
            {
                result.Append(" --seek " + startFrame.ToString());
            }
            if (frameCount > 0)
            {
                result.Append(" --frames " + frameCount.ToString());
            }

            result.Append(" --output \"" + output + "\" \"" + source + "\"");

            return result.ToString();
        }

        /// <summary>
        /// Parses the x264 encoder arguments and returns the source and output filenames.
        /// </summary>
        /// <param name="arguments">A string created by GernerateArguments method.</param>
        /// <param name="source">The source video filename.</param>
        /// <param name="output">The ouput video filename</param>
        public static void ParseFilenames(string arguments, ref string source, ref string output)
        {
            source = string.Empty;
            output = string.Empty;

            //
            // Filenames enclosed in quotes.
            // [...] --output "{ouput filename}" "{source filename}"
            //

            Int32 startIndex = arguments.IndexOf(" --output \"") + 10;
            Int32 endIndex = -1;

            // Find output filename.
            if (startIndex != -1)
            {
                endIndex = arguments.IndexOf('"', startIndex + 1);
                if (endIndex != -1)
                {
                    output = arguments.Substring(startIndex + 1, endIndex - startIndex - 1);
                }
            }

            // Find source filename.
            if (!string.IsNullOrEmpty(output))
            {
                startIndex = arguments.IndexOf('"', endIndex + 1);
                if (startIndex != -1)
                {
                    endIndex = arguments.IndexOf('"', startIndex + 1);
                    if (endIndex != -1)
                    {
                        source = arguments.Substring(startIndex + 1, endIndex - startIndex - 1);
                    }
                }
            }
        }

        #endregion
    }
}
