using csmic;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace butterflow_ui
{
    /// <summary> A butterflow wrapper. Provides interaction with the butterflow executable. </summary>
    public class ButterflowWrapper : PropertyChangedAlerter
    {
        #region Members

        /// <summary> The RegEx string for matching probed resolution. </summary>
        private const string REGEX_RESOLUTION = @"Resolution\s*:\s(?<Width>\d+)x(?<Height>\d+)";
        /// <summary> The RegEx string for matching the probed playback rate. </summary>
        private const string REGEX_RATE = @"Rate\s*:\s(?<Rate>\d+\.\d+) fps";
        /// <summary> The RegEx string for detecting progress made when rendering a video. </summary>
        private const string REGEX_PROGRESS = @"To write\:\s*\w*\s*\w*\,*\w*\s*(?<Progress>\d+\.*\d*)%";
        /// <summary> An alternative RegEx string for detecting progress made when rendering a video. </summary>
        private const string REGEX_PROGRESS_ALT = @"\<Rendering progress\:\s*(?<Progress>\d+\.*\d*)%\>";
        /// <summary> The RegEx string for determining available processing devices in butterflow.. </summary>
        private const string REGEX_DEVICE = @"\*\s*Device\s*(?<DeviceID>\d+)\s*\:\s*(?<DeviceName>(\w*\s*)*)";

        /// <summary> Full pathname of the butterflow executable file. </summary>
        private Lazy<string> executablePath = new Lazy<string>(() => Path.Combine(Directory.GetCurrentDirectory(), "ThirdPartyCompiled", "butterflow.exe"));
        /// <summary> Queue of butterflow commands to run. </summary>
        private Queue<string> runQueue = new Queue<string>();
        /// <summary> The console output from butterflow. </summary>
        private string consoleOutput = string.Empty;
        /// <summary> True if butterflow is running, false if not. </summary>
        private bool isRunning;
        /// <summary> The progress percentage as reported by butterflow. </summary>
        private double progressPercentage;
        /// <summary> The running butterflow process. </summary>
        private Process runningProcess;
        /// <summary> An input interpreter used for converting string values to numeric values. </summary>
        private InputInterpreter interpreter = new InputInterpreter();
        /// <summary> Event queue for all listeners interested in ParsedConsoleOutputRecieved events. </summary>
        public event EventHandler<ButterflowOutputArgs> ParsedConsoleOutputRecieved;
        /// <summary> Event queue for all listeners interested in ButterflowExited events. </summary>
        public event EventHandler<ButterflowExitArgs> ButterflowExited;

        #endregion

        #region Properties

        /// <summary> Gets the console output from butterflow. </summary>
        /// <value> The console output from butterflow. </value>
        public string ConsoleOutput
        {
            get
            {
                return this.consoleOutput;
            }
            private set
            {
                this.consoleOutput = value;
                OnPropertyChanged();
            }
        }

        /// <summary> Gets a value indicating whether butterflow is currently running. </summary>
        /// <value> True if butterflow is running, false if not. </value>
        public bool IsRunning
        {
            get
            {
                return this.isRunning;
            }
            private set
            {
                this.isRunning = value;
                OnPropertyChanged();
            }
        }

        /// <summary> Gets the progress percentage as reported by butterflow. </summary>
        /// <value> The progress percentage as reported by butterflow. </value>
        public double ProgressPercentage
        {
            get
            {
                return this.progressPercentage;
            }
            private set
            {
                this.progressPercentage = value;
                OnPropertyChanged();
            }
        }

        /// <summary> Gets or sets the list of devices available for butterflow processing. </summary>
        /// <value> The devices available for butterflow processing. </value>
        public Dictionary<int, string> Devices { get; private set; } = new Dictionary<int, string>() { { 0, Localization.Localization.ForceCPU } };

        #endregion

        #region Methods

        /// <summary> Runs butterflow with the given <paramref name="optionsConfiguration"/> by adding it to the queue. </summary>
        /// <param name="optionsConfiguration"> The options configuration. </param>
        public void Run(OptionsConfiguration optionsConfiguration)
        {
            for (int i = 0; i < optionsConfiguration.VideoInput.Count(); i++)
            {
                var arguments = optionsConfiguration.ToButterflowArguments(i);
                this.runQueue.Enqueue(arguments);
            }

            ProcessQueue();
        }

        /// <summary> Process the queue of butterflow arguments. </summary>
        public void ProcessQueue()
        {
            if (!this.IsRunning && this.runQueue.Any())
            {
                var arguments = this.runQueue.Dequeue();

                var process = new Process();
                process.StartInfo = new ProcessStartInfo(executablePath.Value, arguments);

                process.StartInfo.CreateNoWindow = true;
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;
                process.EnableRaisingEvents = true;
                process.OutputDataReceived += Process_OutputDataReceived;
                process.ErrorDataReceived += Process_OutputDataReceived;
                process.Exited += Process_Exited;

                process.Start();
                process.BeginOutputReadLine();
                process.BeginErrorReadLine();

                this.IsRunning = true;
                this.runningProcess = process;
            }
        }

        /// <summary> Kills the running instance of butterflow, cancelling its current operation. </summary>
        public void Cancel()
        {
            if (this.IsRunning && this.runningProcess != null)
            {
                this.runningProcess.Kill();
            }

            this.runQueue.Clear();
        }

        /// <summary> Probes a video file. </summary>
        /// <param name="videoFile"> The video file to be probed. </param>
        public void Probe(string videoFile)
        {
            string arguments = string.Format("-prb \"{0}\"", videoFile);
            Run(arguments);
        }

        /// <summary> Gets the devices available for butterflow processing. </summary>
        public void GetDevices()
        {
            string arguments = "--show-devices";
            Run(arguments);
        }

        /// <summary> Runs butterflow with the given <paramref name="arguments"/> by adding it to the queue. </summary>
        /// <param name="arguments">     Options for controlling the operation. </param>
        private void Run(string arguments)
        {
            this.runQueue.Enqueue(arguments);

            ProcessQueue();
        }

        /// <summary> Event handler. Called by Process for exited events. </summary>
        /// <param name="sender"> Source of the event. </param>
        /// <param name="e">      Event information. </param>
        private void Process_Exited(object sender, EventArgs e)
        {
            this.IsRunning = false;
            this.runningProcess = null;

            OnButterflowExited();

            ProcessQueue();
        }

        /// <summary>
        /// Parses console output and attempts to find known values. If a known value is found, the
        /// <seealso cref="ParsedConsoleOutputRecieved"/> event is triggered.
        /// </summary>
        /// <param name="consoleOutput"> The console output from butterflow. </param>
        private void ParseConsoleOutput(string consoleOutput)
        {
            if (string.IsNullOrWhiteSpace(consoleOutput))
            {
                //Ignore null content and just escape.
                return;
            }

            // Test for resolution
            var regex = new Regex(REGEX_RESOLUTION);
            foreach (Match match in regex.Matches(consoleOutput))
            {
                var width = match.Groups["Width"].Value;
                var height = match.Groups["Height"].Value;

                OnParsedConsoleOutputRecieved(ButterflowOutputType.Width, width, consoleOutput);
                OnParsedConsoleOutputRecieved(ButterflowOutputType.Height, height, consoleOutput);
            }

            // Test for playback rate
            regex = new Regex(REGEX_RATE);
            foreach (Match match in regex.Matches(consoleOutput))
            {
                var rate = match.Groups["Rate"].Value;

                OnParsedConsoleOutputRecieved(ButterflowOutputType.Rate, rate, consoleOutput);
            }

            // Test for progress being made when rendering a video
            regex = new Regex(REGEX_PROGRESS);
            foreach (Match match in regex.Matches(consoleOutput))
            {
                var progress = match.Groups["Progress"].Value;

                this.interpreter.Interpret(progress);
                this.ProgressPercentage = this.interpreter.Double;

                OnParsedConsoleOutputRecieved(ButterflowOutputType.Progress, progress, consoleOutput);
            }

            // An alternate test for progression on a rendering video
            regex = new Regex(REGEX_PROGRESS_ALT);
            foreach (Match match in regex.Matches(consoleOutput))
            {
                var progress = match.Groups["Progress"].Value;

                this.interpreter.Interpret(progress);
                this.ProgressPercentage = this.interpreter.Double;

                OnParsedConsoleOutputRecieved(ButterflowOutputType.Progress, progress, consoleOutput);
            }

            // Test for device declaration
            regex = new Regex(REGEX_DEVICE);
            foreach (Match match in regex.Matches(consoleOutput))
            {
                var deviceID = match.Groups["DeviceID"].Value;
                var deviceName = match.Groups["DeviceName"].Value.Trim();

                //Add 1 to the index count. This accounts for the 0 index of the "Force CPU" option in the options window.
                this.interpreter.Interpret(string.Concat(deviceID,"+1"));

                if (!this.Devices.ContainsKey(this.interpreter.Int))
                {
                    this.Devices.Add(this.interpreter.Int, deviceName);
                    OnPropertyChanged("Devices");
                }

                OnParsedConsoleOutputRecieved(ButterflowOutputType.Device, deviceName, consoleOutput);
            }
        }

        /// <summary> Executes the parsed console output recieved action. </summary>
        /// <param name="outputType">    Type of the output. </param>
        /// <param name="value">         The value. </param>
        /// <param name="consoleOutput"> The console output from butterflow. </param>
        private void OnParsedConsoleOutputRecieved(ButterflowOutputType outputType, string value, string consoleOutput)
        {
            this.ParsedConsoleOutputRecieved?.Invoke(this, new ButterflowOutputArgs(outputType, value, consoleOutput));
        }

        /// <summary> Executes the butterflow exited action. </summary>
        private void OnButterflowExited()
        {
            this.ButterflowExited?.Invoke(this, new ButterflowExitArgs());
        }

        /// <summary> Event handler. Called by Process for output data received events. </summary>
        /// <param name="sender"> Source of the event. </param>
        /// <param name="e">      Data received event information. </param>
        private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            this.ConsoleOutput += string.Format("{0}{1}", e.Data, Environment.NewLine);
            ParseConsoleOutput(e.Data);
        }

        #endregion

        #region Subclasses

        /// <summary> Arguments for butterflow output events where a known value of type <seealso cref="ButterflowOutputType"/> can be parsed. </summary>
        public class ButterflowOutputArgs : ButterflowConsoleOutputArgs
        {

            #region Properties

            /// <summary> Gets or sets the type of the output detected from butterflow. </summary>
            /// <value> The type of the output detected from butterflow. </value>
            public ButterflowOutputType OutputType { get; private set; }

            /// <summary> Gets or sets the value detected from butterflow. </summary>
            /// <value> The value detected from butterflow. </value>
            public string Value { get; private set; }

            #endregion

            /// <summary> Constructor. </summary>
            /// <param name="outputType">    The type of the output detected from butterflow. </param>
            /// <param name="value">         The value detected from butterflow. </param>
            /// <param name="consoleOutput"> The console output. </param>
            public ButterflowOutputArgs(ButterflowOutputType outputType, string value, string consoleOutput)
                : base(consoleOutput)
            {
                this.OutputType = outputType;
                this.Value = value;
            }
        }

        /// <summary> Arguments for butterflow console output events. </summary>
        public class ButterflowConsoleOutputArgs : EventArgs
        {
            #region Properties

            /// <summary> Gets the console output. </summary>
            /// <value> The console output. </value>
            public string ConsoleOutput { get; private set; }

            #endregion

            #region Constructors

            /// <summary> Constructor. </summary>
            /// <param name="consoleOutput"> The console output. </param>
            public ButterflowConsoleOutputArgs(string consoleOutput)
            {
                this.ConsoleOutput = consoleOutput;
            }

            #endregion
        }

        /// <summary> Arguments for butterflow exiting. </summary>
        public class ButterflowExitArgs : EventArgs
        {
            #region Constructors

            public ButterflowExitArgs()
            {
                //
            }

            #endregion
        }

        /// <summary> Values that represent butterflow output types. </summary>
        public enum ButterflowOutputType
        {
            /// <summary> Video Width. </summary>
            Width,
            /// <summary> Video Height. </summary>
            Height,
            /// <summary> Video playback rate. </summary>
            Rate,
            /// <summary> Video processing progress. </summary>
            Progress,
            /// <summary> An available processing device. </summary>
            Device
        }

        #endregion
    }
}
