﻿//
// ASCOM.UnityAstro.ASCOM.Winforms.TelescopeServer Local COM Server
//
// This is the core of a managed COM Local Server, capable of serving
// multiple instances of multiple interfaces, within a single
// executable. This implementes the equivalent functionality of VB6
// which has been extensively used in ASCOM for drivers that provide
// multiple interfaces to multiple clients (e.g. Meade Telescope
// and Focuser) as well as hubs (e.g., POTH).
//
// Written by: Robert B. Denny (Version 1.0.1, 29-May-2007)
// Modified by Chris Rowland and Peter Simpson to allow use with multiple devices of the same type March 2011
//
//
using System;
using System.IO;
using System.Windows;
using System.Collections;
using System.Runtime.InteropServices;
using System.Reflection;
using ASCOM.Utilities;
using Microsoft.Win32;
using System.Text;
using System.Threading;
using System.Security.Principal;
using System.Diagnostics;
using ASCOM;
using System.Windows.Threading;
using System.Diagnostics.CodeAnalysis;

namespace ASCOM.Lunatic
{
   public static class TelescopeServer
   {

      #region Access to kernel32.dll, user32.dll, and ole32.dll functions
      [Flags]
      enum CLSCTX : uint
      {
         CLSCTX_INPROC_SERVER = 0x1,
         CLSCTX_INPROC_HANDLER = 0x2,
         CLSCTX_LOCAL_SERVER = 0x4,
         CLSCTX_INPROC_SERVER16 = 0x8,
         CLSCTX_REMOTE_SERVER = 0x10,
         CLSCTX_INPROC_HANDLER16 = 0x20,
         CLSCTX_RESERVED1 = 0x40,
         CLSCTX_RESERVED2 = 0x80,
         CLSCTX_RESERVED3 = 0x100,
         CLSCTX_RESERVED4 = 0x200,
         CLSCTX_NO_CODE_DOWNLOAD = 0x400,
         CLSCTX_RESERVED5 = 0x800,
         CLSCTX_NO_CUSTOM_MARSHAL = 0x1000,
         CLSCTX_ENABLE_CODE_DOWNLOAD = 0x2000,
         CLSCTX_NO_FAILURE_LOG = 0x4000,
         CLSCTX_DISABLE_AAA = 0x8000,
         CLSCTX_ENABLE_AAA = 0x10000,
         CLSCTX_FROM_DEFAULT_CONTEXT = 0x20000,
         CLSCTX_INPROC = CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER,
         CLSCTX_SERVER = CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER,
         CLSCTX_ALL = CLSCTX_SERVER | CLSCTX_INPROC_HANDLER
      }

      [Flags]
      enum COINIT : uint
      {
         /// Initializes the thread for multi-threaded object concurrency.
         COINIT_MULTITHREADED = 0x0,
         /// Initializes the thread for apartment-threaded object concurrency. 
         COINIT_APARTMENTTHREADED = 0x2,
         /// Disables DDE for Ole1 support.
         COINIT_DISABLE_OLE1DDE = 0x4,
         /// Trades memory for speed.
         COINIT_SPEED_OVER_MEMORY = 0x8
      }

      [Flags]
      enum REGCLS : uint
      {
         REGCLS_SINGLEUSE = 0,
         REGCLS_MULTIPLEUSE = 1,
         REGCLS_MULTI_SEPARATE = 2,
         REGCLS_SUSPENDED = 4,
         REGCLS_SURROGATE = 8
      }


      // CoInitializeEx() can be used to set the apartment model
      // of individual threads.
      [SuppressMessage("Microsoft.Design", "CA1060: Move P / Invokes to NativeMethods class")]
      [DllImport("ole32.dll")]
      static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit);

      // CoUninitialize() is used to uninitialize a COM thread.
      [SuppressMessage("Microsoft.Design", "CA1060: Move P / Invokes to NativeMethods class")]
      [DllImport("ole32.dll")]
      static extern void CoUninitialize();

      // PostThreadMessage() allows us to post a Windows Message to
      // a specific thread (identified by its thread id).
      // We will need this API to post a WM_QUIT message to the main 
      // thread in order to terminate this application.
      [DllImport("user32.dll")]
      [SuppressMessage("Microsoft.Design", "CA1060: Move P / Invokes to NativeMethods class")]
      static extern bool PostThreadMessage(uint idThread, uint Msg, UIntPtr wParam,
         IntPtr lParam);

