/******************************************************************************/
/* SlunkCrypt, by LoRd_MuldeR <MuldeR2@GMX.de>                                */
/* This work has been released under the CC0 1.0 Universal license!           */
/******************************************************************************/

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;

namespace com.muldersoft.slunkcrypt.gui.utils
{
    class SystemMenu : IDisposable
    {
        private const int WM_SYSCOMMAND = 0x112;
        public delegate void SystemMenuActionHandler(SystemMenu sender, uint menuId);

        private readonly SystemMenuActionHandler m_handler;
        private readonly HwndSource m_hwndSource;
        private readonly Dictionary<uint, string> m_menuItems = new Dictionary<uint, string>();

        private bool m_firstItem = true;
        private uint m_nextMenuId = 100;

        private volatile bool m_disposed = false;

        // =============================================================================
        // Constructor
        // =============================================================================

        public SystemMenu(Window window, SystemMenuActionHandler handler)
        {
            if (ReferenceEquals(window, null))
            {
                throw new ArgumentNullException("Window must not be null!");
            }
            if (ReferenceEquals(m_handler = handler, null))
            {
                throw new ArgumentNullException("SystemMenuActionHandler must not be null!");
            }
            window.Dispatcher.VerifyAccess();
            m_hwndSource = HwndSource.FromHwnd(new WindowInteropHelper(window).Handle);
            m_hwndSource.AddHook(WndProcHook);
        }

        // =============================================================================
        // Public methods
        // =============================================================================

        public uint AppendMenu(string captionStr)
        {
            if (m_disposed)
            {
                throw new ObjectDisposedException("Already disposed!");
            }
            m_hwndSource.Dispatcher.VerifyAccess();
            IntPtr hSysMenu = GetSystemMenu(m_hwndSource.Handle, false);
            if (hSysMenu != IntPtr.Zero)
            {
                if (m_firstItem)
                {
                    if (AppendMenu(hSysMenu, Native.MenuFlags.MF_SEPARATOR, 0, string.Empty))
                    {
                        m_firstItem = false;
                    }
                }
                if (AppendMenu(hSysMenu, Native.MenuFlags.MF_STRING, m_nextMenuId, captionStr))
                {
                    uint menuItemId = m_nextMenuId++;
                    m_menuItems.Add(menuItemId, captionStr);
                    return menuItemId;
                }
            }
            return 0;
        }

        public bool ModifyMenu(uint menuId, bool isChecked)
        {
            if (m_disposed)
            {
                throw new ObjectDisposedException("Already disposed!");
            }
            m_hwndSource.Dispatcher.VerifyAccess();
            string captionStr;
            if (m_menuItems.TryGetValue(menuId, out captionStr))
            {
                IntPtr hSysMenu = GetSystemMenu(m_hwndSource.Handle, false);
                if (hSysMenu != IntPtr.Zero)
                {
                    return ModifyMenu(hSysMenu, menuId, Native.MenuFlags.MF_BYCOMMAND | (isChecked ? Native.MenuFlags.MF_CHECKED : Native.MenuFlags.MF_UNCHECKED), menuId, captionStr);
                }
                return false;
            }
            else
            {
                throw new ArgumentException("Menu item ID is unknown!");
            }
        }

        // =============================================================================
        // Internal methods
        // =============================================================================

        private IntPtr WndProcHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            if (msg == WM_SYSCOMMAND)
            {
                uint menuId;
                if (m_menuItems.ContainsKey(menuId = (uint) wParam.ToInt32()))
                {
                    m_handler(this, menuId);
                    handled = true;
                }
            }
            return IntPtr.Zero;
        }

        private IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert)
        {
            try
            {
                return Native.GetSystemMenu(hWnd, bRevert);
            }
            catch
            {
                return IntPtr.Zero;
            }
        }

        private bool AppendMenu(IntPtr hMenu, Native.MenuFlags uFlags, uint uIDNewItem, string lpNewItem)
        {
            try
            {
                return Native.AppendMenu(hMenu, uFlags, new UIntPtr(uIDNewItem), lpNewItem);
            }
            catch
            {
                return false;
            }
        }

        private bool ModifyMenu(IntPtr hMenu, uint uPosition, Native.MenuFlags uFlags, uint uIDNewItem, string lpNewItem)
        {
            return Native.ModifyMenu(hMenu, uPosition, uFlags, new UIntPtr(uIDNewItem), lpNewItem);
        }

        public void Dispose()
        {
            if (!m_disposed)
            {
                m_disposed = true;
                m_hwndSource.RemoveHook(WndProcHook);
            }
        }

        // =============================================================================
        // Event handlers
        // =============================================================================

        private class Native
        {
            private const string DLL_USER32 = "user32.dll";

            [Flags]
            public enum MenuFlags : uint
            {
                MF_STRING     = 0x0000,
                MF_UNCHECKED  = 0x0000,
                MF_BYCOMMAND =  0x0000,
                MF_CHECKED    = 0x0008,
                MF_BYPOSITION = 0x0400,
                MF_SEPARATOR  = 0x0800
            }

            [DllImport(DLL_USER32, CharSet = CharSet.Unicode, EntryPoint = "GetSystemMenu", ExactSpelling = true)]
            public static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);

            [DllImport(DLL_USER32, CharSet = CharSet.Unicode, EntryPoint = "AppendMenuW", ExactSpelling = true)]
            public static extern bool AppendMenu(IntPtr hMenu, MenuFlags uFlags, UIntPtr uIDNewItem, string lpNewItem);

            [DllImport(DLL_USER32, CharSet = CharSet.Unicode, EntryPoint = "ModifyMenuW", ExactSpelling = true)]
            public static extern bool ModifyMenu(IntPtr hMenu, uint uPosition, MenuFlags uFlags, UIntPtr uIDNewItem, string lpNewItem);
        }
    }
}
