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


using System;
using MS.Internal;
using MS.Internal.KnownBoxes;
using MS.Utility;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Threading;
using System.Windows;
#if OLD_AUTOMATION
using System.Windows.Automation.Provider;
#endif
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Controls.Primitives;
using System.Windows.Markup;
using System.Windows.Shapes;

namespace System.Windows.Controls
{
    /// <summary>
    ///     Control that defines a menu of choices for users to invoke.
    /// </summary>
    [DefaultEvent("Opened")]
#if OLD_AUTOMATION
    [Automation(AccessibilityControlType = "Menu")]
#endif
    public class ContextMenu : MenuBase
    {
        #region Constructors

        //-------------------------------------------------------------------
        //
        //  Constructors
        //
        //-------------------------------------------------------------------

        static ContextMenu()
        {
            EventManager.RegisterClassHandler(typeof(ContextMenu), AccessKeyManager.AccessKeyPressedEvent, new AccessKeyPressedEventHandler(OnAccessKeyPressed));

            DefaultStyleKeyProperty.OverrideMetadata(typeof(ContextMenu), new FrameworkPropertyMetadata(typeof(ContextMenu)));
            _dType = DependencyObjectType.FromSystemTypeInternal(typeof(ContextMenu));

            IsTabStopProperty.OverrideMetadata(typeof(ContextMenu), new FrameworkPropertyMetadata(BooleanBoxes.FalseBox));
            KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(ContextMenu), new FrameworkPropertyMetadata(KeyboardNavigationMode.Cycle));
            KeyboardNavigation.ControlTabNavigationProperty.OverrideMetadata(typeof(ContextMenu), new FrameworkPropertyMetadata(KeyboardNavigationMode.Contained));
            KeyboardNavigation.DirectionalNavigationProperty.OverrideMetadata(typeof(ContextMenu), new FrameworkPropertyMetadata(KeyboardNavigationMode.Cycle));

            // Disable the default focus visual for ContextMenu
            FocusVisualStyleProperty.OverrideMetadata(typeof(ContextMenu), new FrameworkPropertyMetadata((object)null /* default value */));
        }

        /// <summary>
        ///     Default ContextMenu constructor
        /// </summary>
        /// <remarks>
        ///     Automatic determination of current Dispatcher. Use alternative constructor
        ///     that accepts a Dispatcher for best performance.
        /// </remarks>
        public ContextMenu() : base()
        {
            Initialize();
        }

        #endregion


        #region Public Properties

        //-------------------------------------------------------------------
        //
        //  Public Properties
        //
        //-------------------------------------------------------------------

        /// <summary>
        ///     The DependencyProperty for the HorizontalOffset property.
        /// </summary>
        public static readonly DependencyProperty HorizontalOffsetProperty =
                ContextMenuService.HorizontalOffsetProperty.AddOwner(typeof(ContextMenu),
                            new FrameworkPropertyMetadata(null,
                                                          new CoerceValueCallback(CoerceHorizontalOffset)));

        private static object CoerceHorizontalOffset(DependencyObject d, object value)
        {
            return PopupControlService.CoerceProperty(d, value, ContextMenuService.HorizontalOffsetProperty);
        }

        /// <summary>
        /// Get or set X offset of the ContextMenu
        /// </summary>
        [TypeConverter(typeof(LengthConverter))]
        [Bindable(true), Category("Layout")]
        public double HorizontalOffset
        {
            get { return (double) GetValue(HorizontalOffsetProperty); }
            set { SetValue(HorizontalOffsetProperty, value); }
        }

        /// <summary>
        ///     The DependencyProperty for the VerticalOffset property.
        /// </summary>
        public static readonly DependencyProperty VerticalOffsetProperty =
                ContextMenuService.VerticalOffsetProperty.AddOwner(typeof(ContextMenu),
                            new FrameworkPropertyMetadata(null,
                                                          new CoerceValueCallback(CoerceVerticalOffset)));

