using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using HandyControl.Data;
using HandyControl.Tools;
using HandyControl.Tools.Interop;

namespace HandyControl.Controls
{
    public class NotifyIcon : FrameworkElement, IDisposable
    {
        private bool _isMouseOver;

        private bool _added;

        private readonly object _syncObj = new object();

        private readonly int _id;

        private ImageSource _icon;

        private IntPtr _iconCurrentHandle;

        private IntPtr _iconDefaultHandle;

        private IconHandle _iconHandle;

        private const int WmTrayMouseMessage = InteropValues.WM_USER + 1024;

        private string _windowClassName;

        private int _wmTaskbarCreated;

        private IntPtr _messageWindowHandle;

        private readonly InteropValues.WndProc _callback;

        private Popup _contextContent;

        private bool _doubleClick;

        private DispatcherTimer _dispatcherTimerBlink;

        private DispatcherTimer _dispatcherTimerPos;

        private bool _isTransparent;

        private bool _isDisposed;

        private static int NextId;

        private static readonly Dictionary<string, NotifyIcon> NotifyIconDic = new Dictionary<string, NotifyIcon>();

        static NotifyIcon()
        {
            VisibilityProperty.OverrideMetadata(typeof(NotifyIcon), new PropertyMetadata(Visibility.Visible, OnVisibilityChanged));
            DataContextProperty.OverrideMetadata(typeof(NotifyIcon), new FrameworkPropertyMetadata(DataContextPropertyChanged));
            ContextMenuProperty.OverrideMetadata(typeof(NotifyIcon), new FrameworkPropertyMetadata(ContextMenuPropertyChanged));
        }

        private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var ctl = (NotifyIcon) d;
            var v = (Visibility) e.NewValue;

            if (v == Visibility.Visible)
            {
                if (ctl._iconCurrentHandle == IntPtr.Zero)
                {
                    ctl.OnIconChanged();
                }
                ctl.UpdateIcon(true);
            }
            else if (ctl._iconCurrentHandle != IntPtr.Zero)
            {
                ctl.UpdateIcon(false);
            }
        }

        private static void DataContextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
            ((NotifyIcon) d).OnDataContextPropertyChanged(e);