      // GetCurrentThreadId() allows us to obtain the thread id of the
      // calling thread. This allows us to post the WM_QUIT message to
      // the main thread.
      [DllImport("kernel32.dll")]
      [SuppressMessage("Microsoft.Design", "CA1060: Move P / Invokes to NativeMethods class")]
      static extern uint GetCurrentThreadId();
      #endregion

      #region Private Data
      private static int _ObjsInUse;                // Keeps a count on the total number of objects alive.
      private static int _ServerLocks;                 // Keeps a lock count on this application.
      private static TelescopeServerMainWindow _MainWindow = null;          // Reference to our main form
      private static ArrayList _ComObjectAssys;            // Dynamically loaded assemblies containing served COM objects
      private static ArrayList _ComObjectTypes;            // Served COM object types
      private static ArrayList _ClassFactories;            // Served COM object class factories
      private static string _AppId = "{09A57570-CF27-4026-B575-AF24FEC04858}";  // Our AppId
      private static readonly Object _lockObject = new object();
      #endregion

      // This property returns the main thread's id.
      public static uint MainThreadId { get; private set; } // Stores the main thread's thread id.

      // Used to tell if started by COM or manually
      public static bool StartedByCOM { get; private set; }   // True if server started by COM (-embedding)


      #region Server Lock, Object Counting, and AutoQuit on COM startup
      // Returns the total number of objects alive currently.
      public static int ObjectsCount
      {
         get
         {
            lock (_lockObject) {
               return _ObjsInUse;
            }
         }
      }

      // This method performs a thread-safe incrementation of the objects count.
      public static int CountObject()
      {
         // Increment the global count of objects.
         return Interlocked.Increment(ref _ObjsInUse);
      }

      // This method performs a thread-safe decrementation the objects count.
      public static int UncountObject()
      {
         // Decrement the global count of objects.
         return Interlocked.Decrement(ref _ObjsInUse);
      }

      // Returns the current server lock count.
      public static int ServerLockCount
      {
         get
         {
            lock (_lockObject) {
               return _ServerLocks;
            }
         }
      }

      // This method performs a thread-safe incrementation the 
      // server lock count.
      public static int CountLock()
      {
         // Increment the global lock count of this server.
         return Interlocked.Increment(ref _ServerLocks);
      }

      // This method performs a thread-safe decrementation the 
      // server lock count.
      public static int UncountLock()
      {
         // Decrement the global lock count of this server.
         return Interlocked.Decrement(ref _ServerLocks);
      }

      // AttemptToTerminateServer() will check to see if the objects count and the server 
      // lock count have both dropped to zero.
      //
      // If so, and if we were started by COM, we post a WM_QUIT message to the main thread's
      // message loop. This will cause the message loop to exit and hence the termination 
      // of this application. If hand-started, then just trace that it WOULD exit now.
      //
      public static void ExitIf()
      {
         System.Diagnostics.Trace.WriteLine(string.Format("ExitIf: Objects {0}, ServerLocks {1}", ObjectsCount, ServerLockCount));
         lock (_lockObject) {
            if ((ObjectsCount <= 0) && (ServerLockCount <= 0)) {
               if (StartedByCOM) {
                  UIntPtr wParam = new UIntPtr(0);
                  IntPtr lParam = new IntPtr(0);
                  PostThreadMessage(MainThreadId, 0x0012, wParam, lParam);
               }
            }
         }
      }

      #endregion

      // -----------------
      // PRIVATE FUNCTIONS
      // -----------------