        private static object CoerceVerticalOffset(DependencyObject d, object value)
        {
            return PopupControlService.CoerceProperty(d, value, ContextMenuService.VerticalOffsetProperty);
        }

        /// <summary>
        /// Get or set Y offset of the ContextMenu
        /// </summary>
        [TypeConverter(typeof(LengthConverter))]
        [Bindable(true), Category("Layout")]
        public double VerticalOffset
        {
            get { return (double) GetValue(VerticalOffsetProperty); }
            set { SetValue(VerticalOffsetProperty, value); }
        }

        /// <summary>
        /// DependencyProperty for IsOpen property
        /// </summary>
        public static readonly DependencyProperty IsOpenProperty =
                Popup.IsOpenProperty.AddOwner(
                        typeof(ContextMenu),
                        new FrameworkPropertyMetadata(
                                BooleanBoxes.FalseBox,
                                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                new PropertyChangedCallback(OnIsOpenChanged)));

        /// <summary>
        /// Get or set IsOpen property of the ContextMenu
        /// </summary>
        [Bindable(true), Browsable(false), Category("Appearance")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public bool IsOpen
        {
            get { return (bool) GetValue(IsOpenProperty); }
            set { SetValue(IsOpenProperty, BooleanBoxes.Box(value)); }
        }

        private static void OnIsOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ContextMenu ctrl = (ContextMenu) d;

            if ((bool) e.NewValue)
            {
                if (ctrl._parentPopup == null)
                {
                    ctrl.HookupParentPopup();
                }

                ctrl._parentPopup.Unloaded += new RoutedEventHandler(ctrl.OnPopupUnloaded);

                // Turn on keyboard cues in case ContextMenu was opened with the keyboard
                ctrl.SetValue(KeyboardNavigation.ShowKeyboardCuesProperty, KeyboardNavigation.IsKeyboardMostRecentInputDevice());
            }
            else
            {
                ctrl.ClosingMenu();
            }
        }

        /// <summary>
        ///     The DependencyProperty for the PlacementTarget property.
        /// </summary>
        public static readonly DependencyProperty PlacementTargetProperty =
                ContextMenuService.PlacementTargetProperty.AddOwner(
                        typeof(ContextMenu),
                            new FrameworkPropertyMetadata(null,
                                                          new CoerceValueCallback(CoercePlacementTarget)));

        private static object CoercePlacementTarget(DependencyObject d, object value)
        {
            return PopupControlService.CoerceProperty(d, value, ContextMenuService.PlacementTargetProperty);
        }

