// --------------------------------------------------------------------------------------------------------------------
// <copyright file="LibEncode.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>
//   LibHB Implementation of IEncode
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace HandBrakeWPF.Services.Encode
{
    using System;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;

    using HandBrake.App.Core.Exceptions;
    using HandBrake.App.Core.Utilities;
    using HandBrake.Interop.Interop.Interfaces;
    using HandBrake.Interop.Interop.Interfaces.EventArgs;
    using HandBrake.Interop.Interop.Json.Encode;
    using HandBrake.Interop.Interop.Json.State;

    using HandBrakeWPF.Properties;
    using HandBrakeWPF.Services.Encode.Factories;
    using HandBrakeWPF.Services.Encode.Interfaces;
    using HandBrakeWPF.Services.Interfaces;
    using HandBrakeWPF.Services.Logging.Interfaces;

    using EncodeTask = Model.EncodeTask;
    using HandBrakeInstanceManager = Instance.HandBrakeInstanceManager;
    using IEncode = Interfaces.IEncode;
    using LogService = Logging.LogService;

    public class LibEncode : IEncode
    {
        private readonly IUserSettingService userSettingService;
        private readonly ILogInstanceManager logInstanceManager;
        private readonly IPortService portService;
        private readonly object portLock = new object();
        private readonly EncodeTaskFactory encodeTaskFactory;
        private ILog encodeLogService;

        private IEncodeInstance instance;
        private DateTime startTime;
        private EncodeTask currentTask;
        private bool isPreviewInstance;
        private bool isLoggingInitialised;
        private bool isEncodeComplete;
        private int encodeCounter;
        
        public LibEncode(IUserSettingService userSettingService, ILogInstanceManager logInstanceManager, int encodeCounter, IPortService portService)
        {
            this.userSettingService = userSettingService;
            this.logInstanceManager = logInstanceManager;
            this.encodeCounter = encodeCounter;
            this.portService = portService;
            this.encodeTaskFactory = new EncodeTaskFactory(this.userSettingService);
        }

        public event EventHandler EncodeStarted;

        public event EncodeCompletedStatus EncodeCompleted;

        public event EncodeProgressStatus EncodeStatusChanged;

        public bool IsPaused { get; private set; }

        public bool IsEncoding { get; protected set; }

        public void Start(EncodeTask task, string basePresetName)
        {
            try
            {
                // Sanity Checking and Setup
                if (this.IsEncoding)
                {
                    throw new GeneralApplicationException(Resources.Queue_AlreadyEncoding, Resources.Queue_AlreadyEncodingSolution, null);
                }

                // Setup
                this.startTime = DateTime.Now;
                this.currentTask = task;
                this.isPreviewInstance = task.IsPreviewEncode;

                if (task.IsPreviewEncode)
                {
                    this.encodeLogService = this.logInstanceManager.ApplicationLogInstance;
                    this.encodeLogService.Reset();
                }
                else if (this.userSettingService.GetUserSetting<bool>(UserSettingConstants.ProcessIsolationEnabled) && Portable.IsProcessIsolationEnabled())
                {
                    this.InitRemoteLogging(task.Destination);
                }
                else
                {
                    this.encodeLogService = this.logInstanceManager.ApplicationLogInstance;
                    this.encodeLogService.Reset();
                }
                
                if (this.instance != null)
                {
                    // Cleanup
                    try
                    {
                        this.instance.EncodeCompleted -= this.InstanceEncodeCompleted;
                        this.instance.EncodeProgress -= this.InstanceEncodeProgress;
                        this.instance.Dispose();
                        this.instance = null;
                    }
                    catch (Exception exc)
                    {
                        this.ServiceLogMessage("Failed to cleanup previous instance: " + exc);
                    }
                }

                this.ServiceLogMessage("Starting Encode ...");
                if (!string.IsNullOrEmpty(basePresetName))
                {
                    this.TimedLogMessage(string.Format("base preset: {0}", basePresetName));
                }

                int verbosity = this.userSettingService.GetUserSetting<int>(UserSettingConstants.Verbosity);

                // Prevent port stealing if multiple jobs start at the same time.
                lock (this.portLock) 
                {
                    this.instance = HandBrakeInstanceManager.GetEncodeInstance(verbosity, this.encodeLogService, this.userSettingService, this.portService);

                    this.instance.EncodeCompleted += this.InstanceEncodeCompleted;
                    this.instance.EncodeProgress += this.InstanceEncodeProgress;

                    this.IsEncoding = true;

                    // Verify the Destination Path Exists, and if not, create it.
                    this.VerifyEncodeDestinationPath(task);

                    // Get an EncodeJob object for the Interop Library
                    JsonEncodeObject work = this.encodeTaskFactory.Create(task);

                    this.instance.StartEncode(work);
                }

                // Fire the Encode Started Event
                this.InvokeEncodeStarted(System.EventArgs.Empty);
            }
            catch (Exception exc)
            {
                this.IsEncoding = false;

                this.ServiceLogMessage("Failed to start encoding ..." + Environment.NewLine + exc);
                this.InvokeEncodeCompleted(new EventArgs.EncodeCompletedEventArgs(false, exc, "Unable to start encoding", this.currentTask.Source, this.currentTask.Destination, null, 0, 3));
            }
        }

        public void Pause()
        {
            if (this.instance != null)
            {
                this.instance.PauseEncode();
                this.ServiceLogMessage("Encode Paused");
                this.IsPaused = true;
            }
        }

        public void Resume()
        {
            if (this.instance != null)
            {
                this.instance.ResumeEncode();
                this.ServiceLogMessage("Encode Resumed");
                this.IsPaused = false;
            }
        }

        public void Stop()
        {
            try
            {
                this.IsEncoding = false;
                if (this.instance != null)
                {
                    this.instance.StopEncode();
                    this.ServiceLogMessage("Encode Stopped");
                }
            }
            catch (Exception exc)
            {
                Debug.WriteLine(exc);
            }
        }

        public EncodeTask GetActiveJob()
        {
            if (this.currentTask != null)
            {
                EncodeTask task = new EncodeTask(this.currentTask); // Shallow copy our current instance.
                return task;
            }

            return null;
        }

        protected void ServiceLogMessage(string message)
        {
            this.encodeLogService.LogMessage(string.Format("{1} # {0}{1}", message, Environment.NewLine));
        }

        protected void TimedLogMessage(string message)
        {
            this.encodeLogService.LogMessage(string.Format("[{0}] {1}", DateTime.Now.ToString("HH:mm:ss"), message));
        }

        private void InvokeEncodeStatusChanged(EventArgs.EncodeProgressEventArgs e)
        {
            EncodeProgressStatus handler = this.EncodeStatusChanged;
            handler?.Invoke(this, e);
        }

        private void InvokeEncodeCompleted(EventArgs.EncodeCompletedEventArgs e)
        {
            EncodeCompletedStatus handler = this.EncodeCompleted;
            handler?.Invoke(this, e);
        }

        private void InvokeEncodeStarted(System.EventArgs e)
        {
            EventHandler handler = this.EncodeStarted;
            handler?.Invoke(this, e);
        }

        private void InstanceEncodeProgress(object sender, EncodeProgressEventArgs e)
        {
            EventArgs.EncodeProgressEventArgs args = new EventArgs.EncodeProgressEventArgs
            {
                AverageFrameRate = e.AverageFrameRate, 
                CurrentFrameRate = e.CurrentFrameRate, 
                EstimatedTimeLeft = e.EstimatedTimeLeft, 
                PercentComplete = e.FractionComplete * 100, 
                Task = e.Pass, 
                TaskCount = e.PassCount,
                ElapsedTime = DateTime.Now - this.startTime, 
                PassId = e.PassId,
                IsMuxing = e.StateCode == TaskState.Muxing.Code,
                IsSearching = e.StateCode == TaskState.Searching.Code
            };

            this.InvokeEncodeStatusChanged(args);
        }
        
        private void InstanceEncodeCompleted(object sender, EncodeCompletedEventArgs e)
        {
            this.IsEncoding = false;

            if (this.isEncodeComplete)
            {
                return; // Prevent phantom events bubbling up the stack. 
            }

            this.isEncodeComplete = true;

            string completeMessage = "Job Completed!";
            switch (e.Error)
            {
                case 0:
                    break;
                case 1:
                    completeMessage = "Job Cancelled!";
                    break;
                case 2:
                    completeMessage = string.Format("Job Failed. Check log and input settings ({0})", e.Error);
                    break;
                case 3:
                    completeMessage = string.Format("Job Failed to Initialise. Check log and input settings ({0})", e.Error);
                    break;
                default:
                    completeMessage = string.Format("Job Failed ({0})", e.Error);
                    break;
            }
            
            this.ServiceLogMessage(completeMessage);

            // Handling Log Data 
            string hbLog = this.ProcessLogs(this.currentTask.Destination);
            long filesize = this.GetFilesize(this.currentTask.Destination);

            // Raise the Encode Completed Event.
            this.InvokeEncodeCompleted(
                e.Error != 0
                    ? new EventArgs.EncodeCompletedEventArgs(false, null, e.Error.ToString(), this.currentTask.Source, this.currentTask.Destination, hbLog, filesize, e.Error)
                    : new EventArgs.EncodeCompletedEventArgs(true, null, string.Empty, this.currentTask.Source, this.currentTask.Destination, hbLog, filesize, e.Error));

            this.logInstanceManager.Deregister(Path.GetFileName(hbLog));
        }

        private long GetFilesize(string destination)
        {
            try
            {
                if (!string.IsNullOrEmpty(destination) && File.Exists(destination))
                {
                    return new FileInfo(destination).Length;
                }

                return 0;
            }
            catch (Exception e)
            {
                this.ServiceLogMessage("Unable to get final filesize ..." + Environment.NewLine + e);
                Debug.WriteLine(e);
            }

            return 0;
        }

        private void InitRemoteLogging(string destination)
        {
            if (!this.isLoggingInitialised)
            {
                string logType = this.isPreviewInstance ? "preview" : "encode";
                string destinationFile = Path.GetFileNameWithoutExtension(destination);
                string logFileName = string.Format("{0}_{1}_{2}.txt", destinationFile, logType, DateTime.Now.ToString(CultureInfo.InvariantCulture).Replace("/", ".").Replace(":", "-"));
                string fullLogPath = Path.Combine(DirectoryUtilities.GetLogDirectory(), logFileName);

                this.encodeLogService = new LogService();
                this.encodeLogService.ConfigureLogging(logFileName, fullLogPath);
                this.encodeLogService.SetId(this.encodeCounter);
                this.logInstanceManager.Register(logFileName, this.encodeLogService, false);
                this.isLoggingInitialised = true;
            }
        }

        private string ProcessLogs(string destination)
        {
            try
            {
                string logDir = DirectoryUtilities.GetLogDirectory();
                string filename = this.encodeLogService.FileName;
                string logContent = this.encodeLogService.GetFullLog();

                // Make sure the log directory exists.
                if (!Directory.Exists(logDir))
                {
                    Directory.CreateDirectory(logDir);
                }

                // Copy the Log to HandBrakes log folder in the users application data folder.
                // Only needed for process isolation mode. Worker will handle it's own logging.
                bool processIsolationEnabled = this.userSettingService.GetUserSetting<bool>(UserSettingConstants.ProcessIsolationEnabled) && Portable.IsProcessIsolationEnabled();

                if (!processIsolationEnabled)
                {
                    string logType = this.isPreviewInstance ? "preview" : "encode";
                    string destinationFile = Path.GetFileNameWithoutExtension(destination);
                    string logFileName = string.Format("{0}_{1}_{2}.txt", destinationFile, logType, DateTime.Now.ToString(CultureInfo.InvariantCulture).Replace("/", ".").Replace(":", "-"));
                    this.WriteFile(logContent, Path.Combine(logDir, logFileName));
                    filename = logFileName;
                }

                // Save a copy of the log file in the same location as the encode.
                if (this.userSettingService.GetUserSetting<bool>(UserSettingConstants.SaveLogWithVideo))
                {
                    this.WriteFile(logContent, Path.Combine(Path.GetDirectoryName(destination), filename));
                }

                // Save a copy of the log file to a user specified location
                if (Directory.Exists(this.userSettingService.GetUserSetting<string>(UserSettingConstants.SaveLogCopyDirectory)) && this.userSettingService.GetUserSetting<bool>(UserSettingConstants.SaveLogToCopyDirectory))
                {
                    this.WriteFile(logContent, Path.Combine(this.userSettingService.GetUserSetting<string>(UserSettingConstants.SaveLogCopyDirectory), filename));
                }

                return Path.Combine(logDir, filename);
            }
            catch (Exception exc)
            {
                Debug.WriteLine(exc); // This exception doesn't warrant user interaction, but it should be logged
            }

            return null;
        }

        private void VerifyEncodeDestinationPath(EncodeTask task)
        {
            // Make sure the path exists, attempt to create it if it doesn't
            try
            {
                string path = Directory.GetParent(task.Destination).ToString();
                if (!Directory.Exists(path))
                {
                    Directory.CreateDirectory(path);
                }
            }
            catch (Exception exc)
            {
                throw new GeneralApplicationException(
                    "Unable to create directory for the encoded output.", "Please verify that you have a valid path.", exc);
            }
        }

        private void WriteFile(string content, string fileName)
        {
            try
            {
                using (StreamWriter fileWriter = new StreamWriter(fileName) { AutoFlush = true })
                {
                    fileWriter.Write(content);
                }
            }
            catch (Exception exc)
            {
                Debug.WriteLine(exc);
            }
        }
    }
}
