﻿#if UNITY_STANDALONE || UNITY_EDITOR
using UnityEngine;

namespace Crosstales.Common.Util
{
   #region CTProcess

   /// <summary>Native process class for standalone IL2CPP-builds (mimicking the missing "System.Diagnostics.Process"-class with the most important properties, methods and events).</summary>
   public class CTProcess : System.IDisposable
   {
      #region Variables

      private uint _exitCode = 123456;
      private CTProcessStartInfo _startInfo = new CTProcessStartInfo();

      private static readonly System.Reflection.FieldInfo[] EVENT_FIELDS = typeof(System.Diagnostics.DataReceivedEventArgs).GetFields(
            System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly);

      #endregion


      #region Properties

      /// <summary>Gets the native handle of the associated process.</summary>
      public System.IntPtr Handle { get; private set; }

      /// <summary>Gets the unique identifier for the associated process.</summary>
      public int Id { get; private set; }

      /// <summary>Gets or sets the properties to pass to the Start() method of the Process.</summary>
      public CTProcessStartInfo StartInfo
      {
         get => _startInfo;

         set
         {
            if (value != null)
               _startInfo = value;
         }
      }

      /// <summary>Gets a value indicating whether the associated process has been terminated.</summary>
      public bool HasExited { get; private set; }

      /// <summary>Gets the value that the associated process specified when it terminated.</summary>
      public uint ExitCode => _exitCode;

      /// <summary>Gets the time that the associated process was started.</summary>
      public System.DateTime StartTime { get; private set; }

      /// <summary>Gets the time that the associated process exited.</summary>
      public System.DateTime ExitTime { get; private set; }

      /// <summary>Gets a stream used to read the textual output of the application.</summary>
      public System.IO.StreamReader StandardOutput { get; private set; }

      /// <summary>Gets a stream used to read the error output of the application.</summary>
      public System.IO.StreamReader StandardError { get; private set; }

      /// <summary>Gets a value indicating whether the associated process has been busy.</summary>
      public bool isBusy { get; private set; }

      #endregion


      #region Events

      public event System.EventHandler Exited;

//#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN || ENABLE_IL2CPP
      public event System.Diagnostics.DataReceivedEventHandler OutputDataReceived;

      public event System.Diagnostics.DataReceivedEventHandler ErrorDataReceived;
//#endif

      #region Event-trigger methods

      private void onExited()
      {
         if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG)
            Debug.Log($"onExited: {ExitCode}");

         Exited?.Invoke(this, System.EventArgs.Empty);
      }

      #endregion

      #endregion


      #region Public methods

      public void BeginOutputReadLine()
      {
         //System.Threading.Thread.Sleep(100);
         new System.Threading.Thread(watchStdOut).Start();
      }

      public void BeginErrorReadLine()
      {
         //System.Threading.Thread.Sleep(100);
         new System.Threading.Thread(watchStdErr).Start();
      }

      /// <summary>Starts the process resource that is specified by the parameter containing process start information (for example, the file name of the process to start) and associates the resource with a new Process component..</summary>
      public void Start(CTProcessStartInfo info)
      {
         if (info != null)
            StartInfo = info;

         Start();
      }

      #endregion

#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
      #region Windows

      #region Variables

      private System.IntPtr _threadHandle = System.IntPtr.Zero;

      private const uint INFINITE = 0xffffffff;

      // Creation flags
      private const uint CREATE_NO_WINDOW = 0x08000000;

      #endregion


      #region Public methods

      /// <summary>Starts (or reuses) the process resource that is specified by the StartInfo property of this Process component and associates it with the component.</summary>
      public void Start()
      {
         cleanup();

         isBusy = true;
         HasExited = false;

         if (StartInfo.UseThread)
         {
            new System.Threading.Thread(createProcess).Start();

            System.Threading.Thread.Sleep(200);
         }
         else
         {
            createProcess();
         }
      }

      /// <summary>Immediately stops the associated process.</summary>
      public void Kill()
      {
         if (Handle != System.IntPtr.Zero)
         {
            uint _exitCode = 99999; //killed
            NativeMethods.TerminateProcess(Handle, ref _exitCode);

            Dispose();
         }
      }