        private void OnDataContextPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            UpdateDataContext(_contextContent, e.OldValue, e.NewValue);
            UpdateDataContext(ContextMenu, e.OldValue, e.NewValue);
        }

        private static void ContextMenuPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var ctl = (NotifyIcon) d;
            ctl.OnContextMenuPropertyChanged(e);
        }

        private void OnContextMenuPropertyChanged(DependencyPropertyChangedEventArgs e) =>
            UpdateDataContext((ContextMenu) e.NewValue, null, DataContext);

        public NotifyIcon()
        {
            _id = ++NextId;
            _callback = Callback;

            Loaded += (s, e) => Init();

            if (Application.Current != null) Application.Current.Exit += (s, e) => Dispose();
        }

        ~NotifyIcon() => Dispose(false);

        public void Init()
        {
            RegisterClass();
            if (Visibility == Visibility.Visible)
            {
                OnIconChanged();
                UpdateIcon(true);
            }

            _dispatcherTimerPos = new DispatcherTimer
            {
                Interval = TimeSpan.FromMilliseconds(200)
            };
            _dispatcherTimerPos.Tick += DispatcherTimerPos_Tick;
        }

        public static void Register(string token, NotifyIcon notifyIcon)
        {
            if (string.IsNullOrEmpty(token) || notifyIcon == null) return;
            NotifyIconDic[token] = notifyIcon;
        }

        public static void Unregister(string token, NotifyIcon notifyIcon)
        {
            if (string.IsNullOrEmpty(token) || notifyIcon == null) return;

            if (NotifyIconDic.ContainsKey(token))
            {
                if (ReferenceEquals(NotifyIconDic[token], notifyIcon))
                {
                    NotifyIconDic.Remove(token);
                }
            }
        }

        public static void Unregister(NotifyIcon notifyIcon)
        {
            if (notifyIcon == null) return;
            var first = NotifyIconDic.FirstOrDefault(item => ReferenceEquals(notifyIcon, item.Value));
            if (!string.IsNullOrEmpty(first.Key))
            {
                NotifyIconDic.Remove(first.Key);
            }
        }

        public static void Unregister(string token)
        {
            if (string.IsNullOrEmpty(token)) return;

            if (NotifyIconDic.ContainsKey(token))
            {
                NotifyIconDic.Remove(token);
            }
        }

        public static void ShowBalloonTip(string title, string content, NotifyIconInfoType infoType, string token)
        {
            if (NotifyIconDic.TryGetValue(token, out var notifyIcon))
            {
                notifyIcon.ShowBalloonTip(title, content, infoType);
            }
        }

        public void ShowBalloonTip(string title, string content, NotifyIconInfoType infoType)
        {
            if (!_added || DesignerHelper.IsInDesignMode) return;

            var data = new InteropValues.NOTIFYICONDATA
            {
                uFlags = InteropValues.NIF_INFO,
                hWnd = _messageWindowHandle,
                uID = _id,
                szInfoTitle = title ?? string.Empty,
                szInfo = content ?? string.Empty
            };

            switch (infoType)
            {
                case NotifyIconInfoType.Info:
                    data.dwInfoFlags = InteropValues.NIIF_INFO;
                    break;
                case NotifyIconInfoType.Warning:
                    data.dwInfoFlags = InteropValues.NIIF_WARNING;
                    break;
                case NotifyIconInfoType.Error:
                    data.dwInfoFlags = InteropValues.NIIF_ERROR;
                    break;
                case NotifyIconInfoType.None:
                    data.dwInfoFlags = InteropValues.NIIF_NONE;
                    break;
            }

            InteropMethods.Shell_NotifyIcon(InteropValues.NIM_MODIFY, data);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public void CloseContextControl()
        {
            if (_contextContent != null)
            {
                _contextContent.IsOpen = false;
            }
            else if (ContextMenu != null)
            {
                ContextMenu.IsOpen = false;
            }
        }

        public static readonly DependencyProperty TokenProperty = DependencyProperty.Register(
            "Token", typeof(string), typeof(NotifyIcon), new PropertyMetadata(default(string), OnTokenChanged));

        private static void OnTokenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is NotifyIcon notifyIcon)
            {
                if (e.NewValue == null)
                {
                    Unregister(notifyIcon);
                }
                else
                {
                    Register(e.NewValue.ToString(), notifyIcon);
                }
            }
        }

        public string Token
        {
            get => (string) GetValue(TokenProperty);
            set => SetValue(TokenProperty, value);
        }

        public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
            "Text", typeof(string), typeof(NotifyIcon), new PropertyMetadata(default(string)));

        public string Text
        {
            get => (string) GetValue(TextProperty);
            set => SetValue(TextProperty, value);
        }

        public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
            "Icon", typeof(ImageSource), typeof(NotifyIcon), new PropertyMetadata(default(ImageSource), OnIconChanged));

        private static void OnIconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var ctl = (NotifyIcon) d;
            ctl._icon = (ImageSource) e.NewValue;
            ctl.OnIconChanged();

            if (!string.IsNullOrEmpty(ctl._windowClassName) && !ctl.IsBlink && ctl.Visibility == Visibility.Visible)
            {
                ctl.UpdateIcon(true);
            }
        }

        public ImageSource Icon
        {
            get => (ImageSource) GetValue(IconProperty);
            set => SetValue(IconProperty, value);
        }

        public static readonly DependencyProperty ContextContentProperty = DependencyProperty.Register(
            "ContextContent", typeof(object), typeof(NotifyIcon), new PropertyMetadata(default(object)));

        public object ContextContent
        {
            get => GetValue(ContextContentProperty);
            set => SetValue(ContextContentProperty, value);
        }

        public static readonly DependencyProperty BlinkIntervalProperty = DependencyProperty.Register(
            "BlinkInterval", typeof(TimeSpan), typeof(NotifyIcon), new PropertyMetadata(TimeSpan.FromMilliseconds(500), OnBlinkIntervalChanged));

        private static void OnBlinkIntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var ctl = (NotifyIcon) d;
            if (ctl._dispatcherTimerBlink != null)
            {
                ctl._dispatcherTimerBlink.Interval = (TimeSpan) e.NewValue;
            }
        }

        public TimeSpan BlinkInterval
        {
            get => (TimeSpan) GetValue(BlinkIntervalProperty);
            set => SetValue(BlinkIntervalProperty, value);
        }

        public static readonly DependencyProperty IsBlinkProperty = DependencyProperty.Register(
            "IsBlink", typeof(bool), typeof(NotifyIcon), new PropertyMetadata(ValueBoxes.FalseBox, OnIsBlinkChanged));

        private static void OnIsBlinkChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var ctl = (NotifyIcon) d;
            if (ctl.Visibility != Visibility.Visible) return;
            if ((bool) e.NewValue)
            {
                if (ctl._dispatcherTimerBlink == null)
                {
                    ctl._dispatcherTimerBlink = new DispatcherTimer
                    {
                        Interval = ctl.BlinkInterval
                    };
                    ctl._dispatcherTimerBlink.Tick += ctl.DispatcherTimerBlinkTick;
                }
                ctl._dispatcherTimerBlink.Start();
            }
            else
            {
                ctl._dispatcherTimerBlink?.Stop();
                ctl._dispatcherTimerBlink = null;
                ctl.UpdateIcon(true);
            }
        }

        public bool IsBlink
        {
            get => (bool) GetValue(IsBlinkProperty);
            set => SetValue(IsBlinkProperty, ValueBoxes.BooleanBox(value));
        }

        private void DispatcherTimerBlinkTick(object sender, EventArgs e)
        {
            if (Visibility != Visibility.Visible || _iconCurrentHandle == IntPtr.Zero) return;
            UpdateIcon(true, !_isTransparent);
        }

        private bool CheckMouseIsEnter()
        {
            var isTrue = FindNotifyIcon(out var rectNotifyList);
            if (!isTrue) return false;

            InteropMethods.GetCursorPos(out var point);
            return rectNotifyList.Any(rectNotify =>
                point.X >= rectNotify.Left &&
                point.X <= rectNotify.Right &&
                point.Y >= rectNotify.Top &&
                point.Y <= rectNotify.Bottom);
        }

        private void DispatcherTimerPos_Tick(object sender, EventArgs e)
        {
            if (CheckMouseIsEnter())
            {
                if (!_isMouseOver)
                {
                    _isMouseOver = true;
                    RaiseEvent(new MouseEventArgs(Mouse.PrimaryDevice, Environment.TickCount)
                    {
                        RoutedEvent = MouseEnterEvent
                    });
                    _dispatcherTimerPos.Interval = TimeSpan.FromMilliseconds(500);
                }
            }
            else
            {
                _dispatcherTimerPos.Stop();
                _isMouseOver = false;
                RaiseEvent(new MouseEventArgs(Mouse.PrimaryDevice, Environment.TickCount)
                {
                    RoutedEvent = MouseLeaveEvent
                });
            }
        }

        //referenced from http://www.cnblogs.com/sczmzx/p/5158127.html
        private IntPtr FindTrayToolbarWindow()
        {
            var hWnd = InteropMethods.FindWindow("Shell_TrayWnd", null);
            if (hWnd != IntPtr.Zero)
            {
                hWnd = InteropMethods.FindWindowEx(hWnd, IntPtr.Zero, "TrayNotifyWnd", null);
                if (hWnd != IntPtr.Zero)
                {
                    hWnd = InteropMethods.FindWindowEx(hWnd, IntPtr.Zero, "SysPager", null);
                    if (hWnd != IntPtr.Zero)
                    {
                        hWnd = InteropMethods.FindWindowEx(hWnd, IntPtr.Zero, "ToolbarWindow32", null);
                    }
                }
            }

            return hWnd;
        }

        //referenced from http://www.cnblogs.com/sczmzx/p/5158127.html
        private IntPtr FindTrayToolbarOverFlowWindow()
        {
            var hWnd = InteropMethods.FindWindow("NotifyIconOverflowWindow", null);
            if (hWnd != IntPtr.Zero)
            {
                hWnd = InteropMethods.FindWindowEx(hWnd, IntPtr.Zero, "ToolbarWindow32", null);
            }
            return hWnd;
        }

        private bool FindNotifyIcon(out List<InteropValues.RECT> rectList)
        {
            var rectNotifyList = new List<InteropValues.RECT>();
            var hTrayWnd = FindTrayToolbarWindow();
            var isTrue = FindNotifyIcon(hTrayWnd, ref rectNotifyList);
            if (!isTrue)
            {
                hTrayWnd = FindTrayToolbarOverFlowWindow();
                isTrue = FindNotifyIcon(hTrayWnd, ref rectNotifyList);
            }
            rectList = rectNotifyList;
            return isTrue;
        }

        //referenced from http://www.cnblogs.com/sczmzx/p/5158127.html
        private bool FindNotifyIcon(IntPtr hTrayWnd, ref List<InteropValues.RECT> rectNotifyList)
        {
            InteropMethods.GetWindowRect(hTrayWnd, out var rectTray);
            var count = (int) InteropMethods.SendMessage(hTrayWnd, InteropValues.TB_BUTTONCOUNT, 0, IntPtr.Zero);

            var isFind = false;
            if (count > 0)
            {
                InteropMethods.GetWindowThreadProcessId(hTrayWnd, out var trayPid);
                var hProcess = InteropMethods.OpenProcess(InteropValues.ProcessAccess.VMOperation | InteropValues.ProcessAccess.VMRead | InteropValues.ProcessAccess.VMWrite, false, trayPid);
                var address = InteropMethods.VirtualAllocEx(hProcess, IntPtr.Zero, 1024, InteropValues.AllocationType.Commit, InteropValues.MemoryProtection.ReadWrite);

                var btnData = new InteropValues.TBBUTTON();
                var trayData = new InteropValues.TRAYDATA();
                var handle = Process.GetCurrentProcess().Id;

                for (uint i = 0; i < count; i++)
                {
                    InteropMethods.SendMessage(hTrayWnd, InteropValues.TB_GETBUTTON, i, address);
                    var isTrue = InteropMethods.ReadProcessMemory(hProcess, address, out btnData, Marshal.SizeOf(btnData), out _);
                    if (!isTrue) continue;

                    if (btnData.dwData == IntPtr.Zero)
                    {
                        btnData.dwData = btnData.iString;
                    }

                    InteropMethods.ReadProcessMemory(hProcess, btnData.dwData, out trayData, Marshal.SizeOf(trayData), out _);
                    InteropMethods.GetWindowThreadProcessId(trayData.hwnd, out var dwProcessId);

                    if (dwProcessId == (uint) handle)
                    {
                        var rect = new InteropValues.RECT();
                        var lngRect = InteropMethods.VirtualAllocEx(hProcess, IntPtr.Zero, Marshal.SizeOf(typeof(Rect)), InteropValues.AllocationType.Commit, InteropValues.MemoryProtection.ReadWrite);

                        InteropMethods.SendMessage(hTrayWnd, InteropValues.TB_GETITEMRECT, i, lngRect);
                        InteropMethods.ReadProcessMemory(hProcess, lngRect, out rect, Marshal.SizeOf(rect), out _);

                        InteropMethods.VirtualFreeEx(hProcess, lngRect, Marshal.SizeOf(rect), InteropValues.FreeType.Decommit);
                        InteropMethods.VirtualFreeEx(hProcess, lngRect, 0, InteropValues.FreeType.Release);

                        var left = rectTray.Left + rect.Left;
                        var top = rectTray.Top + rect.Top;
                        var botton = rectTray.Top + rect.Bottom;
                        var right = rectTray.Left + rect.Right;
                        rectNotifyList.Add(new InteropValues.RECT
                        {
                            Left = left,
                            Right = right,
                            Top = top,
                            Bottom = botton
                        });
                        isFind = true;
                    }
                }
                InteropMethods.VirtualFreeEx(hProcess, address, 0x4096, InteropValues.FreeType.Decommit);
                InteropMethods.VirtualFreeEx(hProcess, address, 0, InteropValues.FreeType.Release);
                InteropMethods.CloseHandle(hProcess);
            }
            return isFind;
        }

        private void OnIconChanged()
        {
            if (_windowClassName == null) return;

            if (_icon != null)
            {
                IconHelper.GetIconHandlesFromImageSource(_icon, out _, out _iconHandle);
                _iconCurrentHandle = _iconHandle.CriticalGetHandle();
            }
            else
            {
                if (_iconDefaultHandle == IntPtr.Zero)
                {
                    IconHelper.GetDefaultIconHandles(out _, out _iconHandle);
                    _iconDefaultHandle = _iconHandle.CriticalGetHandle();
                }
                _iconCurrentHandle = _iconDefaultHandle;
            }
        }

        private void UpdateIcon(bool showIconInTray, bool isTransparent = false)
        {
            lock (_syncObj)
            {
                if (DesignerHelper.IsInDesignMode) return;

                _isTransparent = isTransparent;
                var data = new InteropValues.NOTIFYICONDATA
                {
                    uCallbackMessage = WmTrayMouseMessage,
                    uFlags = InteropValues.NIF_MESSAGE | InteropValues.NIF_ICON | InteropValues.NIF_TIP,
                    hWnd = _messageWindowHandle,
                    uID = _id,
                    dwInfoFlags = InteropValues.NIF_TIP,
                    hIcon = isTransparent ? IntPtr.Zero : _iconCurrentHandle,
                    szTip = Text
                };

                if (showIconInTray)
                {
                    if (!_added)
                    {
                        InteropMethods.Shell_NotifyIcon(InteropValues.NIM_ADD, data);
                        _added = true;
                    }
                    else
                    {
                        InteropMethods.Shell_NotifyIcon(InteropValues.NIM_MODIFY, data);
                    }
                }
                else if (_added)
                {
                    InteropMethods.Shell_NotifyIcon(InteropValues.NIM_DELETE, data);
                    _added = false;
                }
            }
        }

        private void RegisterClass()
        {
            _windowClassName = $"HandyControl.Controls.NotifyIcon{Guid.NewGuid()}";
            var wndclass = new InteropValues.WNDCLASS4ICON
            {
                style = 0,
                lpfnWndProc = _callback,
                cbClsExtra = 0,
                cbWndExtra = 0,
                hInstance = IntPtr.Zero,
                hIcon = IntPtr.Zero,
                hCursor = IntPtr.Zero,
                hbrBackground = IntPtr.Zero,
                lpszMenuName = "",
                lpszClassName = _windowClassName
            };

            InteropMethods.RegisterClass(wndclass);
            _wmTaskbarCreated = InteropMethods.RegisterWindowMessage("TaskbarCreated");
            _messageWindowHandle = InteropMethods.CreateWindowEx(0, _windowClassName, "", 0, 0, 0, 1, 1,
                IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
        }

        private IntPtr Callback(IntPtr hWnd, int msg, IntPtr wparam, IntPtr lparam)
        {
            if (IsLoaded)
            {
                if (msg == _wmTaskbarCreated)
                {
                    if (_messageWindowHandle == hWnd && Visibility == Visibility.Visible)
                    {
                        UpdateIcon(true);
                    }
                }
                else
                {
                    switch (lparam.ToInt64())
                    {
                        case InteropValues.WM_LBUTTONDBLCLK:
                            WmMouseDown(MouseButton.Left, 2);
                            break;
                        case InteropValues.WM_LBUTTONUP:
                            WmMouseUp(MouseButton.Left);
                            break;
                        case InteropValues.WM_RBUTTONUP:
                            ShowContextMenu();
                            WmMouseUp(MouseButton.Right);
                            break;
                        case InteropValues.WM_MOUSEMOVE:
                            if (!_dispatcherTimerPos.IsEnabled)
                            {
                                _dispatcherTimerPos.Interval = TimeSpan.FromMilliseconds(200);
                                _dispatcherTimerPos.Start();
                            }
                            break;
                    }
                }
            }

            return InteropMethods.DefWindowProc(hWnd, msg, wparam, lparam);
        }

        private void WmMouseDown(MouseButton button, int clicks)
        {
            if (clicks == 2)
            {
                RaiseEvent(new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, button)
                {
                    RoutedEvent = MouseDoubleClickEvent
                });
                _doubleClick = true;
            }
        }

        private void WmMouseUp(MouseButton button)
        {
            if (!_doubleClick && button == MouseButton.Left)
            {
                RaiseEvent(new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, button)
                {
                    RoutedEvent = ClickEvent
                });
            }
            _doubleClick = false;
        }

        private void ShowContextMenu()
        {

            if (ContextContent != null)
            {
                _contextContent ??= new Popup
                {
                    Placement = PlacementMode.Mouse,
                    AllowsTransparency = true,
                    StaysOpen = false,
                    UseLayoutRounding = true,
                    SnapsToDevicePixels = true
                };

                _contextContent.Child = new ContentControl
                {
                    Content = ContextContent
                };
                _contextContent.IsOpen = true;
                InteropMethods.SetForegroundWindow(_contextContent.Child.GetHandle());
            }
            else if (ContextMenu != null)
            {
                if (ContextMenu.Items.Count == 0) return;

                ContextMenu.InvalidateProperty(StyleProperty);
                foreach (var item in ContextMenu.Items)
                {
                    if (item is MenuItem menuItem)
                    {
                        menuItem.InvalidateProperty(StyleProperty);
                    }
                    else
                    {
                        var container = ContextMenu.ItemContainerGenerator.ContainerFromItem(item) as MenuItem;
                        container?.InvalidateProperty(StyleProperty);
                    }
                }

                ContextMenu.Placement = PlacementMode.Mouse;
                ContextMenu.IsOpen = true;

                InteropMethods.SetForegroundWindow(ContextMenu.GetHandle());
            }
        }

        public static readonly RoutedEvent ClickEvent =
            EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble,
                typeof(RoutedEventHandler), typeof(NotifyIcon));

        public event RoutedEventHandler Click
        {
            add => AddHandler(ClickEvent, value);
            remove => RemoveHandler(ClickEvent, value);
        }

        public static readonly RoutedEvent MouseDoubleClickEvent =
            EventManager.RegisterRoutedEvent("MouseDoubleClick", RoutingStrategy.Bubble,
                typeof(RoutedEventHandler), typeof(NotifyIcon));

        public event RoutedEventHandler MouseDoubleClick
        {
            add => AddHandler(MouseDoubleClickEvent, value);
            remove => RemoveHandler(MouseDoubleClickEvent, value);
        }

        private void UpdateDataContext(FrameworkElement target, object oldValue, object newValue)
        {
            if (target == null || BindingOperations.GetBindingExpression(target, DataContextProperty) != null) return;
            if (ReferenceEquals(this, target.DataContext) || Equals(oldValue, target.DataContext))
            {
                target.DataContext = newValue ?? this;
            }
        }

        private void Dispose(bool disposing)
        {
            if (_isDisposed) return;
            if (disposing)
            {
                if (_dispatcherTimerBlink != null && IsBlink)
                {
                    _dispatcherTimerBlink.Stop();
                }
                UpdateIcon(false);
            }

            _isDisposed = true;
        }
    }
}