      #region Dynamic Driver Assembly Loader
      //
      // Load the assemblies that contain the classes that we will serve
      // via COM. These will be located in the same folder as
      // our executable.
      //
      private static bool LoadComObjectAssemblies()
      {
         _ComObjectAssys = new ArrayList();
         _ComObjectTypes = new ArrayList();

         // put everything into one folder, the same as the server.
         string assyPath = Assembly.GetEntryAssembly().Location;
         assyPath = Path.GetDirectoryName(assyPath);

         DirectoryInfo d = new DirectoryInfo(assyPath);
         foreach (FileInfo fi in d.GetFiles("*.dll")) {
            string aPath = fi.FullName;
            //
            // First try to load the assembly and get the types for
            // the class and the class factory. If this doesn't work ????
            //
            try {
               Assembly so = Assembly.LoadFrom(aPath);
               //PWGS Get the types in the assembly
               Type[] types = so.GetTypes();
               foreach (Type type in types) {
                  // PWGS Now checks the type rather than the assembly
                  // Check to see if the type has the ServedClassName attribute, only use it if it does.
                  MemberInfo info = type;

                  object[] attrbutes = info.GetCustomAttributes(typeof(ServedClassNameAttribute), false);
                  if (attrbutes.Length > 0) {
                     //MessageBox.Show("Adding Type: " + type.Name + " " + type.FullName);
                     _ComObjectTypes.Add(type); //PWGS - much simpler
                     _ComObjectAssys.Add(so);
                  }
               }
            }
            catch (BadImageFormatException) {
               // Probably an attempt to load a Win32 DLL (i.e. not a .net assembly)
               // Just swallow the exception and continue to the next item.
               continue;
            }
            catch (Exception e) {
               MessageBox.Show("Failed to load served COM class assembly " + fi.Name + " - " + e.Message,
                  "ASCOM.Lunatic.TelescopeServer", MessageBoxButton.OK, MessageBoxImage.Stop);
               return false;
            }

         }
         return true;
      }
      #endregion

      #region COM Registration and Unregistration
      //
      // Test if running elevated
      //
      private static bool IsAdministrator
      {
         get
         {
            WindowsIdentity i = WindowsIdentity.GetCurrent();
            WindowsPrincipal p = new WindowsPrincipal(i);
            return p.IsInRole(WindowsBuiltInRole.Administrator);
         }
      }

      //
      // Elevate by re-running ourselves with elevation dialog
      //
      private static void ElevateSelf(string arg)
      {
         ProcessStartInfo si = new ProcessStartInfo();
         si.Arguments = arg;
         si.WorkingDirectory = Environment.CurrentDirectory;
         si.FileName = Assembly.GetExecutingAssembly().Location;
         si.Verb = "runas";
         try { Process.Start(si); }
         catch (System.ComponentModel.Win32Exception) {
            MessageBox.Show("The ASCOM.Lunatic.TelescopeServer was not " + (arg == "/register" ? "registered" : "unregistered") +
                " because you did not allow it.", "UnityAstro.ASCOM.Winforms.TelescopeServer", MessageBoxButton.OK, MessageBoxImage.Warning);
         }
         catch (Exception ex) {
            MessageBox.Show(ex.ToString(), "ASCOM.Lunatic.TelescopeServer", MessageBoxButton.OK, MessageBoxImage.Stop);
         }
         return;
      }

      //
      // Do everything to register this for COM. Never use REGASM on
      // this exe assembly! It would create InProcServer32 entries 
      // which would prevent proper activation!
      //
      // Using the list of COM object types generated during dynamic
      // assembly loading, it registers each one for COM as served by our
      // exe/local server, as well as registering it for ASCOM. It also
      // adds DCOM info for the local server itself, so it can be activated
      // via an outboiud connection from TheSky.
      //
      private static void RegisterObjects()
      {
         if (!IsAdministrator) {
            ElevateSelf("/register");
            return;
         }
         //
         // If reached here, we're running elevated
         //

         Assembly assy = Assembly.GetExecutingAssembly();
         Attribute attr = Attribute.GetCustomAttribute(assy, typeof(AssemblyTitleAttribute));
         string assyTitle = ((AssemblyTitleAttribute)attr).Title;
         attr = Attribute.GetCustomAttribute(assy, typeof(AssemblyDescriptionAttribute));
         string assyDescription = ((AssemblyDescriptionAttribute)attr).Description;

         //
         // Local server's DCOM/AppID information
         //
         try {
            //
            // HKCR\APPID\appid
            //
            using (RegistryKey key = Registry.ClassesRoot.CreateSubKey("APPID\\" + _AppId)) {
               key.SetValue(null, assyDescription);
               key.SetValue("AppID", _AppId);
               key.SetValue("AuthenticationLevel", 1, RegistryValueKind.DWord);
            }
            //
            // HKCR\APPID\exename.ext
            //
            using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(string.Format("APPID\\{0}",
                        Path.GetDirectoryName(assy.Location)))) {
               key.SetValue("AppID", _AppId);
            }
         }
         catch (Exception ex) {
            MessageBox.Show("Error while registering the server:\n" + ex.ToString(),
                  "ASCOM.Lunatic.TelescopeServer", MessageBoxButton.OK, MessageBoxImage.Stop);
            return;
         }
         finally {
         }