      public void WaitForExit(int milliseconds = 0)
      {
         if (milliseconds > 0)
         {
            NativeMethods.WaitForSingleObject(Handle, (uint)milliseconds);
         }
         else
         {
            NativeMethods.WaitForSingleObject(Handle, INFINITE);
         }
      }

      public void Dispose()
      {
         if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG)
            Debug.LogWarning("Dispose called!");

         if (Handle != System.IntPtr.Zero)
            NativeMethods.CloseHandle(Handle);

         if (_threadHandle != System.IntPtr.Zero)
            NativeMethods.CloseHandle(_threadHandle);

         Handle = System.IntPtr.Zero;
         _threadHandle = System.IntPtr.Zero;
         Id = 0;

         isBusy = false;
         HasExited = true;

         StandardOutput?.Dispose();
         StandardError?.Dispose();
      }

      #endregion


      #region Private methods

      private void createProcess()
      {
         StartTime = System.DateTime.Now;

         string app = StartInfo.FileName;
         string args = StartInfo.Arguments;

         if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG)
            Debug.Log($"createProcess: {StartTime}");

         //isBusy = true;
         //HasExited = false;

         NativeMethods.STARTUPINFOEX startupInfo = new NativeMethods.STARTUPINFOEX();

         try
         {
            if ((StartInfo.RedirectStandardOutput || StartInfo.RedirectStandardError || StartInfo.UseCmdExecute) &&
                !StartInfo.FileName.CTContains("cmd"))
            {
               app = Crosstales.Common.Util.BaseConstants.CMD_WINDOWS_PATH;
               args = $"/c call \"{StartInfo.FileName}\" {StartInfo.Arguments}";
            }

            if (StartInfo.RedirectStandardOutput)
            {
               string tempStdFile = FileHelper.TempFile;
               args += $" > \"{tempStdFile}\"";

               if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG)
                  Debug.Log($"tempStdFile: {tempStdFile}");

               StandardOutput = new System.IO.StreamReader(new System.IO.FileStream(tempStdFile, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite), StartInfo.StandardOutputEncoding);
            }
            else
            {
               StandardOutput =
                  new System.IO.StreamReader(new System.IO.MemoryStream(), StartInfo.StandardOutputEncoding);
            }

            if (StartInfo.RedirectStandardError)
            {
               string tempErrFile = FileHelper.TempFile;
               args += $" 2> \"{tempErrFile}\"";

               if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG)
                  Debug.Log($"tempErrFile: {tempErrFile}");

               StandardError = new System.IO.StreamReader(new System.IO.FileStream(tempErrFile, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite), StartInfo.StandardOutputEncoding);
            }
            else
            {
               StandardError = new System.IO.StreamReader(new System.IO.MemoryStream(), StartInfo.StandardOutputEncoding);
            }

            NativeMethods.SECURITY_ATTRIBUTES pSec = new NativeMethods.SECURITY_ATTRIBUTES();
            NativeMethods.SECURITY_ATTRIBUTES tSec = new NativeMethods.SECURITY_ATTRIBUTES();
            pSec.nLength = System.Runtime.InteropServices.Marshal.SizeOf(pSec);
            tSec.nLength = System.Runtime.InteropServices.Marshal.SizeOf(tSec);

            if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG)
               Debug.Log($"application: {app}{System.Environment.NewLine}arguments: {args}");

            bool retValue =
               NativeMethods.CreateProcess(app, $" {args}", ref pSec, ref tSec, true,
                  StartInfo.CreateNoWindow ? CREATE_NO_WINDOW : 0x00000000, System.IntPtr.Zero,
                  StartInfo.WorkingDirectory, ref startupInfo, out NativeMethods.PROCESS_INFORMATION processInfo);

            if (retValue)
            {
               Handle = processInfo.hProcess;
               _threadHandle = processInfo.hThread;
               Id = processInfo.dwProcessId;

               WaitForExit();
            }
            else
            {
               Debug.LogError($"Could not start process: '{StartInfo.FileName}'{System.Environment.NewLine}Arguments: '{StartInfo.Arguments}'{System.Environment.NewLine}Working dir: '{StartInfo.WorkingDirectory}'{System.Environment.NewLine}Last error: {NativeMethods.GetLastError()}");
            }
         }
         catch (System.Exception ex)
         {
            Debug.LogError($"Process threw an error: {ex}");

            Dispose();
         }
         finally
         {
            System.Threading.Thread.Sleep(200); //give the streams the chance to write out all events

            NativeMethods.GetExitCodeProcess(Handle, ref _exitCode);

            ExitTime = System.DateTime.Now;

            if (Handle != System.IntPtr.Zero)
               NativeMethods.CloseHandle(Handle);

            if (_threadHandle != System.IntPtr.Zero)
               NativeMethods.CloseHandle(_threadHandle);

            Handle = System.IntPtr.Zero;
            _threadHandle = System.IntPtr.Zero;
            Id = 0;

            if (!HasExited)
               onExited();

            isBusy = false;
            HasExited = true;
         }
      }

      private void cleanup()
      {
         Kill();
         Dispose();
      }

      #endregion

      #endregion

