using System.Drawing;
using System.Runtime.InteropServices;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using WinRT.Interop;

namespace YMhut.Box.WinUI.Services;

public sealed class WindowsTrayService : ITrayService
{
    private const int GwlWndProc = -4;
    private const uint NifMessage = 0x00000001;
    private const uint NifIcon = 0x00000002;
    private const uint NifTip = 0x00000004;
    private const uint NimAdd = 0x00000000;
    private const uint NimDelete = 0x00000002;
    private const uint WmApp = 0x8000;
    private const uint WmTrayIcon = WmApp + 0x510;
    private const uint WmLButtonDblClk = 0x0203;
    private const uint WmRButtonUp = 0x0205;
    private const uint WmContextMenu = 0x007B;
    private const uint WmNull = 0x0000;
    private const uint MfString = 0x00000000;
    private const uint MfSeparator = 0x00000800;
    private const uint TpmRightButton = 0x0002;
    private const uint TpmReturnCmd = 0x0100;
    private const nuint ShowCommand = 1001;
    private const nuint ExitCommand = 1002;

    private Window? _window;
    private AppWindow? _appWindow;
    private DispatcherQueue? _dispatcherQueue;
    private nint _hwnd;
    private nint _oldWndProc;
    private Icon? _icon;
    private bool _trayAdded;
    private WindowProc? _wndProc;

    public event EventHandler? ExitRequested;

    public bool IsAvailable => _trayAdded;

    public void Initialize(Window window)
    {
        if (_trayAdded)
        {
            return;
        }

        _window = window;
        _dispatcherQueue = window.DispatcherQueue;
        _hwnd = WindowNative.GetWindowHandle(window);
        _appWindow = GetAppWindow(window);
        SubclassWindow();
        AddTrayIcon();
    }

    public void ShowMainWindow()
    {
        Dispatch(() =>
        {
            _appWindow?.Show();
            _window?.Activate();
        });
    }

    public void HideMainWindow()
    {
        Dispatch(() => _appWindow?.Hide());
    }

    public void Dispose()
    {
        RemoveTrayIcon();
        if (_hwnd != 0 && _oldWndProc != 0)
        {
            SetWindowLongPtr(_hwnd, GwlWndProc, _oldWndProc);
            _oldWndProc = 0;
        }

        _icon?.Dispose();
        _icon = null;
    }

    private void SubclassWindow()
    {
        if (_hwnd == 0 || _oldWndProc != 0)
        {
            return;
        }

        _wndProc = WndProc;
        _oldWndProc = SetWindowLongPtr(_hwnd, GwlWndProc, Marshal.GetFunctionPointerForDelegate(_wndProc));
    }

    private void AddTrayIcon()
    {
        if (_hwnd == 0)
        {
            return;
        }

        _icon = LoadIcon();
        var data = CreateNotifyIconData();
        data.uFlags = NifMessage | NifIcon | NifTip;
        data.uCallbackMessage = WmTrayIcon;
        data.hIcon = _icon.Handle;
        data.szTip = "YMhut Box";
        _trayAdded = Shell_NotifyIcon(NimAdd, ref data);
    }

    private void RemoveTrayIcon()
    {
        if (!_trayAdded || _hwnd == 0)
        {
            return;
        }

        var data = CreateNotifyIconData();
        Shell_NotifyIcon(NimDelete, ref data);
        _trayAdded = false;
    }

    private NotifyIconData CreateNotifyIconData()
    {
        return new NotifyIconData
        {
            cbSize = (uint)Marshal.SizeOf<NotifyIconData>(),
            hWnd = _hwnd,
            uID = 1,
            szTip = string.Empty,
            szInfo = string.Empty,
            szInfoTitle = string.Empty
        };
    }

    private nint WndProc(nint hwnd, uint msg, nint wParam, nint lParam)
    {
        if (msg == WmTrayIcon)
        {
            var trayMessage = unchecked((uint)lParam.ToInt64());
            if (trayMessage == WmLButtonDblClk)
            {
                ShowMainWindow();
                return 0;
            }

            if (trayMessage is WmRButtonUp or WmContextMenu)
            {
                ShowContextMenu();
                return 0;
            }
        }

        return CallWindowProc(_oldWndProc, hwnd, msg, wParam, lParam);
    }

    private void ShowContextMenu()
    {
        if (_hwnd == 0 || !GetCursorPos(out var point))
        {
            return;
        }

        var menu = CreatePopupMenu();
        if (menu == 0)
        {
            return;
        }

        try
        {
            AppendMenu(menu, MfString, ShowCommand, "显示 YMhut Box");
            AppendMenu(menu, MfSeparator, 0, null);
            AppendMenu(menu, MfString, ExitCommand, "退出");
            SetForegroundWindow(_hwnd);
            var command = TrackPopupMenu(menu, TpmReturnCmd | TpmRightButton, point.X, point.Y, 0, _hwnd, 0);
            if (command == ShowCommand)
            {
                ShowMainWindow();
            }
            else if (command == ExitCommand)
            {
                Dispatch(() => ExitRequested?.Invoke(this, EventArgs.Empty));
            }

            PostMessage(_hwnd, WmNull, 0, 0);
        }
        finally
        {
            DestroyMenu(menu);
        }
    }

    private static Icon LoadIcon()
    {
        foreach (var path in new[]
        {
            Path.Combine(AppContext.BaseDirectory, "Assets", "app_icon.ico"),
            Path.Combine(AppContext.BaseDirectory, "Assets", "icons", "app_icon.ico"),
            Path.Combine(AppContext.BaseDirectory, "app_icon.ico")
        })
        {
            if (File.Exists(path))
            {
                return new Icon(path);
            }
        }

        return (Icon)SystemIcons.Application.Clone();
    }

    private void Dispatch(Action action)
    {
        if (_dispatcherQueue is not null && !_dispatcherQueue.HasThreadAccess)
        {
            _dispatcherQueue.TryEnqueue(() => action());
            return;
        }

        action();
    }

    private static AppWindow? GetAppWindow(Window window)
    {
        var hwnd = WindowNative.GetWindowHandle(window);
        var windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
        return AppWindow.GetFromWindowId(windowId);
    }

    private delegate nint WindowProc(nint hwnd, uint msg, nint wParam, nint lParam);

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private struct NotifyIconData
    {
        public uint cbSize;
        public nint hWnd;
        public uint uID;
        public uint uFlags;
        public uint uCallbackMessage;
        public nint hIcon;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
        public string szTip;
        public uint dwState;
        public uint dwStateMask;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
        public string szInfo;
        public uint uVersion;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
        public string szInfoTitle;
        public uint dwInfoFlags;
        public Guid guidItem;
        public nint hBalloonIcon;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct Point
    {
        public int X;
        public int Y;
    }

    [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern bool Shell_NotifyIcon(uint dwMessage, ref NotifyIconData lpData);

    [DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = true)]
    private static extern nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern nint CallWindowProc(nint lpPrevWndFunc, nint hWnd, uint msg, nint wParam, nint lParam);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern nint CreatePopupMenu();

    [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern bool AppendMenu(nint hMenu, uint uFlags, nuint uIDNewItem, string? lpNewItem);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern nuint TrackPopupMenu(nint hMenu, uint uFlags, int x, int y, int nReserved, nint hWnd, nint prcRect);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool DestroyMenu(nint hMenu);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool GetCursorPos(out Point lpPoint);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool SetForegroundWindow(nint hWnd);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool PostMessage(nint hWnd, uint msg, nint wParam, nint lParam);
}
