using System;
using System.Windows.Forms;
using System.Threading;
using System.Runtime.InteropServices;
using Process = System.Diagnostics.Process;
using System.ComponentModel;
using System.Globalization;
using Microsoft.Win32.TaskScheduler;
using System.IO.MemoryMappedFiles;
using System.Text;

namespace DS4Windows
{
    [System.Security.SuppressUnmanagedCodeSecurity]
    static class Program
    {
        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

        [DllImport("user32.dll", EntryPoint = "FindWindow")]
        private static extern IntPtr FindWindow(string sClass, string sWindow);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
        private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, ref COPYDATASTRUCT lParam);      

        [StructLayout(LayoutKind.Sequential)]
        public struct COPYDATASTRUCT
        {
            public IntPtr dwData;
            public int cbData;
            public IntPtr lpData;
        }

        // Add "global\" in front of the EventName, then only one instance is allowed on the
        // whole system, including other users. But the application can not be brought
        // into view, of course. 
        private static string SingleAppComEventName = "{a52b5b20-d9ee-4f32-8518-307fa14aa0c6}";
        private static EventWaitHandle threadComEvent = null;
        private static bool exitComThread = false;
        public static ControlService rootHub;
        private static Thread testThread;
        private static Thread controlThread;
        private static System.Threading.Timer collectTimer;
        private static Form ds4form;

        private static MemoryMappedFile ipcClassNameMMF = null; // MemoryMappedFile for inter-process communication used to hold className of DS4Form window
        private static MemoryMappedViewAccessor ipcClassNameMMA = null;

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            //Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("ja");
            for (int i = 0, argsLen = args.Length; i < argsLen; i++)
            {
                string s = args[i];
                if (s == "driverinstall" || s == "-driverinstall")
                {
                    Application.EnableVisualStyles();
                    Application.Run(new Forms.WelcomeDialog(true));
                    return;
                }
                else if (s == "re-enabledevice" || s == "-re-enabledevice")
                {
                    try
                    {
                        i++;
                        string deviceInstanceId = args[i];
                        DS4Devices.reEnableDevice(deviceInstanceId);
                        Environment.ExitCode = 0;
                        return;
                    }
                    catch (Exception)
                    {
                        Environment.ExitCode = Marshal.GetLastWin32Error();
                        return;
                    }
                }
                else if (s == "runtask" || s == "-runtask")
                {
                    TaskService ts = new TaskService();
                    Task tasker = ts.FindTask("RunDS4Windows");
                    if (tasker != null)
                    {
                        tasker.Run("");
                    }

                    Environment.ExitCode = 0;
                    return;
                }
                else if (s == "command" || s == "-command")
                {
                    i++;

                    IntPtr hWndDS4WindowsForm = IntPtr.Zero;

                    if (args[i].Length > 0 && args[i].Length <= 256)
                    {
                        // Find the main DS4Form window handle and post WM_COPYDATA inter-process message. IPCClasName.dat file was created by the main DS4Windows process 
                        // and it contains the name of the DS4Form .NET window class name string (the hash key part of the string is dynamically generated by .NET engine and it may change,
                        // so that's why the main process re-creates the file if the window class name is changed by .NET framework). Finding the WND handle usig both class name and window name
                        // limits chances that WM_COPYDATA message is sent to a wrong window.

                        hWndDS4WindowsForm = FindWindow(ReadIPCClassNameMMF(), "DS4Windows");
                        if (hWndDS4WindowsForm != IntPtr.Zero)
                        {
                            COPYDATASTRUCT cds;
                            cds.lpData = IntPtr.Zero;

                            try
                            {
                                cds.dwData = IntPtr.Zero;
                                cds.cbData = args[i].Length;
                                cds.lpData = Marshal.StringToHGlobalAnsi(args[i]);
                                SendMessage(hWndDS4WindowsForm, Forms.DS4Form.WM_COPYDATA, IntPtr.Zero, ref cds);
                            }
                            finally
                            {
                                if(cds.lpData != IntPtr.Zero)
                                    Marshal.FreeHGlobal(cds.lpData);
                            }
                        }
                        else
                        {
                            Environment.ExitCode = 2;
                        }
                    }
                    else
                    {
                        Environment.ExitCode = 1;
                    }

                    return;
                }
            }

            try
            {
                Process.GetCurrentProcess().PriorityClass =
                    System.Diagnostics.ProcessPriorityClass.High;
            }
            catch { } // Ignore problems raising the priority.

            // Force Normal IO Priority
            IntPtr ioPrio = new IntPtr(2);
            Util.NtSetInformationProcess(Process.GetCurrentProcess().Handle,
                Util.PROCESS_INFORMATION_CLASS.ProcessIoPriority, ref ioPrio, 4);

            // Force Normal Page Priority
            IntPtr pagePrio = new IntPtr(5);
            Util.NtSetInformationProcess(Process.GetCurrentProcess().Handle,
                Util.PROCESS_INFORMATION_CLASS.ProcessPagePriority, ref pagePrio, 4);