        /// <summary>
        /// Get or set PlacementTarget property of the ContextMenu
        /// </summary>
        [Bindable(true), Category("Layout")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public UIElement PlacementTarget
        {
            get { return (UIElement) GetValue(PlacementTargetProperty); }
            set { SetValue(PlacementTargetProperty, value); }
        }

        /// <summary>
        ///     The DependencyProperty for the PlacementRectangle property.
        /// </summary>
        public static readonly DependencyProperty PlacementRectangleProperty =
                ContextMenuService.PlacementRectangleProperty.AddOwner(typeof(ContextMenu),
                            new FrameworkPropertyMetadata(null,
                                                          new CoerceValueCallback(CoercePlacementRectangle)));

        private static object CoercePlacementRectangle(DependencyObject d, object value)
        {
            return PopupControlService.CoerceProperty(d, value, ContextMenuService.PlacementRectangleProperty);
        }

        /// <summary>
        /// Get or set PlacementRectangle property of the ContextMenu
        /// </summary>
        [Bindable(true), Category("Layout")]
        public Rect PlacementRectangle
        {
            get { return (Rect) GetValue(PlacementRectangleProperty); }
            set { SetValue(PlacementRectangleProperty, value); }
        }

        /// <summary>
        ///     The DependencyProperty for the Placement property.
        /// </summary>
        public static readonly DependencyProperty PlacementProperty =
                ContextMenuService.PlacementProperty.AddOwner(typeof(ContextMenu),
                            new FrameworkPropertyMetadata(null,
                                                          new CoerceValueCallback(CoercePlacement)));

        private static object CoercePlacement(DependencyObject d, object value)
        {
            return PopupControlService.CoerceProperty(d, value, ContextMenuService.PlacementProperty);
        }

        /// <summary>
        /// Get or set Placement property of the ContextMenu
        /// </summary>
        [Bindable(true), Category("Layout")]
        public PlacementMode Placement
        {
            get { return (PlacementMode) GetValue(PlacementProperty); }
            set { SetValue(PlacementProperty, value); }
        }

        /// <summary>
        ///     The DependencyProperty for HasDropShadow
        /// </summary>
        public static readonly DependencyProperty HasDropShadowProperty =
                ContextMenuService.HasDropShadowProperty.AddOwner(
                        typeof(ContextMenu),
                        new FrameworkPropertyMetadata(null,
                                                      new CoerceValueCallback(CoerceHasDropShadow)));

        private static object CoerceHasDropShadow(DependencyObject d, object value)
        {
            ContextMenu cm = (ContextMenu)d;

            if (cm._parentPopup == null || !cm._parentPopup.AllowsTransparency || !SystemParameters.DropShadow)
            {
                return BooleanBoxes.FalseBox;
            }

            return PopupControlService.CoerceProperty(d, value, ContextMenuService.HasDropShadowProperty);
        }

        /// <summary>
        ///     Whether the control has a drop shadow.
        /// </summary>
        public bool HasDropShadow
        {
            get { return (bool)GetValue(HasDropShadowProperty); }
            set { SetValue(HasDropShadowProperty, value); }
        }

        /// <summary>
        ///     The DependencyProperty for the CustomPopupPlacementCallback property.
        ///     Flags:              None
        ///     Default Value:      null
        /// </summary>
        public static readonly DependencyProperty CustomPopupPlacementCallbackProperty =
                Popup.CustomPopupPlacementCallbackProperty.AddOwner(typeof(ContextMenu));

        /// <summary>
        ///     Chooses the behavior of where the ContextMenu should be placed on screen.
        /// </summary>
        [Bindable(false), Category("Layout")]
        public CustomPopupPlacementCallback CustomPopupPlacementCallback
        {
            get { return (CustomPopupPlacementCallback) GetValue(CustomPopupPlacementCallbackProperty); }
            set { SetValue(CustomPopupPlacementCallbackProperty, value); }
        }

        /// <summary>
        ///     The DependencyProperty for the StaysOpen property.
        ///     Indicates that, once opened, ContextMenu should stay open until IsOpenProperty changed to 'false'.
        ///     Flags:              None
        ///     Default Value:      false
        /// </summary>
        public static readonly DependencyProperty StaysOpenProperty =
                Popup.StaysOpenProperty.AddOwner(typeof(ContextMenu));

        /// <summary>
        ///     Chooses the behavior of when the ContextMenu should automatically close.
        /// </summary>
        [Bindable(true), Category("Behavior")]
        public bool StaysOpen
        {
            get { return (bool) GetValue(StaysOpenProperty); }
            set { SetValue(StaysOpenProperty, value); }
        }

        #endregion

        #region Events

        /// <summary>
        ///     Opened event
        /// </summary>
        public static readonly RoutedEvent OpenedEvent = PopupControlService.ContextMenuOpenedEvent.AddOwner(typeof(ContextMenu));

        /// <summary>
        ///     Event that fires when the popup opens.
        /// </summary>
        public event RoutedEventHandler Opened
        {
            add
            {
                AddHandler(OpenedEvent, value);
            }
            remove
            {
                RemoveHandler(OpenedEvent, value);
            }
        }

        /// <summary>
        ///     Called when the OpenedEvent fires.
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnOpened(RoutedEventArgs e)
        {
            RaiseEvent(e);
        }

        /// <summary>
        ///     Closed event
        /// </summary>
        public static readonly RoutedEvent ClosedEvent = PopupControlService.ContextMenuClosedEvent.AddOwner(typeof(ContextMenu));

        /// <summary>
        ///     Event that fires when the popup closes
        /// </summary>
        public event RoutedEventHandler Closed
        {
            add
            {
                AddHandler(ClosedEvent, value);
            }
            remove
            {
                RemoveHandler(ClosedEvent, value);
            }
        }

        /// <summary>
        ///     Called when the ClosedEvent fires.
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnClosed(RoutedEventArgs e)
        {
            RaiseEvent(e);
        }

        #endregion


        //-------------------------------------------------------------------
        //
        //  Protected Methods
        //
        //-------------------------------------------------------------------

        #region Protected Methods

        /// <summary>
        /// Creates AutomationPeer (<see cref="UIElement.OnCreateAutomationPeer"/>)
        /// </summary>
        protected override System.Windows.Automation.Peers.AutomationPeer OnCreateAutomationPeer()
        {
            return new System.Windows.Automation.Peers.ContextMenuAutomationPeer(this);
        }

        /// <summary>
        /// Prepare the element to display the item.  This may involve
        /// applying styles, setting bindings, etc.
        /// </summary>
        protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
        {
            base.PrepareContainerForItemOverride(element, item);

            MenuItem.PrepareMenuItem(element, item);
        }

        /// <summary>
        ///     If control has a scrollviewer in its style and has a custom keyboard scrolling behavior when HandlesScrolling should return true.
        /// Then ScrollViewer will not handle keyboard input and leave it up to the control.
        /// </summary>
        protected internal override bool HandlesScrolling
        {
            get { return true; }
        }

        /// <summary>
        ///     This is the method that responds to the KeyDown event.
        /// </summary>
        /// <param name="e">Event arguments</param>
        protected override void OnKeyDown(KeyEventArgs e)
        {
            base.OnKeyDown(e);
            if (e.Handled || !IsOpen)
            {
                // Ignore if the event was already handled or if the menu closed. This might happen
                // if input events get queued up and one in the middle caused the menu to close.
                return;
            }

            Key key = e.Key;

            switch (key)
            {
                case Key.Down:
                    if (CurrentSelection == null)
                    {
                        NavigateToStart(new ItemNavigateArgs(e.Device, Keyboard.Modifiers));
                        e.Handled = true;
                    }

                    break;

                case Key.Up:
                    if (CurrentSelection == null)
                    {
                        NavigateToEnd(new ItemNavigateArgs(e.Device, Keyboard.Modifiers));
                        e.Handled = true;
                    }

                    break;
            }
        }

        /// <summary>
        ///     This is the method that responds to the KeyUp event.
        /// </summary>
        /// <param name="e">Event arguments</param>
        protected override void OnKeyUp(KeyEventArgs e)
        {
            base.OnKeyUp(e);
            if (!e.Handled && IsOpen && e.Key == Key.Apps)
            {
                    KeyboardLeaveMenuMode();
                    e.Handled = true;
            }
        }

        #endregion

        #region Implementation

        private static readonly DependencyProperty InsideContextMenuProperty =
            MenuItem.InsideContextMenuProperty.AddOwner(typeof(ContextMenu),
                                                        new FrameworkPropertyMetadata(BooleanBoxes.TrueBox,
                                                                                      FrameworkPropertyMetadataOptions.Inherits));

        //-------------------------------------------------------------------
        //
        //  Implementation
        //
        //-------------------------------------------------------------------

        private void Initialize()
        {
            // We have to set this locally in order for inheritance to work
            MenuItem.SetInsideContextMenuProperty(this, true);

            InternalMenuModeChanged += new EventHandler(OnIsMenuModeChanged);
        }

        private void HookupParentPopup()
        {
            Debug.Assert(_parentPopup == null, "_parentPopup should be null");

            _parentPopup = new Popup();

            _parentPopup.AllowsTransparency = true;

            // Coerce HasDropShadow property in case popup can't be transparent
            CoerceValue(HasDropShadowProperty);

            _parentPopup.DropOpposite = false;

            // Listening to the Opened and Closed events lets us guarantee that
            // the popup is actually opened when we perform those functions.
            _parentPopup.Opened += new EventHandler(OnPopupOpened);
            _parentPopup.Closed += new EventHandler(OnPopupClosed);
            _parentPopup.PopupCouldClose += new EventHandler(OnPopupCouldClose);

            _parentPopup.SetResourceReference(Popup.PopupAnimationProperty, SystemParameters.MenuPopupAnimationKey);

            // Hooks up the popup properties from this menu to the popup so that
            // setting them on this control will also set them on the popup.
            Popup.CreateRootPopup(_parentPopup, this);
        }

        private void OnPopupCouldClose(object sender, EventArgs e)
        {
            SetCurrentValueInternal(IsOpenProperty, BooleanBoxes.FalseBox);
        }

        private void OnPopupOpened(object source, EventArgs e)
        {
            if (CurrentSelection != null)
            {
                CurrentSelection = null;
            }
            IsMenuMode = true;

            // When we open, if the Left or Right buttons are pressed, MenuBase should not
            // dismiss when it sees the up for those buttons.
            if (Mouse.LeftButton == MouseButtonState.Pressed)
            {
                IgnoreNextLeftRelease = true;
            }
            if (Mouse.RightButton == MouseButtonState.Pressed)
            {
                IgnoreNextRightRelease = true;
            }

            OnOpened(new RoutedEventArgs(OpenedEvent, this));
        }

        private void OnPopupClosed(object source, EventArgs e)
        {
            // Clear out any state we stored for this time around
            IgnoreNextLeftRelease = false;
            IgnoreNextRightRelease = false;

            IsMenuMode = false;
            OnClosed(new RoutedEventArgs(ClosedEvent, this));
        }

        private void ClosingMenu()
        {
            if (_parentPopup != null)
            {
                _parentPopup.Unloaded -= new RoutedEventHandler(OnPopupUnloaded);

                // As the menu closes, we need the parent connection to be maintained
                // while we do things like release capture so that notifications
                // go up the tree correctly. Post this for later.
                Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                    (DispatcherOperationCallback)delegate(object arg)
                    {
                        ContextMenu cm = (ContextMenu)arg;
                        if (!cm.IsOpen) // Check that the menu is still closed
                        {
                            // Prevent focus scoping from remembering the last focused element.
                            // The next time the menu opens, we want to start clean.
                            FocusManager.SetFocusedElement(cm, null);
                        }
                        return null;
                    },
                    this);
            }
        }