         //
         // For each of the driver assemblies
         //
         foreach (Type type in _ComObjectTypes) {
            bool bFail = false;
            try {
               //
               // HKCR\CLSID\clsid
               //
               string clsid = Marshal.GenerateGuidForType(type).ToString("B");
               string progid = Marshal.GenerateProgIdForType(type);
               //PWGS Generate device type from the Class name
               string deviceType = type.Name;

               using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(string.Format("CLSID\\{0}", clsid))) {
                  key.SetValue(null, progid);                  // Could be assyTitle/Desc??, but .NET components show ProgId here
                  key.SetValue("AppId", _AppId);
                  using (RegistryKey key2 = key.CreateSubKey("Implemented Categories")) {
                     key2.CreateSubKey("{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}");
                  }
                  using (RegistryKey key2 = key.CreateSubKey("ProgId")) {
                     key2.SetValue(null, progid);
                  }
                  key.CreateSubKey("Programmable");
                  using (RegistryKey key2 = key.CreateSubKey("LocalServer32")) {
                     key2.SetValue(null, assy.Location);
                  }
               }
               //
               // HKCR\CLSID\progid
               //
               using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(progid)) {
                  key.SetValue(null, assyTitle);
                  using (RegistryKey key2 = key.CreateSubKey("CLSID")) {
                     key2.SetValue(null, clsid);
                  }
               }
               //
               // ASCOM 
               //
               assy = type.Assembly;

