using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;

namespace NHotkey.Wpf
{
    [System.Diagnostics.CodeAnalysis.SuppressMessage(
        "Microsoft.Design",
        "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
        Justification = "This is a singleton; disposing it would break it")]
    public class HotkeyManager : HotkeyManagerBase
    {
        #region Singleton implementation

        public static HotkeyManager Current { get { return LazyInitializer.Instance; } }

        private static class LazyInitializer
        {
            static LazyInitializer() { }
            public static readonly HotkeyManager Instance = new HotkeyManager();
        }

        public void AddOrReplace(string v, object incrementKeys, object onIncrement)
        {
            throw new NotImplementedException();
        }

        #endregion

        #region Attached property for KeyBindings

        [AttachedPropertyBrowsableForType(typeof(KeyBinding))]
        public static bool GetRegisterGlobalHotkey(KeyBinding binding)
        {
            return (bool)binding.GetValue(RegisterGlobalHotkeyProperty);
        }

        public static void SetRegisterGlobalHotkey(KeyBinding binding, bool value)
        {
            binding.SetValue(RegisterGlobalHotkeyProperty, value);
        }

        public static readonly DependencyProperty RegisterGlobalHotkeyProperty =
            DependencyProperty.RegisterAttached(
                "RegisterGlobalHotkey",
                typeof(bool),
                typeof(HotkeyManager),
                new PropertyMetadata(
                    false,
                    RegisterGlobalHotkeyPropertyChanged));

        private static void RegisterGlobalHotkeyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var keyBinding = d as KeyBinding;
            if (keyBinding == null)
                return;

            bool oldValue = (bool) e.OldValue;
            bool newValue = (bool) e.NewValue;

            if (DesignerProperties.GetIsInDesignMode(d))
                return;

            if (oldValue && !newValue)
            {
                Current.RemoveKeyBinding(keyBinding);
            }
            else if (newValue && !oldValue)
            {
                Current.AddKeyBinding(keyBinding);
            }
        }

        #endregion

        #region HotkeyAlreadyRegistered event

        public static event EventHandler<HotkeyAlreadyRegisteredEventArgs> HotkeyAlreadyRegistered;

        private static void OnHotkeyAlreadyRegistered(string name)
        {
            var handler = HotkeyAlreadyRegistered;
            if (handler != null)
                handler(null, new HotkeyAlreadyRegisteredEventArgs(name));
        }

        #endregion

        // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
        private readonly HwndSource _source;
        private readonly WeakReferenceCollection<KeyBinding> _keyBindings;

        private HotkeyManager()
        {
            _keyBindings = new WeakReferenceCollection<KeyBinding>();

            var parameters = new HwndSourceParameters("Hotkey sink")
                             {
                                 HwndSourceHook = HandleMessage,
                                 ParentWindow = HwndMessage
                             };
            _source = new HwndSource(parameters);
            SetHwnd(_source.Handle);
        }

        public void AddOrReplace(string name, KeyGesture gesture, EventHandler<HotkeyEventArgs> handler)
        {
            AddOrReplace(name, gesture, false, handler);
        }

        public void AddOrReplace(string name, KeyGesture gesture, bool noRepeat, EventHandler<HotkeyEventArgs> handler)
        {
            AddOrReplace(name, gesture.Key, gesture.Modifiers, noRepeat, handler);
        }

        public void AddOrReplace(string name, Key key, ModifierKeys modifiers, EventHandler<HotkeyEventArgs> handler)
        {
            AddOrReplace(name, key, modifiers, false, handler);
        }

        public void AddOrReplace(string name, Key key, ModifierKeys modifiers, bool noRepeat, EventHandler<HotkeyEventArgs> handler)
        {
            var flags = GetFlags(modifiers, noRepeat);
            var vk = (uint)KeyInterop.VirtualKeyFromKey(key);
            AddOrReplace(name, vk, flags, handler);
        }

        private static HotkeyFlags GetFlags(ModifierKeys modifiers, bool noRepeat)
        {
            var flags = HotkeyFlags.None;
            if (modifiers.HasFlag(ModifierKeys.Shift))
                flags |= HotkeyFlags.Shift;
            if (modifiers.HasFlag(ModifierKeys.Control))
                flags |= HotkeyFlags.Control;
            if (modifiers.HasFlag(ModifierKeys.Alt))
                flags |= HotkeyFlags.Alt;
            if (modifiers.HasFlag(ModifierKeys.Windows))
                flags |= HotkeyFlags.Windows;
            if (noRepeat)
                flags |= HotkeyFlags.NoRepeat;
            return flags;
        }

        private static ModifierKeys GetModifiers(HotkeyFlags flags)
        {
            var modifiers = ModifierKeys.None;
            if (flags.HasFlag(HotkeyFlags.Shift))
                modifiers |= ModifierKeys.Shift;
            if (flags.HasFlag(HotkeyFlags.Control))
                modifiers |= ModifierKeys.Control;
            if (flags.HasFlag(HotkeyFlags.Alt))
                modifiers |= ModifierKeys.Alt;
            if (flags.HasFlag(HotkeyFlags.Windows))
                modifiers |= ModifierKeys.Windows;
            return modifiers;
        }

        private void AddKeyBinding(KeyBinding keyBinding)
        {
            var gesture = (KeyGesture)keyBinding.Gesture;
            string name = GetNameForKeyBinding(gesture);
            try
            {
                AddOrReplace(name, gesture.Key, gesture.Modifiers, null);
                _keyBindings.Add(keyBinding);
            }
            catch (HotkeyAlreadyRegisteredException)
            {
                OnHotkeyAlreadyRegistered(name);
            }
        }

        private void RemoveKeyBinding(KeyBinding keyBinding)
        {
            var gesture = (KeyGesture)keyBinding.Gesture;
            string name = GetNameForKeyBinding(gesture);
            Remove(name);
            _keyBindings.Remove(keyBinding);
        }

        private readonly KeyGestureConverter _gestureConverter = new KeyGestureConverter();
        private string GetNameForKeyBinding(KeyGesture gesture)
        {
            string name = gesture.DisplayString;
            if (string.IsNullOrEmpty(name))
                name = _gestureConverter.ConvertToString(gesture);
            return name;
        }

        private IntPtr HandleMessage(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled)
        {
            Hotkey hotkey;
            var result = HandleHotkeyMessage(hwnd, msg, wparam, lparam, ref handled, out hotkey);
            if (handled)
                return result;

            if (hotkey != null)
                handled = ExecuteBoundCommand(hotkey);

            return result;
        }

        private bool ExecuteBoundCommand(Hotkey hotkey)
        {
            var key = KeyInterop.KeyFromVirtualKey((int)hotkey.VirtualKey);
            var modifiers = GetModifiers(hotkey.Flags);
            bool handled = false;
            foreach (var binding in _keyBindings)
            {
                if (binding.Key == key && binding.Modifiers == modifiers)
                {
                    handled |= ExecuteCommand(binding);
                }
            }
            return handled;
        }

        private static bool ExecuteCommand(InputBinding binding)
        {
            var command = binding.Command;
            var parameter = binding.CommandParameter;
            var target = binding.CommandTarget;

            if (command == null)
                return false;

            var routedCommand = command as RoutedCommand;
            if (routedCommand != null)
            {
                if (routedCommand.CanExecute(parameter, target))
                {
                    routedCommand.Execute(parameter, target);
                    return true;
                }
            }
            else
            {
                if (command.CanExecute(parameter))
                {
                    command.Execute(parameter);
                    return true;
                }
            }
            return false;
        }
    }
}