        private void OnPopupUnloaded(object sender, RoutedEventArgs e)
        {
            // The tree that the ContextMenu is in is being torn down, close the menu.

            if (IsOpen)
            {
                // This will be called during a tree walk, closing the menu will cause a tree change,
                // so post for later.
                Dispatcher.BeginInvoke(DispatcherPriority.Send,
                    (DispatcherOperationCallback)delegate(object arg)
                    {
                        ContextMenu cm = (ContextMenu)arg;
                        if (cm.IsOpen) // Check that the menu is still open
                        {
                            cm.SetCurrentValueInternal(IsOpenProperty, BooleanBoxes.FalseBox);
                        }
                        return null;
                    },
                    this);
            }
        }

        /// <summary>
        ///     Called when IsMenuMode changes on this class
        /// </summary>
        private void OnIsMenuModeChanged(object sender, EventArgs e)
        {
            // IsMenuMode changed from false to true
            if (IsMenuMode)
            {
                // Keep the previous focus
                if (Keyboard.FocusedElement != null)
                {
                    _weakRefToPreviousFocus = new WeakReference<IInputElement>(Keyboard.FocusedElement);
                }

                // Take focus so we get keyboard events.
                Focus();
            }
            else // IsMenuMode changed from true to false
            {
                SetCurrentValueInternal(IsOpenProperty, BooleanBoxes.FalseBox);

                if(_weakRefToPreviousFocus != null)
                {
                    IInputElement previousFocus;
                    if (_weakRefToPreviousFocus.TryGetTarget(out previousFocus))
                    {
                        // Previous focused element is still alive, so return focus to it.
                        previousFocus.Focus();
                    }
                    
                    _weakRefToPreviousFocus = null;
                }
            }
        }

