using System;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using System.Windows;

namespace HandyControl.Interactivity
{
    public abstract class EventTriggerBase : TriggerBase
    {
        public static readonly DependencyProperty SourceNameProperty = DependencyProperty.Register("SourceName",
            typeof(string), typeof(EventTriggerBase), new PropertyMetadata(OnSourceNameChanged));

        public static readonly DependencyProperty SourceObjectProperty = DependencyProperty.Register("SourceObject",
            typeof(object), typeof(EventTriggerBase), new PropertyMetadata(OnSourceObjectChanged));

        private MethodInfo _eventHandlerMethodInfo;

        internal EventTriggerBase(Type sourceTypeConstraint) : base(typeof(DependencyObject))
        {
            SourceTypeConstraint = sourceTypeConstraint;
            SourceNameResolver = new NameResolver();
            RegisterSourceChanged();
        }

        protected sealed override Type AssociatedObjectTypeConstraint
        {
            get
            {
                if (TypeDescriptor.GetAttributes(GetType())[typeof(TypeConstraintAttribute)] is TypeConstraintAttribute attribute)
                    return attribute.Constraint;
                return typeof(DependencyObject);
            }
        }

        private bool IsLoadedRegistered { get; set; }

        private bool IsSourceChangedRegistered { get; set; }

        private bool IsSourceNameSet
        {
            get
            {
                if (string.IsNullOrEmpty(SourceName))
                    return ReadLocalValue(SourceNameProperty) != DependencyProperty.UnsetValue;
                return true;
            }
        }

