using System;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;

namespace RowDragDrop
{
    internal class DragDropManager
    {
        private readonly UIElement dragScope;
        private DragAdorner dragAdorner;
        private bool isDraggable;
        private Point? startingPoint;

        /// <summary>
        /// Initializes a new instance of the <see cref="DragDropManager"/> class.
        /// </summary>
        /// <param name="owner">The owner.</param>
        public DragDropManager(UIElement owner)
        {
            this.dragScope = owner;
        }

        /// <summary>
        /// Gets or sets the active dragged element.
        /// </summary>
        /// <value>The active dragged element.</value>
        public UIElement ActiveDraggedElement { get; private set; }

        /// <summary>
        /// Registers the specified <see cref="UIElement"/> for drag-and-drop.
        /// </summary>
        /// <param name="frameworkElement">The UI element.</param>
        public void RegisterForDragDrop(FrameworkElement frameworkElement)
        {
            frameworkElement.MouseEnter += this.UIElement_MouseEnter;
            frameworkElement.PreviewMouseDown += this.UIElement_MouseDown;
            frameworkElement.PreviewMouseUp += this.UIElement_MouseUp;
            frameworkElement.PreviewMouseMove += this.UIElement_MouseMove;
            frameworkElement.Unloaded += this.UIElement_Unloaded;
        }

        public void UnregisterForDragDrop(FrameworkElement frameworkElement)
        {
            frameworkElement.MouseEnter -= this.UIElement_MouseEnter;
            frameworkElement.PreviewMouseDown -= this.UIElement_MouseDown;
            frameworkElement.PreviewMouseUp -= this.UIElement_MouseUp;
            frameworkElement.PreviewMouseMove -= this.UIElement_MouseMove;
            frameworkElement.Unloaded -= this.UIElement_Unloaded;
        }

        /// <summary>
        /// Handles the MouseUp event of the ParentItemsControl control; used for testing the ungrouping.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Windows.Input.MouseButtonEventArgs"/> instance containing the event data.</param>
        internal void ParentControl_MouseUp(object sender, MouseButtonEventArgs e)
        {
            this.RemoveDragAdorner();
            this.UnCaptureMouse();
        }

        /// <summary>
        /// Prepares the element to drag; also used in the tests.
        /// </summary>
        /// <param name="elementToDrag">The element to drag.</param>
        internal void PrepareElementToDrag(UIElement elementToDrag)
        {
            if (this.ActiveDraggedElement == null)
            {
                this.ActiveDraggedElement = elementToDrag;
                this.CaptureMouse();
                this.CreateDragAdorner(elementToDrag);
            }
        }

        private void CaptureMouse()
        {
            this.dragScope.AddHandler(UIElement.MouseUpEvent, new MouseButtonEventHandler(this.ParentControl_MouseUp), true);
            this.dragScope.AddHandler(UIElement.MouseMoveEvent, new MouseEventHandler(this.ParentControl_MouseMove), true);

            Mouse.Capture(this.dragScope, CaptureMode.SubTree);
        }

        private void CreateDragAdorner(UIElement elementToDrag)
        {
            this.dragAdorner = new DragAdorner(this.dragScope, elementToDrag, true, 0.5);
            AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(this.dragScope);
            adornerLayer.Add(this.dragAdorner);
        }

        private bool IsReadyToDrag(Point position)
        {
            if (this.startingPoint == null)
            {
                return false;
            }

            return Mouse.LeftButton == MouseButtonState.Pressed && this.isDraggable &&
                   (Math.Abs(position.X - this.startingPoint.Value.X) > SystemParameters.MinimumHorizontalDragDistance ||
                    Math.Abs(position.Y - this.startingPoint.Value.Y) > SystemParameters.MinimumVerticalDragDistance);
        }

        private void ParentControl_MouseMove(object sender, MouseEventArgs e)
        {
            if (Mouse.LeftButton == MouseButtonState.Pressed)
            {
                this.UpdateAdornerPosition(e);
            }
        }

        /// <summary>
        /// Removes the adorner with the content being dragged from the drag-drop system.
        /// </summary>
        private void RemoveDragAdorner()
        {
            if (this.dragAdorner != null)
            {
                AdornerLayer.GetAdornerLayer(this.dragScope).Remove(this.dragAdorner);
                this.dragAdorner = null;
                this.ActiveDraggedElement = null;
            }
        }

        private void UIElement_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if (this.startingPoint == null)
            {
                this.startingPoint = e.GetPosition((IInputElement) sender);
            }
        }

        private void UIElement_MouseEnter(object sender, MouseEventArgs e)
        {
            this.isDraggable = (Mouse.LeftButton == MouseButtonState.Released);
        }

        private void UIElement_MouseMove(object sender, MouseEventArgs e)
        {
            var element = (UIElement) sender;
            if (this.IsReadyToDrag(e.GetPosition(element)))
            {
                this.PrepareElementToDrag(element);
            }
        }

        private void UIElement_MouseUp(object sender, MouseButtonEventArgs e)
        {
            this.startingPoint = null;
            this.isDraggable = true;
        }

        private void UIElement_Unloaded(object sender, RoutedEventArgs e)
        {
            this.UnregisterForDragDrop(sender as FrameworkElement);
        }

        private void UnCaptureMouse()
        {
            Mouse.Capture(null);
            this.dragScope.MouseMove -= this.ParentControl_MouseMove;
            this.dragScope.MouseUp -= this.ParentControl_MouseUp;
        }

        private void UpdateAdornerPosition(MouseEventArgs args)
        {
            if (this.dragAdorner != null)
            {
                Point point = args.GetPosition(this.dragScope);
                this.dragAdorner.LeftOffset = point.X;
                this.dragAdorner.TopOffset = point.Y;
            }
        }
    }
}