#else

      #region Unix

      #region Variables

      private System.Threading.Thread _worker;

      #endregion


      #region Public methods

      /// <summary>Starts (or reuses) the process resource that is specified by the StartInfo property of this Process component and associates it with the component.</summary>
      public void Start()
      {
         isBusy = true;
         HasExited = false;

         if (StartInfo.UseThread)
         {
            _worker = new System.Threading.Thread(() => createProcess());
            _worker.Start();

            System.Threading.Thread.Sleep(200);
         }
         else
         {
            createProcess();
         }
      }

      /// <summary>Immediately stops the associated process.</summary>
      public void Kill()
      {
         _worker.CTAbort();

         Dispose();
      }

      public void WaitForExit(int milliseconds = 0)
      {
         //Debug.Log("Not implemented!");
      }

      public void Dispose()
      {
         if (BaseConstants.DEV_DEBUG)
            Debug.LogWarning("Dispose called!");

         Id = 0;
         isBusy = false;
         HasExited = true;

         if (StandardOutput != null)
            StandardOutput.Dispose();

         if (StandardError != null)
            StandardError.Dispose();
      }

      #endregion


      #region Private methods

      private void createProcess()
      {
         StartTime = System.DateTime.Now;

         string app = StartInfo.FileName;
         string args = StartInfo.Arguments;

         if (BaseConstants.DEV_DEBUG)
            Debug.LogWarning($"createProcess: {StartTime}");

         try
         {
#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
//#if false
            if (StartInfo.RedirectStandardOutput)
            {
               StandardOutput = new System.IO.StreamReader(new System.IO.MemoryStream(), StartInfo.StandardOutputEncoding);
            }

            if (StartInfo.RedirectStandardError)
            {
               string tempErrFile = FileHelper.TempFile;
               args += $" 2> \"{tempErrFile}\"";

               if (BaseConstants.DEV_DEBUG)
                  Debug.Log($"tempErrFile: {tempErrFile}");

               StandardError = new System.IO.StreamReader(new System.IO.FileStream(tempErrFile, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite), StartInfo.StandardOutputEncoding);
            }
            else
            {
               StandardError = new System.IO.StreamReader(new System.IO.MemoryStream(), StartInfo.StandardOutputEncoding);
            }

            string result = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(NativeMethods.RunCommand($"{app} {args}"));

            if (StartInfo.RedirectStandardOutput && !string.IsNullOrEmpty(result))
            {
               byte[] byteArray = StartInfo.StandardOutputEncoding.GetBytes(result);
               System.IO.MemoryStream stream = new System.IO.MemoryStream(byteArray);
               StandardOutput = new System.IO.StreamReader(stream);
            }

            _exitCode = 0;
#else
            if (StartInfo.RedirectStandardOutput)
            {
               string tempStdFile = FileHelper.TempFile;
               args += $" > \"{tempStdFile}\"";

               if (BaseConstants.DEV_DEBUG)
                  Debug.Log($"tempStdFile: {tempStdFile}");

               StandardOutput = new System.IO.StreamReader(new System.IO.FileStream(tempStdFile, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite), StartInfo.StandardOutputEncoding);
            }
            else
            {
               StandardOutput = new System.IO.StreamReader(new System.IO.MemoryStream(), StartInfo.StandardOutputEncoding);
            }

            if (StartInfo.RedirectStandardError)
            {
               string tempErrFile = FileHelper.TempFile;
               args += $" 2> \"{tempErrFile}\"";

               if (BaseConstants.DEV_DEBUG)
                  Debug.Log($"tempErrFile: {tempErrFile}");

               StandardError = new System.IO.StreamReader(new System.IO.FileStream(tempErrFile, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite), StartInfo.StandardOutputEncoding);
            }
            else
            {
               StandardError = new System.IO.StreamReader(new System.IO.MemoryStream(), StartInfo.StandardOutputEncoding);
            }

            _exitCode = (uint)NativeMethods.RunCommand($"{app} {args}");
#endif
         }
         catch (System.Threading.ThreadAbortException)
         {
            //Debug.LogWarning("Process killed!");
            _exitCode = 123456;
         }
         catch (System.Exception ex)
         {
            Debug.LogError($"Process threw an error: {ex}");
            _exitCode = 99;
         }
         finally
         {
            ExitTime = System.DateTime.Now;
            Id = 0;

            if (!HasExited)
               onExited();

            isBusy = false;
            HasExited = true;
         }
      }

      #endregion

      #endregion