        public object Source
        {
            get
            {
                object associatedObject = AssociatedObject;
                if (SourceObject != null)
                    return SourceObject;
                if (IsSourceNameSet)
                {
                    associatedObject = SourceNameResolver.Object;
                    if (associatedObject != null && !SourceTypeConstraint.IsInstanceOfType(associatedObject))
                        throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
                            ExceptionStringTable.RetargetedTypeConstraintViolatedExceptionMessage, GetType().Name,
                            associatedObject.GetType(), SourceTypeConstraint, "Source"));
                }
                return associatedObject;
            }
        }

        public string SourceName
        {
            get =>
                (string) GetValue(SourceNameProperty);
            set => SetValue(SourceNameProperty, value);
        }

        private NameResolver SourceNameResolver { get; }

        public object SourceObject
        {
            get =>
                GetValue(SourceObjectProperty);
            set => SetValue(SourceObjectProperty, value);
        }

        protected Type SourceTypeConstraint { get; }

        protected abstract string GetEventName();

        private static bool IsValidEvent(EventInfo eventInfo)
        {
            var eventHandlerType = eventInfo.EventHandlerType;
            if (!typeof(Delegate).IsAssignableFrom(eventInfo.EventHandlerType))
                return false;
            var parameters = eventHandlerType.GetMethod("Invoke")?.GetParameters();
            return parameters != null && parameters.Length == 2 && typeof(object).IsAssignableFrom(parameters[0].ParameterType) && typeof(EventArgs).IsAssignableFrom(parameters[1].ParameterType);
        }

        protected override void OnAttached()
        {
            base.OnAttached();
            var associatedObject = AssociatedObject;
            var behavior = associatedObject as Behavior;
            var element = associatedObject as FrameworkElement;
            RegisterSourceChanged();
            if (behavior != null)
            {
                behavior.AssociatedObjectChanged += OnBehaviorHostChanged;
            }
            else if (SourceObject != null || element == null)
            {
                try
                {
                    OnSourceChanged(null, Source);
                }
                catch (InvalidOperationException)
                {
                }
            }
            else
            {
                SourceNameResolver.NameScopeReferenceElement = element;
            }
            if (string.Compare(GetEventName(), "Loaded", StringComparison.Ordinal) == 0 && element != null &&
                !Interaction.IsElementLoaded(element))
                RegisterLoaded(element);
        }

        private void OnBehaviorHostChanged(object sender, EventArgs e)
        {
            SourceNameResolver.NameScopeReferenceElement =
                ((IAttachedObject) sender).AssociatedObject as FrameworkElement;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            var associatedObject = AssociatedObject as Behavior;
            var associatedElement = AssociatedObject as FrameworkElement;
            try
            {
                OnSourceChanged(Source, null);
            }
            catch (InvalidOperationException)
            {
            }
            UnregisterSourceChanged();
            if (associatedObject != null)
                associatedObject.AssociatedObjectChanged -= OnBehaviorHostChanged;
            SourceNameResolver.NameScopeReferenceElement = null;
            if (string.Compare(GetEventName(), "Loaded", StringComparison.Ordinal) == 0 && associatedElement != null)
                UnregisterLoaded(associatedElement);
        }

        protected virtual void OnEvent(EventArgs eventArgs)
        {
            InvokeActions(eventArgs);
        }

        private void OnEventImpl(object sender, EventArgs eventArgs)
        {
            OnEvent(eventArgs);
        }

        internal void OnEventNameChanged(string oldEventName, string newEventName)
        {
            if (AssociatedObject != null)
            {
                var source = Source as FrameworkElement;
                if (source != null && string.Compare(oldEventName, "Loaded", StringComparison.Ordinal) == 0)
                    UnregisterLoaded(source);
                else if (!string.IsNullOrEmpty(oldEventName))
                    UnregisterEvent(Source, oldEventName);
                if (source != null && string.Compare(newEventName, "Loaded", StringComparison.Ordinal) == 0)
                    RegisterLoaded(source);
                else if (!string.IsNullOrEmpty(newEventName))
                    RegisterEvent(Source, newEventName);
            }
        }

        private void OnSourceChanged(object oldSource, object newSource)
        {
            if (AssociatedObject != null)
                OnSourceChangedImpl(oldSource, newSource);
        }

        internal virtual void OnSourceChangedImpl(object oldSource, object newSource)
        {
            if (!string.IsNullOrEmpty(GetEventName()) &&
                string.Compare(GetEventName(), "Loaded", StringComparison.Ordinal) != 0)
            {
                if (oldSource != null && SourceTypeConstraint.IsInstanceOfType(oldSource))
                    UnregisterEvent(oldSource, GetEventName());
                if (newSource != null && SourceTypeConstraint.IsInstanceOfType(newSource))
                    RegisterEvent(newSource, GetEventName());
            }
        }

        private static void OnSourceNameChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            var base2 = (EventTriggerBase) obj;
            base2.SourceNameResolver.Name = (string) args.NewValue;
        }

        private void OnSourceNameResolverElementChanged(object sender, NameResolvedEventArgs e)
        {
            if (SourceObject == null)
                OnSourceChanged(e.OldObject, e.NewObject);
        }

        private static void OnSourceObjectChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            var base2 = (EventTriggerBase) obj;
            object newSource = base2.SourceNameResolver.Object;
            if (args.NewValue == null)
            {
                base2.OnSourceChanged(args.OldValue, newSource);
            }
            else
            {
                if (args.OldValue == null && newSource != null)
                    base2.UnregisterEvent(newSource, base2.GetEventName());
                base2.OnSourceChanged(args.OldValue, args.NewValue);
            }
        }

        private void RegisterEvent(object obj, string eventName)
        {
            var eventInfo = obj.GetType().GetEvent(eventName);
            if (eventInfo == null)
            {
                if (SourceObject != null)
                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                        ExceptionStringTable.EventTriggerCannotFindEventNameExceptionMessage,
                        new object[] {eventName, obj.GetType().Name}));
            }
            else if (!IsValidEvent(eventInfo))
            {
                if (SourceObject != null)
                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                        ExceptionStringTable.EventTriggerBaseInvalidEventExceptionMessage,
                        new object[] {eventName, obj.GetType().Name}));
            }
            else
            {
                _eventHandlerMethodInfo =
                    typeof(EventTriggerBase).GetMethod("OnEventImpl", BindingFlags.NonPublic | BindingFlags.Instance);
                eventInfo.AddEventHandler(obj,
                    Delegate.CreateDelegate(eventInfo.EventHandlerType, this, _eventHandlerMethodInfo ?? throw new InvalidOperationException()));
            }
        }

        private void RegisterLoaded(FrameworkElement associatedElement)
        {
            if (!IsLoadedRegistered && associatedElement != null)
            {
                associatedElement.Loaded += OnEventImpl;
                IsLoadedRegistered = true;
            }
        }

        private void RegisterSourceChanged()
        {
            if (!IsSourceChangedRegistered)
            {
                SourceNameResolver.ResolvedElementChanged += OnSourceNameResolverElementChanged;
                IsSourceChangedRegistered = true;
            }
        }

        private void UnregisterEvent(object obj, string eventName)
        {
            if (string.Compare(eventName, "Loaded", StringComparison.Ordinal) == 0)
            {
                if (obj is FrameworkElement associatedElement)
                    UnregisterLoaded(associatedElement);
            }
            else
            {
                UnregisterEventImpl(obj, eventName);
            }
        }

        private void UnregisterEventImpl(object obj, string eventName)
        {
            var type = obj.GetType();
            if (_eventHandlerMethodInfo != null)
            {
                var info = type.GetEvent(eventName);
                info.RemoveEventHandler(obj,
                    Delegate.CreateDelegate(info.EventHandlerType, this, _eventHandlerMethodInfo));
                _eventHandlerMethodInfo = null;
            }
        }

        private void UnregisterLoaded(FrameworkElement associatedElement)
        {
            if (IsLoadedRegistered && associatedElement != null)
            {
                associatedElement.Loaded -= OnEventImpl;
                IsLoadedRegistered = false;
            }
        }

        private void UnregisterSourceChanged()
        {
            if (IsSourceChangedRegistered)
            {
                SourceNameResolver.ResolvedElementChanged -= OnSourceNameResolverElementChanged;
                IsSourceChangedRegistered = false;
            }
        }
    }
}