// --------------------------------------------------------------------------------------------------------------------
// <copyright file="HandBrakeUtils.cs" company="HandBrake Project (http://handbrake.fr)">
//   This file is part of the HandBrake source code - It may be used under the terms of the GNU General Public License.
// </copyright>
// <summary>
//   Defines the HandBrakeUtils type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace HandBrake.Interop.Interop
{
    using System;
    using System.Collections.Generic;
    using System.Linq.Expressions;
    using System.Runtime.CompilerServices;
    using System.Runtime.ExceptionServices;
    using System.Runtime.InteropServices;

    using HandBrake.Interop.Interop.EventArgs;
    using HandBrake.Interop.Interop.HbLib;
    using HandBrake.Interop.Interop.HbLib.Wrappers.Interfaces;
    using HandBrake.Interop.Interop.Json.Anamorphic;
    using HandBrake.Interop.Interop.Json.Shared;
    using HandBrake.Interop.Interop.Providers;
    using HandBrake.Interop.Interop.Providers.Interfaces;

    using Newtonsoft.Json;

    /// <summary>
    /// HandBrake Interop Utilities
    /// </summary>
    public static class HandBrakeUtils
    {
        /// <summary>
        /// The callback for log messages from HandBrake.
        /// </summary>
        private static LoggingCallback loggingCallback;

        /// <summary>
        /// The callback for error messages from HandBrake.
        /// </summary>
        private static LoggingCallback errorCallback;

        /// <summary>
        /// True if the global initialize function has been called.
        /// </summary>
        private static bool globalInitialized;

        private static bool initSuccess = false;
        private static bool initNoHardware = false;

        /// <summary>
        /// Fires when HandBrake has logged a message.
        /// </summary>
        public static event EventHandler<MessageLoggedEventArgs> MessageLogged;

        /// <summary>
        /// Fires when HandBrake has logged an error.
        /// </summary>
        public static event EventHandler<MessageLoggedEventArgs> ErrorLogged;

        private static IHbFunctions hbFunctions;

        static HandBrakeUtils()
        {
            IHbFunctionsProvider hbFunctionsProvider = new HbFunctionsProvider();
            hbFunctions = hbFunctionsProvider.GetHbFunctionsWrapper();
        }

        /// <summary>
        /// Ensures the HB global initialize method has been called.
        /// </summary>
        public static void EnsureGlobalInit(bool initNoHardwareMode)
        {
            if (!globalInitialized)
            {
                try
                {
                    if (initNoHardwareMode)
                    {
                        initNoHardware = true;
                        if (hbFunctions.hb_global_init_no_hardware() == -1)
                        {
                            throw new InvalidOperationException("HB global init failed.");
                        }

                        initSuccess = true;
                    }
                    else
                    {
                        initSuccess = TryInit();
                    }
                }
                catch (Exception e)
                {
                    initSuccess = false;
                }

                // Try without Hardware support. Bad drivers can sometimes cause issues.
                if (!initSuccess)
                {
                    if (hbFunctions.hb_global_init_no_hardware() == -1)
                    {
                        throw new InvalidOperationException("HB global init failed.");
                    }
                }

                globalInitialized = true;
            }
        }

        /// <summary>
        /// Enables or disables LibDVDNav. If disabled libdvdread will be used instead.
        /// </summary>
        /// <param name="enableDvdNav">
        /// True to enable LibDVDNav.
        /// </param>
        public static void SetDvdNav(bool enableDvdNav)
        {
            hbFunctions.hb_dvd_set_dvdnav(enableDvdNav ? 1 : 0);
        }

        /// <summary>
        /// Call before app shutdown. Performs global cleanup.
        /// </summary>
        public static void DisposeGlobal()
        {
            hbFunctions.hb_global_close();
        }

        /// <summary>
        /// Register the logger.
        /// </summary>
        public static void RegisterLogger()
        {
            // Register the logger if we have not already
            if (loggingCallback == null)
            {
                // Keep the callback as a member to prevent it from being garbage collected.
                loggingCallback = LoggingHandler;
                errorCallback = ErrorHandler;
                hbFunctions.hb_register_logger(loggingCallback);
                hbFunctions.hb_register_error_handler(errorCallback);
            }
        }

        /// <summary>
        /// Handles log messages from HandBrake.
        /// </summary>
        /// <param name="message">
        /// The log message (including newline).
        /// </param>
        public static void LoggingHandler(string message)
        {
            message = message.TrimEnd();
            if (!string.IsNullOrEmpty(message))
            {
                SendMessageEvent(message);
            }
        }

        /// <summary>
        /// Handles errors from HandBrake.
        /// </summary>
        /// <param name="message">
        /// The error message.
        /// </param>
        public static void ErrorHandler(string message)
        {
            if (!string.IsNullOrEmpty(message))
            {
                // These errors happen in normal operations. Log them as messages.
                if (message == "dvd: ifoOpen failed" || message.Contains("avformat_seek_file failed") || message.Contains("nav_get_title_list"))
                {
                    SendMessageEvent(message);
                    return;
                }

                SendErrorEvent(message);
            }
        }

        /// <summary>
        /// Gets the standard x264 option name given the starting point.
        /// </summary>
        /// <param name="name">
        /// The name.
        /// </param>
        /// <returns>
        /// The standard x264 option name.
        /// </returns>
        public static string SanitizeX264OptName(string name)
        {
            IntPtr namePtr = Marshal.StringToHGlobalAnsi(name);
            string sanitizedName = Marshal.PtrToStringAnsi(hbFunctions.hb_x264_encopt_name(namePtr));
            Marshal.FreeHGlobal(namePtr);
            return sanitizedName;
        }

        /// <summary>
        /// Checks to see if the given H.264 level is valid given the inputs.
        /// </summary>
        /// <param name="level">
        /// The level to check.
        /// </param>
        /// <param name="width">
        /// The output picture width.
        /// </param>
        /// <param name="height">
        /// The output picture height.
        /// </param>
        /// <param name="fpsNumerator">
        /// The rate numerator.
        /// </param>
        /// <param name="fpsDenominator">
        /// The rate denominator.
        /// </param>
        /// <param name="interlaced">
        /// True if x264 interlaced output is enabled.
        /// </param>
        /// <param name="fakeInterlaced">
        /// True if x264 fake interlacing is enabled.
        /// </param>
        /// <returns>
        /// True if the level is valid.
        /// </returns>
        public static bool IsH264LevelValid(string level, int width, int height, int fpsNumerator, int fpsDenominator, bool interlaced, bool fakeInterlaced)
        {
            return hbFunctions.hb_check_h264_level(
                level,
                width,
                height,
                fpsNumerator,
                fpsDenominator,
                interlaced ? 1 : 0,
                fakeInterlaced ? 1 : 0) == 0;
        }

        /// <summary>
        /// Creates an X264 options string from the given settings.
        /// </summary>
        /// <param name="preset">
        /// The x264 preset.
        /// </param>
        /// <param name="tunes">
        /// The x264 tunes being used.
        /// </param>
        /// <param name="extraOptions">
        /// The extra options string.
        /// </param>
        /// <param name="profile">
        /// The H.264 profile.
        /// </param>
        /// <param name="level">
        /// The H.264 level.
        /// </param>
        /// <param name="width">
        /// The width of the final picture.
        /// </param>
        /// <param name="height">
        /// The height of the final picture.
        /// </param>
        /// <returns>
        /// The full x264 options string from the given inputs.
        /// </returns>
        public static string CreateX264OptionsString(
            string preset,
            IList<string> tunes,
            string extraOptions,
            string profile,
            string level,
            int width,
            int height)
        {
            if (width <= 0)
            {
                throw new ArgumentException("width must be positive.");
            }

            if (height <= 0)
            {
                throw new ArgumentException("height must be positive.");
            }

            IntPtr ptr = hbFunctions.hb_x264_param_unparse(
                8,
                preset,
                string.Join(",", tunes),
                extraOptions,
                profile,
                level,
                width,
                height); // TODO add bit-depth support.

            string x264Settings = Marshal.PtrToStringAnsi(ptr);

            return x264Settings;
        }

        /// <summary>
        /// Gets the final size and PAR of the video, given anamorphic inputs.
        /// </summary>
        /// <param name="anamorphicGeometry">Anamorphic inputs.</param>
        /// <returns>The final size and PAR of the video.</returns>
        public static Geometry GetAnamorphicSize(AnamorphicGeometry anamorphicGeometry)
        {
            string encode = JsonConvert.SerializeObject(anamorphicGeometry, Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
            IntPtr json = hbFunctions.hb_set_anamorphic_size_json(Marshal.StringToHGlobalAnsi(encode));
            string result = Marshal.PtrToStringAnsi(json);
            return JsonConvert.DeserializeObject<Geometry>(result);
        }

        public static void Reduce(long den, long num, out long x, out long y)
        {
            // find the greatest common divisor of num & den by Euclid's algorithm
            long n = num, d = den;
            while (d > 0)
            {
                long t = d;
                d = n % d;
                n = t;
            }

            // at this point n is the gcd. if it's non-zero remove it from num
            // and den. Otherwise just return the original values.
            if (n > 0)
            {
                num /= n;
                den /= n;
            }

            x = num;
            y = den;
        }

        /// <summary>
        /// Sends the message logged event to any registered listeners.
        /// </summary>
        /// <param name="message">
        /// The message to send.
        /// </param>
        public static void SendMessageEvent(string message)
        {
            MessageLogged?.Invoke(null, new MessageLoggedEventArgs(message));
        }

        /// <summary>
        /// Sends the error logged event to any registered listeners.
        /// </summary>
        /// <param name="message">
        /// The message to send
        /// </param>
        public static void SendErrorEvent(string message)
        {
            ErrorLogged?.Invoke(null, new MessageLoggedEventArgs(message));
        }

        public static bool IsInitialised()
        {
            return initSuccess;
        }

        public static bool IsInitNoHardware()
        {
            return initNoHardware;
        }

        [HandleProcessCorruptedStateExceptions]
        private static bool TryInit()
        {
            try
            {
                if (hbFunctions.hb_global_init() == -1)
                {
                    throw new InvalidOperationException("HB global init failed.");
                }
            }
            catch (Exception e)
            {
                return false;
            }

            return true;
        }
    }
}