            try
            {
                // another instance is already running if OpenExsting succeeds.
                threadComEvent = EventWaitHandle.OpenExisting(SingleAppComEventName,
                    System.Security.AccessControl.EventWaitHandleRights.Synchronize |
                    System.Security.AccessControl.EventWaitHandleRights.Modify);
                threadComEvent.Set();  // signal the other instance.
                threadComEvent.Close();
                return;    // return immediatly.
            }
            catch { /* don't care about errors */ }

            // Create the Event handle
            threadComEvent = new EventWaitHandle(false, EventResetMode.ManualReset, SingleAppComEventName);
            //System.Threading.Tasks.Task.Run(() => CreateTempWorkerThread());
            //CreateInterAppComThread();
            CreateTempWorkerThread();
            //System.Threading.Tasks.Task.Run(() => { Thread.CurrentThread.Priority = ThreadPriority.Lowest; CreateInterAppComThread(); Thread.CurrentThread.Priority = ThreadPriority.Lowest; }).Wait();

            //if (mutex.WaitOne(TimeSpan.Zero, true))
            //{
            createControlService();
                //rootHub = new ControlService();
                Application.EnableVisualStyles();
                ds4form = new Forms.DS4Form(args);
                Application.Run();
            //mutex.ReleaseMutex();
            //}

            exitComThread = true;
            threadComEvent.Set();  // signal the other instance.
            while (testThread.IsAlive)
                Thread.SpinWait(500);
            threadComEvent.Close();

            if (ipcClassNameMMA != null) ipcClassNameMMA.Dispose();
            if (ipcClassNameMMF != null) ipcClassNameMMF.Dispose();
        }

        private static void createControlService()
        {
            controlThread = new Thread(() => {
                rootHub = new ControlService();
                collectTimer = new System.Threading.Timer(GarbageTask, null, 30000, 30000);
            });
            controlThread.Priority = ThreadPriority.Normal;
            controlThread.IsBackground = true;
            controlThread.Start();
            while (controlThread.IsAlive)
                Thread.SpinWait(500);
        }

        private static void GarbageTask(object state)
        {
            GC.Collect(0, GCCollectionMode.Forced, false);
        }

        private static void CreateTempWorkerThread()
        {
            testThread = new Thread(SingleAppComThread_DoWork);
            testThread.Priority = ThreadPriority.Lowest;
            testThread.IsBackground = true;
            testThread.Start();
        }

        private static void SingleAppComThread_DoWork()
        {
            while (!exitComThread)
            {
                // check for a signal.
                if (threadComEvent.WaitOne())
                {
                    threadComEvent.Reset();
                    // The user tried to start another instance. We can't allow that, 
                    // so bring the other instance back into view and enable that one. 
                    // That form is created in another thread, so we need some thread sync magic.
                    if (!exitComThread)
                    {
                        ds4form?.Invoke(new SetFormVisableDelegate(ThreadFormVisable), ds4form);
                    }
                }
            }
        }

        /// <summary>
        /// When this method is called using a Invoke then this runs in the thread
        /// that created the form, which is nice. 
        /// </summary>
        /// <param name="frm"></param>
        private delegate void SetFormVisableDelegate(Form frm);
        private static void ThreadFormVisable(Form frm)
        {
            if (frm is Forms.DS4Form)
            {
                // display the form and bring to foreground.
                Forms.DS4Form temp = (Forms.DS4Form)frm;
                temp.Show();
                temp.WindowState = FormWindowState.Normal;
            }
        }

        public static void CreateIPCClassNameMMF(IntPtr hWnd)
        {
            if (ipcClassNameMMA != null) return; // Already holding a handle to MMF file. No need to re-write the data

            try
            {
                StringBuilder wndClassNameStr = new StringBuilder(128);
                if (GetClassName(hWnd, wndClassNameStr, wndClassNameStr.Capacity) != 0 && wndClassNameStr.Length > 0)
                {
                    byte[] buffer = ASCIIEncoding.ASCII.GetBytes(wndClassNameStr.ToString());

                    ipcClassNameMMF = MemoryMappedFile.CreateNew("DS4Windows_IPCClassName.dat", 128);
                    ipcClassNameMMA = ipcClassNameMMF.CreateViewAccessor(0, buffer.Length);
                    ipcClassNameMMA.WriteArray(0, buffer, 0, buffer.Length);
                    // The MMF file is alive as long this process holds the file handle open
                }
            }
            catch (Exception)
            {
                /* Eat all exceptions because errors here are not fatal for DS4Win */
            }
        }

        private static string ReadIPCClassNameMMF()
        {
            MemoryMappedFile mmf = null;
            MemoryMappedViewAccessor mma = null;

            try
            {
                byte[] buffer = new byte[128];
                mmf = MemoryMappedFile.OpenExisting("DS4Windows_IPCClassName.dat");
                mma = mmf.CreateViewAccessor(0, 128);
                mma.ReadArray(0, buffer, 0, buffer.Length);
                return ASCIIEncoding.ASCII.GetString(buffer);
            }
            catch (Exception)
            {
                // Eat all exceptions                
            }
            finally
            {
                if (mma != null) mma.Dispose();
                if (mmf != null) mmf.Dispose();
            }

            return null;
        }
    }
}