﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Rocket.UI
{
    /// <summary>
    /// An abstract base class for elements that form a tree.
    /// </summary>
    public abstract class Element
    {
        /// <summary>
        /// The value that indicates that a property has no value.
        /// </summary>
        private static readonly object NoValue = new object();

        /// <summary>
        /// The parent element.
        /// </summary>
        private Element parent;

        /// <summary>
        /// The data source.
        /// </summary>
        private object dataSource;

        /// <summary>
        /// The attachment entries.
        /// </summary>
        private Attachment.Entry[] entries;

        /// <summary>
        /// Gets the parent element.
        /// </summary>
        /// <value>The parent element.</value>
        public Element Parent
        {
            get
            {
                return this.parent;
            }

            set
            {
                if (this.parent != value)
                {
                    var oldParent = this.parent;
                    var oldDataSource = this.DataSource;

                    this.parent = value;
                    this.OnParentChanged(oldParent, value);

                    if (oldParent != null)
                    {
                        oldParent.OnChildChanged(this, null);
                    }

                    if (value != null)
                    {
                        value.OnChildChanged(null, this);
                    }

                    var newDataSource = this.DataSource;

                    if (oldDataSource != newDataSource)
                    {
                        this.OnDataSourceChanged(oldDataSource, newDataSource);
                    }
                }
            }
        }

        /// <summary>
        /// Gets the child elements.
        /// </summary>
        /// <value>The child elements.</value>
        public virtual IEnumerable Children
        {
            get { yield break; }
        }

        /// <summary>
        /// Gets or sets the data source.
        /// </summary>
        /// <value>The data source.</value>
        public object DataSource
        {
            get
            {
                if (this.dataSource != null)
                {
                    return dataSource;
                }

                if (this.Parent != null)
                {
                    return this.Parent.DataSource;
                }

                return null;
            }

            set
            {
                if (this.dataSource != value)
                {
                    var oldDataSource = this.dataSource;
                    this.dataSource = value;
                    this.OnDataSourceChanged(oldDataSource, value);
                }
            }
        }

        /// <summary>
        /// Gets the data bindings.
        /// </summary>
        /// <value>The data bindings.</value>
        private IEnumerable<DataBinding> DataBindings
        {
            get
            {
                if (this.entries != null)
                {
                    for (var i = 0; i < this.entries.Length; i++)
                    {
                        var binding = this.entries[i].Value as DataBinding;

                        if (binding != null)
                        {
                            yield return binding;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Element"/> class.
        /// </summary>
        public Element() { }

        /// <summary>
        /// Initializes a new instance of the <see cref="Element"/> class.
        /// </summary>
        /// <param name="setters">The property value setters.</param>
        public Element(params Setter[] setters)
        {
            if (setters != null)
            {
                foreach (var setter in setters)
                {
                    if (setter != null)
                    {
                        setter.SetValue(this);
                    }
                }
            }
        }

        /// <summary>
        /// Gets the property value.
        /// </summary>
        /// <typeparam name="TValue">The property value type.</typeparam>
        /// <param name="property">The property.</param>
        /// <returns>The property value.</returns>
        public TValue Get<TValue>(Property<TValue> property)
        {
            return (TValue)this.GetValue(property);
        }

        /// <summary>
        /// Sets the property value.
        /// </summary>
        /// <typeparam name="TValue">The property value type.</typeparam>
        /// <param name="property">The property.</param>
        /// <param name="value">The property value.</param>
        public void Set<TValue>(Property<TValue> property, TValue value)
        {
            this.SetValue(property, value);
        }

        /// <summary>
        /// Sets the property value binding.
        /// </summary>
        /// <typeparam name="TDataSource">The data source type.</typeparam>
        /// <typeparam name="TValue">The property value type.</typeparam>
        /// <param name="targetProperty">The target property.</param>
        /// <param name="sourceProperty">The data source property expression.</param>
        public void SetBinding<TDataSource, TValue>(Property<TValue> targetProperty, Expression<Func<TDataSource, TValue>> sourceProperty)
        {
            if (targetProperty == null)
            {
                throw new ArgumentNullException(nameof(targetProperty));
            }

            if (sourceProperty == null)
            {
                throw new ArgumentNullException(nameof(sourceProperty));
            }

            var sourcePropertyInfo = ((sourceProperty.Body as MemberExpression)?.Member as PropertyInfo);

            if (sourcePropertyInfo == null)
            {
                throw new ArgumentException("Expression must refer to a data source property.", nameof(sourceProperty));
            }

            new DataBinding<TDataSource, TValue>(targetProperty, sourcePropertyInfo).BindTo(this);
        }

        /// <summary>
        /// Sets the property value binding.
        /// </summary>
        /// <typeparam name="TParent">The parent element type.</typeparam>
        /// <typeparam name="TValue">The property value type.</typeparam>
        /// <param name="targetProperty">The target property.</param>
        /// <param name="sourceProperty">The source property.</param>
        public void SetBinding<TParent, TValue>(Property<TValue> targetProperty, Property<TValue> sourceProperty) where TParent : Element
        {
            if (targetProperty == null)
            {
                throw new ArgumentNullException(nameof(targetProperty));
            }

            if (sourceProperty == null)
            {
                throw new ArgumentNullException(nameof(sourceProperty));
            }

            new ParentBinding<TParent, TValue>(targetProperty, sourceProperty).BindTo(this);
        }

        /// <summary>
        /// Gets the property value.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <returns>The property value.</returns>
        public object GetValue(Property property)
        {
            if (property == null)
            {
                throw new ArgumentNullException(nameof(property));
            }

            var value = NoValue;

            for (var obj = this; obj != null && value == NoValue; obj = obj.Parent)
            {
                if (value == NoValue)
                {
                    value = property.GetValue(obj.entries, NoValue);

                    if (value is Binding)
                    {
                        value = (value as Binding).GetValue(this);
                    }
                }

                if (!property.HasOption(PropertyOptions.Inherited))
                {
                    break;
                }
            }

            for (var obj = this; obj != null && value == NoValue; obj = obj.Parent)
            {
                value = obj.GetDefaultValue(property);

                if (!property.HasOption(PropertyOptions.Inherited))
                {
                    break;
                }
            }

            if (value == NoValue)
            {
                value = property.DefaultValue;
            }

            return value;
        }

        /// <summary>
        /// Sets the property value.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <param name="value">The property value.</param>
        public void SetValue(Property property, object value)
        {
            if (property == null)
            {
                throw new ArgumentNullException(nameof(property));
            }

            var previousValue = property.GetValue(this.entries, NoValue);
            var binding = previousValue as Binding;

            if (binding != null)
            {
                binding.SetValue(this, value);
            }
            else if (!Equals(previousValue, value))
            {
                property.SetValue(ref this.entries, value);
                this.OnPropertyChanged(property, previousValue);
            }
        }

        /// <summary>
        /// Clears the property value.
        /// </summary>
        /// <param name="property">The property.</param>
        public void ClearValue(Property property)
        {
            if (property == null)
            {
                throw new ArgumentNullException(nameof(property));
            }

            var previousValue = property.ClearValue(this.entries, NoValue);

            if (previousValue != NoValue)
            {
                var binding = (previousValue as Binding);

                if (binding != null)
                {
                    binding.Unbind(this);
                }
                else
                {
                    this.OnPropertyChanged(property, previousValue);
                }
            }
        }

        /// <summary>
        /// Adds the event handler.
        /// </summary>
        /// <typeparam name="TEventArgs">The event argument type.</typeparam>
        /// <param name="event">The event.</param>
        /// <param name="eventHandler">The event handler.</param>
        public void AddHandler<TEventArgs>(Event<TEventArgs> @event, EventHandler<TEventArgs> eventHandler) where TEventArgs : HandledEventArgs
        {
            if (@event == null)
            {
                throw new ArgumentNullException(nameof(@event));
            }

            @event.SetValue(ref this.entries, (@event.GetValue(this.entries, null) as EventHandler<TEventArgs>) + eventHandler);
        }

        /// <summary>
        /// Removes the event handler.
        /// </summary>
        /// <typeparam name="TEventArgs">The event argument type.</typeparam>
        /// <param name="event">The event.</param>
        /// <param name="eventHandler">The event handler.</param>
        public void RemoveHandler<TEventArgs>(Event<TEventArgs> @event, EventHandler<TEventArgs> eventHandler) where TEventArgs : HandledEventArgs
        {
            if (@event == null)
            {
                throw new ArgumentNullException(nameof(@event));
            }

            @event.SetValue(ref this.entries, (@event.GetValue(this.entries, null) as EventHandler<TEventArgs>) - eventHandler);
        }

        /// <summary>
        /// Raises the event.
        /// </summary>
        /// <typeparam name="TEventArgs">The event argument type.</typeparam>
        /// <param name="event">The event.</param>
        /// <param name="eventArguments">The event arguments.</param>
        public void RaiseEvent<TEventArgs>(Event<TEventArgs> @event, TEventArgs eventArguments) where TEventArgs : HandledEventArgs
        {
            if (@event == null)
            {
                throw new ArgumentNullException(nameof(@event));
            }

            if (eventArguments == null)
            {
                throw new ArgumentNullException(nameof(eventArguments));
            }

            this.RaiseEvent(@event, this, eventArguments);
        }

        /// <summary>
        /// Raises the events.
        /// </summary>
        /// <typeparam name="TEventArgs">The event argument type.</typeparam>
        /// <param name="previewEvent">The preview event.</param>
        /// <param name="event">The event.</param>
        /// <param name="eventArguments">The event arguments.</param>
        public void RaiseEvent<TEventArgs>(Event<TEventArgs> previewEvent, Event<TEventArgs> @event, TEventArgs eventArguments) where TEventArgs : HandledEventArgs
        {
            if (previewEvent == null)
            {
                throw new ArgumentNullException(nameof(previewEvent));
            }

            if (@event == null)
            {
                throw new ArgumentNullException(nameof(@event));
            }

            if (eventArguments == null)
            {
                throw new ArgumentNullException(nameof(eventArguments));
            }

            if (previewEvent.Routing != EventRouting.Tunnel)
            {
                throw new ArgumentException("Preview event must be tunneling.", nameof(previewEvent));
            }

            if (@event.Routing == EventRouting.Tunnel)
            {
                throw new ArgumentException("The event cannot be tunneling.", nameof(@event));
            }

            this.RaiseEvent(previewEvent, this, eventArguments);
            this.RaiseEvent(@event, this, eventArguments);
        }

        /// <summary>
        /// Finds the child elements that match the condition.
        /// </summary>
        /// <typeparam name="TElement">The element type.</typeparam>
        /// <param name="condition">The condition predicate. Null matches all elements.</param>
        /// <returns>The matching elements.</returns>
        public IEnumerable<TElement> FindChild<TElement>(Func<TElement, bool> condition = null) where TElement : class
        {
            foreach (Element child in this.Children)
            {
                if (child.Match(condition))
                {
                    yield return child as TElement;
                }

                foreach (var item in child.FindChild(condition))
                {
                    yield return item;
                }
            }
        }

        /// <summary>
        /// Finds the parent elements that match the condition.
        /// </summary>
        /// <typeparam name="TElement">The element type.</typeparam>
        /// <param name="condition">The condition predicate. Null matches all elements.</param>
        /// <returns>The matching elements.</returns>
        public IEnumerable<TElement> FindParent<TElement>(Func<TElement, bool> condition = null) where TElement : class
        {
            for (var parent = this.Parent; parent != null; parent = parent.Parent)
            {
                if (parent.Match(condition))
                {
                    yield return parent as TElement;
                }
            }
        }

        /// <summary>
        /// Invalidates the element.
        /// </summary>
        public void Invalidate()
        {
            this.OnInvalidate(this);
        }

        /// <summary>
        /// Called when an element is invalidated.
        /// </summary>
        /// <param name="element">The invalidated element.</param>
        protected virtual void OnInvalidate(Element element)
        {
            this.Parent?.OnInvalidate(element);
        }

        /// <summary>
        /// Attaches a child element.
        /// </summary>
        /// <param name="child">The child element.</param>
        protected void AttachChild(Element child)
        {
            if (child == null)
            {
                throw new ArgumentNullException(nameof(child));
            }

            if (child.Parent != null)
            {
                throw new InvalidOperationException("The child element is already a part of another tree.");
            }

            child.Parent = this;
        }

        /// <summary>
        /// Detaches a child element.
        /// </summary>
        /// <param name="child">The child element.</param>
        protected void DetachChild(Element child)
        {
            if (child == null)
            {
                throw new ArgumentNullException(nameof(child));
            }

            if (child.Parent != this)
            {
                throw new InvalidOperationException("The child element is not a part of this tree.");
            }

            child.Parent = null;
        }

        /// <summary>
        /// Called when the parent element is changed.
        /// </summary>
        /// <param name="oldParent">The old parent element.</param>
        /// <param name="newParent">The new parent element.</param>
        protected virtual void OnParentChanged(Element oldParent, Element newParent)
        {
            foreach (Element child in this.Children)
            {
                child.OnParentChanged(oldParent, newParent);
            }
        }

        /// <summary>
        /// Called when a child element is added to or removed from the tree.
        /// </summary>
        /// <param name="removed">The removed element. Null if nothing was removed.</param>
        /// <param name="added">The added element. Null if nothing was added.</param>
        protected virtual void OnChildChanged(Element removed, Element added)
        {
            this.Parent?.OnChildChanged(removed, added);
        }

        /// <summary>
        /// Gets the default value for the property.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <returns>The default value for the property.</returns>
        protected virtual object GetDefaultValue(Property property)
        {
            return NoValue;
        }

        /// <summary>
        /// Called when a property value is changed.
        /// </summary>
        /// <param name="source">The source element.</param>
        /// <param name="property">The property.</param>
        /// <param name="previousValue">The previous value.</param>
        protected virtual void OnPropertyChanged(Element source, Property property, object previousValue)
        {
        }

        /// <summary>
        /// Called when a property value change affects the parent element.
        /// </summary>
        /// <param name="source">The source element.</param>
        /// <param name="property">The property.</param>
        /// <param name="previousValue">The previous value.</param>
        protected virtual void OnParentAffected(Element source, Property property, object previousValue)
        {
            if (this.Parent != null)
            {
                this.Parent.OnPropertyChanged(source, property, previousValue);
                this.Parent.OnParentAffected(source, property, previousValue);
            }
        }

        /// <summary>
        /// Called when a property value change affects the child elements.
        /// </summary>
        /// <param name="source">The source element.</param>
        /// <param name="property">The property.</param>
        /// <param name="previousValue">The previous value.</param>
        protected virtual void OnChildAffected(Element source, Property property, object previousValue)
        {
            foreach (Element child in this.Children)
            {
                child.OnPropertyChanged(source, property, previousValue);
                child.OnChildAffected(source, property, previousValue);
            }
        }

        /// <summary>
        /// Called when a property value is changed.
        /// </summary>
        /// <param name="property">The property.</param>
        /// <param name="previousValue">The old value.</param>
        private void OnPropertyChanged(Property property, object previousValue)
        {
            this.OnPropertyChanged(this, property, previousValue);

            if (property.HasOption(PropertyOptions.AffectsChild))
            {
                this.OnChildAffected(this, property, previousValue);
            }

            if (property.HasOption(PropertyOptions.AffectsParent))
            {
                this.OnParentAffected(this, property, previousValue);
            }
        }

        /// <summary>
        /// Called when the data source is changed.
        /// </summary>
        /// <param name="oldDataSource">The old data source.</param>
        /// <param name="newDataSource">The new data source.</param>
        private void OnDataSourceChanged(object oldDataSource, object newDataSource)
        {
            if (oldDataSource is INotifyPropertyChanged)
            {
                (oldDataSource as INotifyPropertyChanged).PropertyChanged -= this.OnDataSourcePropertyChanged;
            }

            if (newDataSource is INotifyPropertyChanged)
            {
                (newDataSource as INotifyPropertyChanged).PropertyChanged += this.OnDataSourcePropertyChanged;
            }

            foreach (Element child in this.Children)
            {
                if (child.dataSource == null)
                {
                    child.OnDataSourceChanged(oldDataSource, newDataSource);
                }
            }

            foreach (var binding in this.DataBindings)
            {
                binding.Synchronize(this, null);
            }
        }

        /// <summary>
        /// Called when a data source property is changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The event arguments.</param>
        private void OnDataSourcePropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            foreach (var binding in this.DataBindings)
            {
                binding.Synchronize(this, e.PropertyName);
            }
        }

        /// <summary>
        /// Raises the event.
        /// </summary>
        /// <typeparam name="TEventArgs">The event argument type.</typeparam>
        /// <param name="event">The event.</param>
        /// <param name="source">The source object.</param>
        /// <param name="eventArguments">The event arguments.</param>
        private void RaiseEvent<TEventArgs>(Event<TEventArgs> @event, object source, TEventArgs eventArguments) where TEventArgs : HandledEventArgs
        {
            if (!eventArguments.Handled && this.Parent != null && @event.Routing == EventRouting.Tunnel)
            {
                this.Parent.RaiseEvent(@event, source, eventArguments);
            }

            if (!eventArguments.Handled)
            {
                (@event.GetValue(this.entries, null) as EventHandler<TEventArgs>)?.Invoke(source, eventArguments);
            }

            if (!eventArguments.Handled && this.Parent != null && @event.Routing == EventRouting.Bubble)
            {
                this.Parent.RaiseEvent(@event, source, eventArguments);
            }
        }

        /// <summary>
        /// An abstract base class for property bindings.
        /// </summary>
        private abstract class Binding
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="Binding" /> class.
            /// </summary>
            /// <param name="property">The bound property.</param>
            public Binding(Property property)
            {
                this.Property = property;
            }

            /// <summary>
            /// Gets the bound property.
            /// </summary>
            /// <value>The bound property.</value>
            public Property Property { get; }

            /// <summary>
            /// Binds to the element.
            /// </summary>
            /// <param name="element">The element.</param>
            public virtual void BindTo(Element element)
            {
                this.Property.SetValue(ref element.entries, this);
            }

            /// <summary>
            /// Unbinds the element.
            /// </summary>
            /// <param name="element">The element.</param>
            public virtual void Unbind(Element element)
            {
                this.Property.ClearValue(element.entries, NoValue);
            }

            /// <summary>
            /// Gets the binding value.
            /// </summary>
            /// <param name="element">The element.</param>
            /// <returns>The binding value.</returns>
            public abstract object GetValue(Element element);

            /// <summary>
            /// Sets the binding value.
            /// </summary>
            /// <param name="element">The element.</param>
            /// <param name="value">The binding value.</param>
            public abstract void SetValue(Element element, object value);
        }

        /// <summary>
        /// An abstract base class for data bindings.
        /// </summary>
        private abstract class DataBinding : Binding
        {
            /// <summary>
            /// The source property.
            /// </summary>
            private readonly string sourceProperty;

            /// <summary>
            /// The cached value.
            /// </summary>
            private object value;

            /// <summary>
            /// Initializes a new instance of the <see cref="DataBinding" /> class.
            /// </summary>
            /// <param name="targetProperty">The target property.</param>
            /// <param name="sourceProperty">The source property.</param>
            public DataBinding(Property targetProperty, PropertyInfo sourceProperty) : base(targetProperty)
            {
                this.sourceProperty = sourceProperty.Name;
            }

            /// <summary>
            /// Binds to the element.
            /// </summary>
            /// <param name="element">The element.</param>
            public override void BindTo(Element element)
            {
                this.value = element.GetValue(this.Property);
                base.BindTo(element);
                this.Synchronize(element, null);
            }

            /// <summary>
            /// Unbinds the element.
            /// </summary>
            /// <param name="element">The element.</param>
            public override void Unbind(Element element)
            {
                base.Unbind(element);
                element.OnPropertyChanged(this.Property, this.value);
            }

            /// <summary>
            /// Synchronizes the data binding.
            /// </summary>
            /// <param name="element">The element.</param>
            /// <param name="sourceProperty">The source property.</param>
            public void Synchronize(Element element, string sourceProperty)
            {
                if (sourceProperty == this.sourceProperty || sourceProperty == null)
                {
                    var oldValue = this.value;
                    var newValue = this.GetValue(element);

                    if (!Equals(oldValue, newValue))
                    {
                        this.value = newValue;
                        element.OnPropertyChanged(this.Property, oldValue);
                    }
                }
            }
        }

        /// <summary>
        /// Data binding.
        /// </summary>
        /// <typeparam name="TDataSource">The data source type.</typeparam>
        /// <typeparam name="TValue">The property value type.</typeparam>
        private sealed class DataBinding<TDataSource, TValue> : DataBinding
        {
            /// <summary>
            /// The property value getter.
            /// </summary>
            private readonly Func<TDataSource, TValue> getValue;

            /// <summary>
            /// The property value setter.
            /// </summary>
            private readonly Action<TDataSource, TValue> setValue;

            /// <summary>
            /// Initializes a new instance of the <see cref="DataBinding{TDataSource, TPropertyType}" /> class.
            /// </summary>
            /// <param name="targetProperty">The target property.</param>
            /// <param name="sourceProperty">The source property.</param>
            public DataBinding(Property targetProperty, PropertyInfo sourceProperty) : base(targetProperty, sourceProperty)
            {
                if (sourceProperty.CanRead)
                {
                    this.getValue = sourceProperty.GetGetMethod()?.CreateDelegate<Func<TDataSource, TValue>>();
                }

                if (sourceProperty.CanWrite)
                {
                    this.setValue = sourceProperty.GetSetMethod()?.CreateDelegate<Action<TDataSource, TValue>>();
                }
            }

            /// <summary>
            /// Gets the binding value.
            /// </summary>
            /// <param name="element">The element.</param>
            /// <returns>The binding value.</returns>
            public override object GetValue(Element element)
            {
                if (this.getValue != null)
                {
                    var dataSource = element.DataSource;

                    if (dataSource is TDataSource)
                    {
                        return this.getValue((TDataSource)dataSource);
                    }
                }

                return NoValue;
            }

            /// <summary>
            /// Sets the binding value.
            /// </summary>
            /// <param name="element">The element.</param>
            /// <param name="value">The binding value.</param>
            public override void SetValue(Element element, object value)
            {
                if (this.setValue != null)
                {
                    var dataSource = element.DataSource;

                    if (dataSource is TDataSource)
                    {
                        this.setValue((TDataSource)dataSource, (TValue)value);
                    }
                }
            }
        }

        /// <summary>
        /// Parent property binding.
        /// </summary>
        /// <typeparam name="TParent">The parent element type.</typeparam>
        /// <typeparam name="TValue">The property value type.</typeparam>
        private sealed class ParentBinding<TParent, TValue> : Binding where TParent : Element
        {
            /// <summary>
            /// The source property.
            /// </summary>
            private readonly Property<TValue> sourceProperty;

            /// <summary>
            /// Initializes a new instance of the <see cref="ParentBinding{TParent, TValue}"/> class.
            /// </summary>
            /// <param name="targetProperty">The target property.</param>
            /// <param name="sourceProperty">The source property.</param>
            public ParentBinding(Property<TValue> targetProperty, Property<TValue> sourceProperty) : base(targetProperty)
            {
                this.sourceProperty = sourceProperty;
            }

            /// <summary>
            /// Gets the binding value.
            /// </summary>
            /// <param name="element">The element.</param>
            /// <returns>The binding value.</returns>
            public override object GetValue(Element element)
            {
                var parent = element.FindParent<TParent>().FirstOrDefault();

                if (parent != null)
                {
                    return parent.GetValue(this.sourceProperty);
                }

                return NoValue;
            }

            /// <summary>
            /// Sets the binding value.
            /// </summary>
            /// <param name="element">The element.</param>
            /// <param name="value">The binding value.</param>
            public override void SetValue(Element element, object value)
            {
                var parent = element.FindParent<TParent>().FirstOrDefault();

                if (parent != null)
                {
                    parent.SetValue(this.sourceProperty, value);
                }
            }
        }
    }
}