#endif

      private void watchStdOut()
      {
         using (System.IO.StreamReader streamReader = StandardOutput)
         {
            while (!streamReader.EndOfStream)
            {
               string reply = streamReader.ReadLine();

               if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG)
                  Debug.Log($"watchStdOut: {reply}");

               OutputDataReceived?.Invoke(this, createMockDataReceivedEventArgs(reply));
            }
         }
      }

      private void watchStdErr()
      {
         using (System.IO.StreamReader streamReader = StandardError)
         {
            while (!streamReader.EndOfStream)
            {
               string reply = streamReader.ReadLine();

               if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG)
                  Debug.Log($"watchStdErr: {reply}");

               ErrorDataReceived?.Invoke(this, createMockDataReceivedEventArgs(reply));
            }
         }
      }

      private static System.Diagnostics.DataReceivedEventArgs createMockDataReceivedEventArgs(string data)
      {
         if (string.IsNullOrEmpty(data))
            throw new System.ArgumentException("Data is null or empty.", nameof(data));

         System.Diagnostics.DataReceivedEventArgs mockEventArgs =
            (System.Diagnostics.DataReceivedEventArgs)System.Runtime.Serialization.FormatterServices
               .GetUninitializedObject(typeof(System.Diagnostics.DataReceivedEventArgs));

         if (EVENT_FIELDS.Length > 0)
         {
            EVENT_FIELDS[0].SetValue(mockEventArgs, data);
         }
         else
         {
            Debug.LogError("Could not create 'DataReceivedEventArgs'!");
         }

         return mockEventArgs;
      }
   }

   #endregion


   #region Native methods

   /// <summary>Native methods (bridge to Windows).</summary>
   internal static class NativeMethods
   {
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
//#if false

      #region Windows

      [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError = true,
         CharSet = System.Runtime.InteropServices.CharSet.Auto)]
      [return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
      internal static extern bool CreateProcess(
         string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes,
         ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags,
         System.IntPtr lpEnvironment, string lpCurrentDirectory,
         [System.Runtime.InteropServices.In] ref STARTUPINFOEX lpStartupInfo,
         out PROCESS_INFORMATION lpProcessInformation);

      [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError = true)]
      [System.Runtime.ConstrainedExecution.ReliabilityContract(
         System.Runtime.ConstrainedExecution.Consistency.WillNotCorruptState,
         System.Runtime.ConstrainedExecution.Cer.MayFail)]
      internal static extern bool CloseHandle(System.IntPtr hObject);

      [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError = true)]
      internal static extern bool GetExitCodeProcess(System.IntPtr process, ref uint exitCode);

      [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError = true)]
      internal static extern uint WaitForSingleObject(System.IntPtr handle, uint milliseconds);

      [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError = true)]
      internal static extern bool TerminateProcess(System.IntPtr hProcess, ref uint exitCode);

      [System.Runtime.InteropServices.DllImport("Kernel32.dll")]
      internal static extern uint GetLastError();

      [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet =
         System.Runtime.InteropServices.CharSet.Unicode)]
      internal struct STARTUPINFOEX
      {
         public STARTUPINFO StartupInfo;
         public System.IntPtr lpAttributeList;
      }

      [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet =
         System.Runtime.InteropServices.CharSet.Unicode)]
      internal struct STARTUPINFO
      {
         public int cb;
         public string lpReserved;
         public string lpDesktop;
         public string lpTitle;
         public int dwX;
         public int dwY;
         public int dwXSize;
         public int dwYSize;
         public int dwXCountChars;
         public int dwYCountChars;
         public int dwFillAttribute;
         public int dwFlags;
         public short wShowWindow;
         public short cbReserved2;
         public System.IntPtr lpReserved2;
         public System.IntPtr hStdInput;
         public System.IntPtr hStdOutput;
         public System.IntPtr hStdError;
      }

      [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
      internal struct PROCESS_INFORMATION
      {
         public System.IntPtr hProcess;
         public System.IntPtr hThread;
         public int dwProcessId;
         public int dwThreadId;
      }

      [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
      internal struct SECURITY_ATTRIBUTES
      {
         public int nLength;
         public System.IntPtr lpSecurityDescriptor;
         public int bInheritHandle;
      }

      #endregion

#elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
//#elif false

      #region macOS

      [System.Runtime.InteropServices.DllImport("ProcessStart")]
      internal static extern System.IntPtr RunCommand(string command);

      #endregion

#elif UNITY_STANDALONE_LINUX || UNITY_EDITOR_LINUX
//#elif true

        #region Linux

        [System.Runtime.InteropServices.DllImport("libProcessStart")]
        internal static extern int RunCommand(string command);

        #endregion

#endif
   }

   #endregion


   #region CTProcessStartInfo

   /// <summary>Specifies a set of values that are used when you start a process (mimicking the "System.Diagnostics.ProcessStartInfo"-class with the most important properties).</summary>
   public class CTProcessStartInfo
   {
      public CTProcessStartInfo()
      {
         StandardErrorEncoding = StandardOutputEncoding = System.Text.Encoding.UTF8;
         UseThread = true;
      }

      /// <summary>Gets or sets the application to be threaded.</summary>
      public bool UseThread { get; set; }

      /// <summary>Gets or sets the application to be started in cmd (command prompt).</summary>
      public bool UseCmdExecute { get; set; }

      /// <summary>Gets or sets the application or document to start.</summary>
      public string FileName { get; set; }

      /// <summary>Gets or sets the set of command-line arguments to use when starting the application.</summary>
      public string Arguments { get; set; }

      /// <summary>Gets or sets a value indicating whether to start the process in a new window.</summary>
      public bool CreateNoWindow { get; set; }

      /// <summary>Gets or sets the working directory for the process to be started.</summary>
      public string WorkingDirectory { get; set; }

      /// <summary>Gets or sets a value that indicates whether the textual output of an application is written to the StandardOutput stream.</summary>
      public bool RedirectStandardOutput { get; set; }

      /// <summary>Gets or sets a value that indicates whether the error output of an application is written to the StandardError stream.</summary>
      public bool RedirectStandardError { get; set; }

      /// <summary>Gets or sets the preferred encoding for standard output (UTF8 per default).</summary>
      public System.Text.Encoding StandardOutputEncoding { get; set; }

      /// <summary>Gets or sets the preferred encoding for error output (UTF8 per default).</summary>
      public System.Text.Encoding StandardErrorEncoding { get; set; }

      /// <summary>Gets or sets a value indicating whether to use the operating system shell to start the process (ignored, always false).</summary>
      public bool UseShellExecute { get; set; }
   }

   #endregion
}
#endif
// © 2019-2023 crosstales LLC (https://www.crosstales.com)