// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

/***************************************************************************\
*
*
* An event trigger is a set of actions that will be activated in response to
*  the specified event fired elsewhere.
*
*
\***************************************************************************/
using System.Collections;
using System.Diagnostics;
using System.Windows.Markup;
using System.Collections.Specialized;
using System.ComponentModel;

namespace System.Windows
{
    /// <summary>
    ///   A class that controls a set of actions to activate in response to an event
    /// </summary>
    [ContentProperty("Actions")]
    public class EventTrigger : TriggerBase, IAddChild
    {
        ///////////////////////////////////////////////////////////////////////
        // Public members

        /// <summary>
        ///     Build an empty instance of the EventTrigger object
        /// </summary>
        public EventTrigger()
        {
        }

        /// <summary>
        ///     Build an instance of EventTrigger associated with the given event
        /// </summary>
        public EventTrigger( RoutedEvent routedEvent )
        {
            RoutedEvent = routedEvent;
        }

        /// <summary>
        ///  Add an object child to this trigger's Actions
        /// </summary>
        void IAddChild.AddChild(object value)
        {
            AddChild(value);
        }

        /// <summary>
        ///  Add an object child to this trigger's Actions
        /// </summary>
        protected virtual void AddChild(object value)
        {
            TriggerAction action = value as TriggerAction;
            if (action == null)
            {
                throw new ArgumentException(SR.Format(SR.EventTriggerBadAction, value.GetType().Name));
            }
            Actions.Add(action);
        }

        /// <summary>
        ///  Add a text string to this trigger's Actions.  Note that this
        ///  is not supported and will result in an exception.
        /// </summary>
        void IAddChild.AddText(string text)
        {
            AddText(text);
        }

        /// <summary>
        ///  Add a text string to this trigger's Actions.  Note that this
        ///  is not supported and will result in an exception.
        /// </summary>
        protected virtual void AddText(string text)
        {
            XamlSerializerUtil.ThrowIfNonWhiteSpaceInAddText(text, this);
        }
        
        /// <summary>
        ///     The Event that will activate this trigger - one must be specified
        /// before an event trigger is meaningful.
        /// </summary>
        public RoutedEvent RoutedEvent
        {
            get
            {
                return _routedEvent;
            }
            set
            {
                ArgumentNullException.ThrowIfNull(value);

                if ( IsSealed )
                {
                    throw new InvalidOperationException(SR.Format(SR.CannotChangeAfterSealed, "EventTrigger"));
                }

                // When used as an element trigger, we don't actually need to seal
                //  to ensure cross-thread usability.  However, if we *are* fixed on
                //  listening to an event already, don't allow this change.
                if( _routedEventHandler != null )
                {
                    // Recycle the Seal error message.
                    throw new InvalidOperationException(SR.Format(SR.CannotChangeAfterSealed, "EventTrigger"));
                }

                _routedEvent = value;
            }
        }

        /// <summary>
        ///     The x:Name of the object whose event shall trigger this 
        /// EventTrigger.   If null, then this is the object being Styled
        /// and not anything under its Style.VisualTree.
        /// </summary>
        [DefaultValue(null)]
        public string SourceName
        {
            get
            {
                return _sourceName;
            }
            set
            {
                if( IsSealed )
                {
                    throw new InvalidOperationException(SR.Format(SR.CannotChangeAfterSealed, "EventTrigger"));
                }

                _sourceName = value;
            }
        }

        /// <summary>
        ///     Internal method to get the childId corresponding to the public
        /// sourceId.
        /// </summary>
        internal int TriggerChildIndex
        {
            get
            {
                return _childId;
            }
            set
            {
                _childId = value;
            }
        }

        /// <summary>
        ///     The collection of actions to activate when the Event occurs.
        /// At least one action is required for the trigger to be meaningful.
        /// </summary>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]        
        public TriggerActionCollection Actions
        {
            get
            {
                if( _actions == null )
                {
                    _actions = new TriggerActionCollection();

                    // Give the collection a back-link, this is used for the inheritance context
                    _actions.Owner = this;
                }
                return _actions;
            }
        }


        /// <summary>
        ///     If we get a new inheritance context (or it goes to null)
        ///     we need to tell actions about it.
        /// </summary>
        internal override void OnInheritanceContextChangedCore(EventArgs args)
        {
            base.OnInheritanceContextChangedCore(args);

            if (_actions == null)
            {
                return;
            }

            for (int i=0; i<_actions.Count; i++)
            {
                DependencyObject action = _actions[i] as DependencyObject;
                if (action != null && action.InheritanceContext == this)
                {
                    action.OnInheritanceContextChanged(args);
                }
            }
        }

        /// <summary>
        /// This method is used by TypeDescriptor to determine if this property should
        /// be serialized.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public bool ShouldSerializeActions()
        {
            return ( _actions != null && _actions.Count > 0 );
        }

        ///////////////////////////////////////////////////////////////////////
        // Internal members
        
        internal sealed override void Seal()
        {
            if( PropertyValues.Count > 0 )
            {
                throw new InvalidOperationException(SR.EventTriggerDoNotSetProperties);
            }

            // EnterActions/ExitActions aren't meaningful on event triggers.
            if( HasEnterActions || HasExitActions )
            {
                throw new InvalidOperationException(SR.EventTriggerDoesNotEnterExit);
            }

            if (_routedEvent != null && _actions != null && _actions.Count > 0)
            {
                _actions.Seal(this);  // TriggerActions need a link back to me to fetch the childId corresponding the sourceId string.
            }

            base.Seal(); // Should be almost a no-op given lack of PropertyValues
        }

        ///////////////////////////////////////////////////////////////////////
        // Private members