        protected override void OnIsKeyboardFocusWithinChanged(DependencyPropertyChangedEventArgs e)
        {
            // If keyboard focus moves out from within the ContextMenu, the
            // ContextMenu will be dismissed.  We do not want to restore focus
            // in this case.
            //
            // See MenuBase.OnIsKeyboardFocusWithinChanged
            if((bool)e.NewValue == false)
            {
                _weakRefToPreviousFocus = null;
            }

            // Allow the base class to dismiss us as needed.
            base.OnIsKeyboardFocusWithinChanged(e);
        }

        internal override bool IgnoreModelParentBuildRoute(RoutedEventArgs e)
        {
            // Context menus are logically connected to their host element.  Generally, we don't
            // want input events to route out of the context menu.  Consider the sitituation where
            // a TextBox has a ContextMenu.  It is confusing for the text box to move the cursor
            // when I press the arrow keys while the context menu is being displayed.
            //
            // For now we only block keyboard events and ToolTip events.  What about mouse & stylus events?
            //
            // Note: This will cause the route to not follow the logical link, but it will still
            // follow the visual link.  At the time of writing this comment, the visual link
            // contained things like an adorner decorator.  Eventually the visual ancestory lead
            // to a PopupRoot, which also has a logical link over to the Popup element.  Since
            // the PopupRoot does not override this virtual, the route continues through its logical
            // link and ends up escaping into the larger logical tree anyways.
            //
            // The solution is that the PopupRoot element (on the top of this visual tree) will
            // defer back to this method to determine if it should route any further.
            //
            return (e is KeyEventArgs) || (e is FindToolTipEventArgs);
        }

