// 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.

/***************************************************************************\
*
*
* This object includes a Storyboard reference.  When triggered, the Storyboard
*  is started.
*
*
\***************************************************************************/
using System.ComponentModel;            // DefaultValueAttribute
using System.Diagnostics;               // Debug.Assert
using System.Windows;                   // SR.Get
using System.Windows.Documents;         // TableTemplate
using System.Windows.Markup;     // IAddChild

namespace System.Windows.Media.Animation
{
/// <summary>
/// BeginStoryboard will call begin on its Storyboard reference when
///  it is triggered.
/// </summary>
[RuntimeNameProperty("Name")] // Enables INameScope.FindName to find BeginStoryboard objects.
[ContentProperty("Storyboard")] // Enables <Storyboard> child without explicit <BeginStoryboard.Storyboard> tag.
public sealed class BeginStoryboard : TriggerAction
{
    /// <summary>
    ///     Creates an instance of the BeginStoryboard object.
    /// </summary>
    public BeginStoryboard()
        : base()
    {
    }

    /// <summary>
    ///     DependencyProperty to back the Storyboard property
    /// </summary>
    public static readonly DependencyProperty StoryboardProperty =
                DependencyProperty.Register( "Storyboard", typeof(Storyboard), typeof(BeginStoryboard) );


    /// <summary>
    ///     The Storyboard object that this action is associated with.  This 
    /// must be specified before Invoke is called.
    /// </summary>
    [DefaultValue(null)]
    public Storyboard Storyboard
    {
        get
        {
            return GetValue(StoryboardProperty) as Storyboard;
        }
        set
        {
            ThrowIfSealed();

            SetValue( StoryboardProperty, value );
        }
    }

    /// <summary>
    ///     Specify the hand-off behavior to use when starting the animation
    /// clocks in this storyboard
    /// </summary>
    [DefaultValue(HandoffBehavior.SnapshotAndReplace)]
    public HandoffBehavior HandoffBehavior 
    {
        get
        {
            return _handoffBehavior;
        }
        set
        {
            ThrowIfSealed();

            if(HandoffBehaviorEnum.IsDefined(value)) 
            {
                _handoffBehavior = value;
            }
            else
            {
                throw new ArgumentException(SR.Storyboard_UnrecognizedHandoffBehavior);
            }
        }
    }

    /// <summary>
    ///     The name to use for referencing this Storyboard.  This named is used 
    /// by a control action such as pause and resume.  Defaults to null, which
    /// means this storyboard is not going to be interactively controlled.
    /// </summary>
    // Null == no interactive control == "Fire and Forget"
    [DefaultValue(null)]
    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            ThrowIfSealed();

            if(value != null && !System.Windows.Markup.NameValidationHelper.IsValidIdentifierName(value))
            {
                // Duplicate the error string thrown from DependencyObject.SetValueValidateParams
                throw new ArgumentException(SR.Format(SR.InvalidPropertyValue, value, "Name"));
            }
            
            // Null is OK - it's to remove whatever name was previously set.
            _name = value;
        }
    }

    private void ThrowIfSealed()
    {
        if (IsSealed)
        {
            throw new InvalidOperationException(SR.Format(SR.CannotChangeAfterSealed, "BeginStoryboard"));
        }
    }
    
    // Bug #1329664 workaround to make beta 2
    // Remove thread affinity when sealed, but before doing that, snapshot the
    //  current Storyboard value and remove *its* thread affinity too.
    internal override void Seal()
    {
        if( !IsSealed )
        {
            // Gets our Storyboard value.  This value may have come from a
            //  ResourceReferenceExpression or might have been a deferred
            //  reference that has since been realized.
            Storyboard snapshot = GetValue(StoryboardProperty) as Storyboard;

            if( snapshot == null )
            {
                // This is the same error thrown by Begin if the Storyboard
                //  property couldn't be resolved at Begin time.  Since we're
                //  not allowing changes after this point, lack of resolution
                //  here means the same thing.
                throw new InvalidOperationException(SR.Storyboard_StoryboardReferenceRequired);
            }
            
            // We're planning to break our thread affinity - we also need to
            //  make sure the Storyboard can also be used accross threads.
            if(!snapshot.CanFreeze)
            {
                throw new InvalidOperationException(SR.Storyboard_UnableToFreeze);
            }
            if(!snapshot.IsFrozen)
            {
                snapshot.Freeze();
            }

            // Promote that snapshot into a local value.  This is a no-op if it
            //  was a deferred reference or local Storyboard, but if it came from a 
            //  ResourceReferenceExpression it will replace the Expression object
            //  with a snapshot of its current value.
            Storyboard = snapshot;
        }
        else
        {
            ; // base.Seal() will throw exception for us if already sealed.
        }

        base.Seal();        

        // Now we can break our thread affinity
        DetachFromDispatcher();
    }
    
    /// <summary>
    ///     Called when it's time to execute this storyboard action
    /// </summary>

    internal sealed override void Invoke( FrameworkElement fe, FrameworkContentElement fce, Style targetStyle, FrameworkTemplate frameworkTemplate, Int64 layer )
    {
        Debug.Assert( fe != null || fce != null, "Caller of internal function failed to verify that we have a FE or FCE - we have neither." );

        INameScope nameScope = null;
        if( targetStyle != null )
        {
            nameScope = targetStyle;
        }
        else
        {
            Debug.Assert( frameworkTemplate != null );
            nameScope = frameworkTemplate;
        }


        Begin( (fe != null ) ? (DependencyObject)fe : (DependencyObject)fce, nameScope, layer );
    }

    internal sealed override void Invoke( FrameworkElement fe )
    {
        Debug.Assert( fe != null, "Invoke needs an object as starting point");

        Begin( fe, null, Storyboard.Layers.ElementEventTrigger );
    }

    private void Begin( DependencyObject targetObject, INameScope nameScope, Int64 layer )
    {
        if( Storyboard == null )
        {
            throw new InvalidOperationException(SR.Storyboard_StoryboardReferenceRequired);
        }

        if( Name != null )
        {
            Storyboard.BeginCommon(targetObject, nameScope, _handoffBehavior, true /* == is controllable */, layer );
        }
        else
        {
            Storyboard.BeginCommon(targetObject, nameScope, _handoffBehavior, false /* == not controllable */, layer );
        }
    }

    private HandoffBehavior _handoffBehavior = HandoffBehavior.SnapshotAndReplace;
    private string _name = null;
}
}