        // Event that will fire this trigger
        private RoutedEvent _routedEvent = null;

        // Name of the Style.VisualTree node whose event to listen to.
        //  May remain the default value of null, which  means the object being 
        //  Styled is the target instead of something within the Style.VisualTree.
        private string _sourceName = null;

        // Style childId corresponding to the SourceName string
        private int _childId = 0;

        // Actions to invoke when this trigger is fired
        private TriggerActionCollection _actions = null;

        ///////////////////////////////////////////////////////////////////////
        // Storage attached to other objects to support triggers
        //  Some of these can be moved to base class as necessary.

        //  Exists on objects that have information in their [Class].Triggers collection property. (Currently root FrameworkElement only.)
        internal static readonly UncommonField<TriggerCollection> TriggerCollectionField = new UncommonField<TriggerCollection>(null);

        // This is the listener that we hook up to the SourceId element.
        RoutedEventHandler _routedEventHandler = null;

        // This is the SourceId-ed element.
        FrameworkElement _source;
        


        ///////////////////////////////////////////////////////////////////////
        // Internal static methods to process event trigger information stored
        //  in attached storage of other objects.
        //
        // Called when the FrameworkElement and the tree structure underneath it has been
        //  built up.  This is the earliest point we can resolve all the child 
        //  node identification that may exist in a Trigger object.
        // This should be moved to base class if PropertyTrigger support is added.
        internal static void ProcessTriggerCollection( FrameworkElement triggersHost )
        {
            TriggerCollection triggerCollection = TriggerCollectionField.GetValue(triggersHost);
            if( triggerCollection != null )
            {
                // Don't seal the collection, because we allow it to change.  We will,
                // however, seal each of the triggers.
                
                for( int i = 0; i < triggerCollection.Count; i++ )
                {
                    ProcessOneTrigger( triggersHost, triggerCollection[i] );
                }
            }
        }


        ////////////////////////////////////////////////////////////////////////
        // ProcessOneTrigger
        //
        // Find the target element for this trigger, and set a listener for 
        // the event into (pointing back to the trigger).
        
        internal static void ProcessOneTrigger( FrameworkElement triggersHost, TriggerBase triggerBase )
        {
            // This code path is used in the element trigger case.  We don't actually
            //  need these guys to be usable cross-thread, so we don't really need
            //  to freeze/seal these objects.  The only one expected to cause problems
            //  is a change to the RoutedEvent.  At the same time we remove this
            //  Seal(), the RoutedEvent setter will check to see if the handler has
            //  already been created and refuse an update if so.
            // triggerBase.Seal();
            
            EventTrigger eventTrigger = triggerBase as EventTrigger;
            if( eventTrigger != null )
            {
                Debug.Assert( eventTrigger._routedEventHandler == null && eventTrigger._source == null);
                
                // PERF: Cache this result if it turns out we're doing a lot of lookups on the same name.
                eventTrigger._source = FrameworkElement.FindNamedFrameworkElement( triggersHost, eventTrigger.SourceName );

                // Create a statefull event delegate (which keeps a ref to the FE).
                EventTriggerSourceListener listener = new EventTriggerSourceListener( eventTrigger, triggersHost );


                // Store the RoutedEventHandler & target for use in DisconnectOneTrigger
                eventTrigger._routedEventHandler = new RoutedEventHandler(listener.Handler);
                eventTrigger._source.AddHandler( eventTrigger.RoutedEvent, eventTrigger._routedEventHandler,
                                                 false /* HandledEventsToo */ );
            }
            else
            {
                throw new InvalidOperationException(SR.TriggersSupportsEventTriggersOnly);
            }
        }

        ////////////////////////////////////////////////////////////////////////
        //
        // DisconnectAllTriggers
        //
        // Call DisconnectOneTrigger for each trigger in the Triggers collection.

        internal static void DisconnectAllTriggers( FrameworkElement triggersHost )
        {
            TriggerCollection triggerCollection = TriggerCollectionField.GetValue(triggersHost);

            if( triggerCollection != null )
            {
                for( int i = 0; i < triggerCollection.Count; i++ )
                {
                    DisconnectOneTrigger( triggersHost, triggerCollection[i] );
                }

            }
        }
        
        ////////////////////////////////////////////////////////////////////////
        //
        // DisconnectOneTrigger
        //
        // In ProcessOneTrigger, we connect an event trigger to the element
        // which it targets.  Here, we remove the event listener to clean up.

        internal static void DisconnectOneTrigger( FrameworkElement triggersHost, TriggerBase triggerBase )
        {
            EventTrigger eventTrigger = triggerBase as EventTrigger;
            
            if( eventTrigger != null )
            {
                eventTrigger._source.RemoveHandler( eventTrigger.RoutedEvent, eventTrigger._routedEventHandler);
                eventTrigger._routedEventHandler = null;
            }
            else
            {
                throw new InvalidOperationException(SR.TriggersSupportsEventTriggersOnly);
            }
        }



        internal class EventTriggerSourceListener
        {
            internal EventTriggerSourceListener( EventTrigger trigger, FrameworkElement host )
            {
                _owningTrigger = trigger;
                _owningTriggerHost = host;
            }

            internal void Handler(object sender, RoutedEventArgs e)
            {
                // Invoke all actions of the associated EventTrigger object.
                TriggerActionCollection actions = _owningTrigger.Actions;
                for( int j = 0; j < actions.Count; j++ )
                {
                    actions[j].Invoke(_owningTriggerHost);
                }
            }

            private EventTrigger     _owningTrigger;
            private FrameworkElement _owningTriggerHost;
        }
        
    }
}