               // Pull the display name from the ServedClassName attribute.
               attr = Attribute.GetCustomAttribute(type, typeof(ServedClassNameAttribute)); //PWGS Changed to search type for attribute rather than assembly
               string chooserName = ((ServedClassNameAttribute)attr).DisplayName ?? "MultiServer";
               using (var P = new ASCOM.Utilities.Profile()) {
                  P.DeviceType = deviceType;
                  P.Register(progid, chooserName);
               }
            }
            catch (Exception ex) {
               MessageBox.Show("Error while registering the server:\n" + ex.ToString(),
                     "ASCOM.Lunatic.TelescopeServer", MessageBoxButton.OK, MessageBoxImage.Stop);
               bFail = true;
            }
            finally {
            }
            if (bFail) break;
         }
      }

      //
      // Remove all traces of this from the registry. 
      //
      // **TODO** If the above does AppID/DCOM stuff, this would have
      // to remove that stuff too.
      //
      private static void UnregisterObjects()
      {
         if (!IsAdministrator) {
            ElevateSelf("/unregister");
            return;
         }

         //
         // Local server's DCOM/AppID information
         //
         Registry.ClassesRoot.DeleteSubKey(string.Format("APPID\\{0}", _AppId), false);
         Registry.ClassesRoot.DeleteSubKey(string.Format("APPID\\{0}",
               Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)), false);

         //
         // For each of the driver assemblies
         //
         foreach (Type type in _ComObjectTypes) {
            string clsid = Marshal.GenerateGuidForType(type).ToString("B");
            string progid = Marshal.GenerateProgIdForType(type);
            string deviceType = type.Name;
            //
            // Best efforts
            //
            //
            // HKCR\progid
            //
            Registry.ClassesRoot.DeleteSubKey(String.Format("{0}\\CLSID", progid), false);
            Registry.ClassesRoot.DeleteSubKey(progid, false);
            //
            // HKCR\CLSID\clsid
            //
            Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\Implemented Categories\\{{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}}", clsid), false);
            Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\Implemented Categories", clsid), false);
            Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\ProgId", clsid), false);
            Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\LocalServer32", clsid), false);
            Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\Programmable", clsid), false);
            Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}", clsid), false);
            try {
               //
               // ASCOM
               //
               using (var P = new ASCOM.Utilities.Profile()) {
                  P.DeviceType = deviceType;
                  P.Unregister(progid);
               }
            }
            catch (Exception) { }
         }
      }
      #endregion

      #region Class Factory Support
      //
      // On startup, we register the class factories of the COM objects
      // that we serve. This requires the class facgtory name to be
      // equal to the served class name + "ClassFactory".
      //
      private static bool RegisterClassFactories()
      {
         _ClassFactories = new ArrayList();
         foreach (Type type in _ComObjectTypes) {
            ClassFactory factory = new ClassFactory(type);              // Use default context & flags
            _ClassFactories.Add(factory);
            if (!factory.RegisterClassObject()) {
               MessageBox.Show("Failed to register class factory for " + type.Name,
                  "ASCOM.Lunatic.TelescopeServer", MessageBoxButton.OK, MessageBoxImage.Stop);
               return false;
            }
         }
         ClassFactory.ResumeClassObjects();                          // Served objects now go live
         return true;
      }

      private static void RevokeClassFactories()
      {
         ClassFactory.SuspendClassObjects();                         // Prevent race conditions
         foreach (ClassFactory factory in _ClassFactories)
            factory.RevokeClassObject();
      }
      #endregion

      #region Command Line Arguments
      //
      // ProcessArguments() will process the command-line arguments
      // If the return value is true, we carry on and start this application.
      // If the return value is false, we terminate this application immediately.
      //
      private static bool ProcessArguments(string[] args)
      {
         bool bRet = true;

         //
         //**TODO** -Embedding is "ActiveX start". Prohibit non_AX starting?
         //
         if (args.Length > 0) {

            switch (args[0].ToLower()) {
               case "-embedding":
                  StartedByCOM = true;                            // Indicate COM started us
                  break;

               case "-register":
               case @"/register":
               case "-regserver":                                 // Emulate VB6
               case @"/regserver":
                  RegisterObjects();                              // Register each served object
                  bRet = false;
                  break;

               case "-unregister":
               case @"/unregister":
               case "-unregserver":                            // Emulate VB6
               case @"/unregserver":
                  UnregisterObjects();                         //Unregister each served object
                  bRet = false;
                  break;

               default:
                  MessageBox.Show("Unknown argument: " + args[0] + "\nValid are : -register, -unregister and -embedding",
                     "ASCOM.Lunatic.TelescopeServer", MessageBoxButton.OK, MessageBoxImage.Exclamation);
                  break;
            }
         }
         else
            StartedByCOM = false;

         return bRet;
      }
      #endregion

      #region SERVER ENTRY POINT (main)
      //
      // ==================
      // SERVER ENTRY POINT
      // ==================
      //
      //[STAThread]
      /// <summary>
      /// Application Entry Point.
      /// </summary>
      [System.STAThreadAttribute()]
      [System.Diagnostics.DebuggerNonUserCodeAttribute()]
      [System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]
      static void Main(string[] args)
      {
         if (!LoadComObjectAssemblies()) return;                  // Load served COM class assemblies, get types

         if (!ProcessArguments(args)) return;                  // Register/Unregister
                                                               //#if DEBUG
                                                               //         RegisterObjects();
                                                               //#endif
         MessageBox.Show("Starting Telescope Server");

         // Initialize critical member variables.
         _ObjsInUse = 0;
         _ServerLocks = 0;
         MainThreadId = GetCurrentThreadId();
         Thread.CurrentThread.Name = "Main Thread";

         _MainWindow = new TelescopeServerMainWindow();
         //if (StartedByCOM) {
         //   _MainWindow.WindowState = WindowState.Minimized;
         //}

         ASCOM.Lunatic.TelescopeServerApp app = new ASCOM.Lunatic.TelescopeServerApp();
         app.MainWindow = _MainWindow;

         // Register the class factories of the served objects
         RegisterClassFactories();

         // Start up the garbage collection thread.
         GarbageCollection GarbageCollector = new GarbageCollection(1000);
         Thread GCThread = new Thread(new ThreadStart(GarbageCollector.GCWatch));
         GCThread.Name = "Garbage Collection Thread";
         GCThread.Start();

         //
         // Start the message loop. This serializes incoming calls to our
         // served COM objects, making this act like the VB6 equivalent!
         //
         try {
            if (StartedByCOM) {
               _MainWindow.WindowState = WindowState.Minimized;
            }
            app.Run();
         }
         finally {
            // Revoke the class factories immediately.
            // Don't wait until the thread has stopped before
            // we perform revocation!!!
            RevokeClassFactories();
            //#if DEBUG
            //            UnregisterObjects();
            //#endif
            // Now stop the Garbage Collector thread.
            GarbageCollector.StopThread();
            GarbageCollector.WaitForThreadToStop();
            GarbageCollector.Dispose();
         }
      }

      private static void TestExitIf(object state, EventArgs e)
      {
         System.Diagnostics.Trace.WriteLine(string.Format("ExitIf: Objects {0}, ServerLocks {1}", ObjectsCount, ServerLockCount));

      }
      #endregion
   }
}