        private static void OnAccessKeyPressed(object sender, AccessKeyPressedEventArgs e)
        {
            e.Scope = sender;
            e.Handled = true;
        }

        /// <summary>
        /// Called when this element's visual parent changes
        /// </summary>
        /// <param name="oldParent"></param>
        protected internal override void OnVisualParentChanged(DependencyObject oldParent)
        {
            base.OnVisualParentChanged(oldParent);

            if (!Popup.IsRootedInPopup(_parentPopup, this))
            {
                throw new InvalidOperationException(SR.Format(SR.ElementMustBeInPopup, "ContextMenu"));
            }
        }

        internal override void OnAncestorChanged()
        {
            base.OnAncestorChanged();

            if (!Popup.IsRootedInPopup(_parentPopup, this))
            {
                throw new InvalidOperationException(SR.Format(SR.ElementMustBeInPopup, "ContextMenu"));
            }
        }

        #endregion

        #region Private Fields

        private Popup _parentPopup;
        private WeakReference<IInputElement> _weakRefToPreviousFocus; // Keep the previously focused element before CM to open

        #endregion

        #region DTypeThemeStyleKey

        // Returns the DependencyObjectType for the registered ThemeStyleKey's default
        // value. Controls will override this method to return approriate types.
        internal override DependencyObjectType DTypeThemeStyleKey
        {
            get { return _dType; }
        }

        private static DependencyObjectType _dType;

        #endregion DTypeThemeStyleKey
    }
}
