using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

using Point = System.Drawing.Point;

namespace ScreenToGif.Controls
{
    public class CroppingAdorner : Adorner
    {
        #region Private variables

        /// <summary>
        /// The size of the Thumb in pixels.
        /// </summary>
        private const double ThumbWidth = 10;

        /// <summary>
        /// Rectangle Shape, visual aid for the cropping selection.
        /// </summary>
        private readonly PuncturedRect _cropMask;

        /// <summary>
        /// Canvas that holds the Thumb collection.
        /// </summary>
        private readonly Canvas _thumbCanvas;

        /// <summary>
        /// Corner Thumbs used to change the crop selection.
        /// </summary>
        private readonly Thumb _thumbTopLeft, _thumbTopRight, _thumbBottomLeft,
            _thumbBottomRight, _thumbTop, _thumbLeft, _thumbBottom, _thumbRight, _thumbCenter;

        /// <summary>
        /// Stores and manages the adorner's visual children.
        /// </summary>
        private readonly VisualCollection _visualCollection;

        /// <summary>
        /// Screen DPI.
        /// </summary>
        private static readonly double DpiX, DpiY;

        #endregion

        #region Routed Events

        public static readonly RoutedEvent CropChangedEvent = EventManager.RegisterRoutedEvent(
            "CropChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(CroppingAdorner));

        public event RoutedEventHandler CropChanged
        {
            add => AddHandler(CropChangedEvent, value);
            remove => RemoveHandler(CropChangedEvent, value);
        }

        #endregion

        #region Dependency Properties

        public static DependencyProperty FillProperty = Shape.FillProperty.AddOwner(typeof(CroppingAdorner),
            new FrameworkPropertyMetadata(new SolidColorBrush(Color.FromArgb(110, 0, 0, 0)), FillPropChanged));

        public static readonly DependencyProperty ClipRectangleProperty = DependencyProperty.Register("ClipRectangle", typeof(Rect), typeof(CroppingAdorner),
            new FrameworkPropertyMetadata(new Rect(0, 0, 0, 0), ClipRectanglePropertyChanged));

        public Brush Fill
        {
            get => (Brush)GetValue(FillProperty);
            set => SetValue(FillProperty, value);
        }

        public Rect ClipRectangle
        {
            get => _cropMask.Interior;
            set => SetValue(ClipRectangleProperty, value);
        }

        private static void FillPropChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is CroppingAdorner crp)
                crp._cropMask.Fill = (Brush) e.NewValue;
        }

        private static void ClipRectanglePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (!(d is CroppingAdorner crp))
                return;

            crp._cropMask.Interior = (Rect)e.NewValue;
            crp.SetThumbs(crp._cropMask.Interior);
            crp.RaiseEvent(new RoutedEventArgs(CropChangedEvent, crp));
        }

        #endregion

        #region Constructor

        static CroppingAdorner()
        {
            using (var g = System.Drawing.Graphics.FromHwnd((IntPtr)0))
            {
                DpiX = g.DpiX;
                DpiY = g.DpiY;
            }
        }

        public CroppingAdorner(UIElement adornedElement, Rect rcInit)
            : base(adornedElement)
        {
            _cropMask = new PuncturedRect
            {
                IsHitTestVisible = false,
                Interior = rcInit,
                Fill = Fill,
                Focusable = true
            };

            _thumbCanvas = new Canvas
            {
                HorizontalAlignment = HorizontalAlignment.Stretch,
                VerticalAlignment = VerticalAlignment.Stretch
            };

            _visualCollection = new VisualCollection(this) { _cropMask, _thumbCanvas };

            BuildCorner(ref _thumbTop, Cursors.SizeNS);
            BuildCorner(ref _thumbBottom, Cursors.SizeNS);
            BuildCorner(ref _thumbLeft, Cursors.SizeWE);
            BuildCorner(ref _thumbRight, Cursors.SizeWE);
            BuildCorner(ref _thumbTopLeft, Cursors.SizeNWSE);
            BuildCorner(ref _thumbTopRight, Cursors.SizeNESW);
            BuildCorner(ref _thumbBottomLeft, Cursors.SizeNESW);
            BuildCorner(ref _thumbBottomRight, Cursors.SizeNWSE);
            BuildCenter(ref _thumbCenter);

            _cropMask.PreviewKeyDown += CropMask_PreviewKeyDown;

            //Cropping handlers.
            _thumbBottomLeft.DragDelta += HandleBottomLeft;
            _thumbBottomRight.DragDelta += HandleBottomRight;
            _thumbTopLeft.DragDelta += HandleTopLeft;
            _thumbTopRight.DragDelta += HandleTopRight;
            _thumbTop.DragDelta += HandleTop;
            _thumbBottom.DragDelta += HandleBottom;
            _thumbRight.DragDelta += HandleRight;
            _thumbLeft.DragDelta += HandleLeft;
            _thumbCenter.DragDelta += HandleCenter;

            //Clipping interior should be withing the bounds of the adorned element.
            if (adornedElement is FrameworkElement element)
                element.SizeChanged += AdornedElement_SizeChanged;
        }

        private void CropMask_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            //Control + Shift: Expand both ways.
            if ((Keyboard.Modifiers & ModifierKeys.Control) != 0 && (Keyboard.Modifiers & ModifierKeys.Shift) != 0)
            {
                switch (e.Key)
                {
                    case Key.Up:
                        HandleBottom(_thumbCenter, new DragDeltaEventArgs(0, 1));
                        HandleTop(_thumbCenter, new DragDeltaEventArgs(0, -1));
                        break;
                    case Key.Down:
                        HandleBottom(_thumbCenter, new DragDeltaEventArgs(0, -1));
                        HandleTop(_thumbCenter, new DragDeltaEventArgs(0, 1));
                        break;
                    case Key.Left:
                        HandleRight(_thumbCenter, new DragDeltaEventArgs(-1, 0));
                        HandleLeft(_thumbCenter, new DragDeltaEventArgs(1, 0));
                        break;
                    case Key.Right:
                        HandleRight(_thumbCenter, new DragDeltaEventArgs(1, 0));
                        HandleLeft(_thumbCenter, new DragDeltaEventArgs(-1, 0));
                        break;
                }

                return;
            }

            //If the Shift key is pressed, the sizing mode is enabled (bottom right).
            if ((Keyboard.Modifiers & ModifierKeys.Shift) != 0)
            {
                switch (e.Key)
                {
                    case Key.Up:
                        HandleBottom(_thumbCenter, new DragDeltaEventArgs(0, -1));
                        break;
                    case Key.Down:
                        HandleBottom(_thumbCenter, new DragDeltaEventArgs(0, 1));
                        break;
                    case Key.Left:
                        HandleRight(_thumbCenter, new DragDeltaEventArgs(-1, 0));
                        break;
                    case Key.Right:
                        HandleRight(_thumbCenter, new DragDeltaEventArgs(1, 0));
                        break;
                }

                return;
            }

            //If the Control key is pressed, the sizing mode is enabled (top left).
            if ((Keyboard.Modifiers & ModifierKeys.Control) != 0)
            {
                switch (e.Key)
                {
                    case Key.Up:
                        HandleTop(_thumbCenter, new DragDeltaEventArgs(0, -1));
                        break;
                    case Key.Down:
                        HandleTop(_thumbCenter, new DragDeltaEventArgs(0, 1));
                        break;
                    case Key.Left:
                        HandleLeft(_thumbCenter, new DragDeltaEventArgs(-1, 0));
                        break;
                    case Key.Right:
                        HandleLeft(_thumbCenter, new DragDeltaEventArgs(1, 0));
                        break;
                }

                return;
            }

            //If no other key is pressed, the movement mode is enabled.
            switch (e.Key)
            {
                case Key.Up:
                    HandleCenter(_thumbCenter, new DragDeltaEventArgs(0, -1));
                    break;
                case Key.Down:
                    HandleCenter(_thumbCenter, new DragDeltaEventArgs(0, 1));
                    break;
                case Key.Left:
                    HandleCenter(_thumbCenter, new DragDeltaEventArgs(-1, 0));
                    break;
                case Key.Right:
                    HandleCenter(_thumbCenter, new DragDeltaEventArgs(1, 0));
                    break;
            }
        }

        #endregion

        #region Thumb handlers

        private void HandleThumb(double drcL, double drcT, double drcW, double drcH, double dx, double dy)
        {
            var interior = _cropMask.Interior;

            if (interior.Width + drcW * dx < 0)
                dx = -interior.Width / drcW;

            if (interior.Height + drcH * dy < 0)
                dy = -interior.Height / drcH;

            interior = new Rect(interior.Left + drcL * dx,
                interior.Top + drcT * dy,
                interior.Width + drcW * dx,
                interior.Height + drcH * dy);

            //Minimum of 10x10.
            if (interior.Width < 10)
            {
                if (interior.X + interior.Width > _cropMask.Exterior.Width)
                    interior.X = interior.Right - interior.Width;
                else
                    interior.Width = 10;
            }

            if (interior.Height < 10)
            {
                if (interior.Y + interior.Height > _cropMask.Exterior.Height)
                    interior.Y = interior.Bottom - interior.Height;
                else
                    interior.Height = 10;
            }

            _cropMask.Interior = interior;

            SetThumbs(_cropMask.Interior);
            RaiseEvent(new RoutedEventArgs(CropChangedEvent, this));

            Keyboard.Focus(_cropMask);
        }

        //Cropping from the bottom-left.
        private void HandleBottomLeft(object sender, DragDeltaEventArgs args)
        {
            if (sender is Thumb)
                HandleThumb(1, 0, -1, 1, args.HorizontalChange, args.VerticalChange);
        }

        //Cropping from the bottom-right.
        private void HandleBottomRight(object sender, DragDeltaEventArgs args)
        {
            if (sender is Thumb)
                HandleThumb(0, 0, 1, 1, args.HorizontalChange, args.VerticalChange);
        }

        //Cropping from the top-right.
        private void HandleTopRight(object sender, DragDeltaEventArgs args)
        {
            if (sender is Thumb)
                HandleThumb(0, 1, 1, -1, args.HorizontalChange, args.VerticalChange);
        }

        //Cropping from the top-left.
        private void HandleTopLeft(object sender, DragDeltaEventArgs args)
        {
            if (sender is Thumb)
                HandleThumb(1, 1, -1, -1, args.HorizontalChange, args.VerticalChange);
        }

        //Cropping from the top.
        private void HandleTop(object sender, DragDeltaEventArgs args)
        {
            if (sender is Thumb)
                HandleThumb(0, 1, 0, -1, args.HorizontalChange, args.VerticalChange);
        }

        //Cropping from the left.
        private void HandleLeft(object sender, DragDeltaEventArgs args)
        {
            if (sender is Thumb)
                HandleThumb(1, 0, -1, 0, args.HorizontalChange, args.VerticalChange);
        }

        //Cropping from the right.
        private void HandleRight(object sender, DragDeltaEventArgs args)
        {
            if (sender is Thumb)
                HandleThumb(0, 0, 1, 0, args.HorizontalChange, args.VerticalChange);
        }

        //Cropping from the bottom.
        private void HandleBottom(object sender, DragDeltaEventArgs args)
        {
            if (sender is Thumb)
                HandleThumb(0, 0, 0, 1, args.HorizontalChange, args.VerticalChange);
        }

        //Dragging the cropping selection.
        private void HandleCenter(object sender, DragDeltaEventArgs args)
        {
            if (!(sender is Thumb))
                return;

            //Creates a new Rect based on the drag.
            var interior = new Rect(_cropMask.Interior.Left + args.HorizontalChange,
                _cropMask.Interior.Top + args.VerticalChange,
                _cropMask.Interior.Width, _cropMask.Interior.Height);

            #region Limit the drag to inside the bounds

            if (interior.Left < 0)
                interior.X = 0;

            if (interior.Top < 0)
                interior.Y = 0;

            if (interior.Right > _thumbCanvas.ActualWidth)
                interior.X = _thumbCanvas.ActualWidth - interior.Width;

            if (interior.Bottom > _thumbCanvas.ActualHeight)
                interior.Y = _thumbCanvas.ActualHeight - interior.Height;

            #endregion

            _cropMask.Interior = interior;

            SetThumbs(_cropMask.Interior);
            RaiseEvent(new RoutedEventArgs(CropChangedEvent, this));

            Keyboard.Focus(_cropMask);
        }

        #endregion

        #region Other handlers

        private void AdornedElement_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (!(sender is FrameworkElement element))
                return;

            var wasChanged = false;

            double intLeft = ClipRectangle.Left, intTop = ClipRectangle.Top,
                intWidth = ClipRectangle.Width, intHeight = ClipRectangle.Height;

            if (ClipRectangle.Left > element.RenderSize.Width)
            {
                intLeft = element.RenderSize.Width;
                intWidth = 0;
                wasChanged = true;
            }

            if (ClipRectangle.Top > element.RenderSize.Height)
            {
                intTop = element.RenderSize.Height;
                intHeight = 0;
                wasChanged = true;
            }

            if (ClipRectangle.Right > element.RenderSize.Width)
            {
                intWidth = Math.Max(0, element.RenderSize.Width - intLeft);
                wasChanged = true;
            }

            if (ClipRectangle.Bottom > element.RenderSize.Height)
            {
                intHeight = Math.Max(0, element.RenderSize.Height - intTop);
                wasChanged = true;
            }

            if (wasChanged)
                ClipRectangle = new Rect(intLeft, intTop, intWidth, intHeight);
        }

        #endregion

        #region Arranging

        protected override Size ArrangeOverride(Size finalSize)
        {
            var rcExterior = new Rect(0, 0, AdornedElement.RenderSize.Width, AdornedElement.RenderSize.Height);
            _cropMask.Exterior = rcExterior;

            var rcInterior = _cropMask.Interior;
            _cropMask.Arrange(rcExterior);

            SetThumbs(rcInterior);
            _thumbCanvas.Arrange(rcExterior);
            return finalSize;
        }

        #endregion

        #region Public Methods

        public BitmapSource CropImage()
        {
            var margin = AdornerMargin();
            var rcInterior = _cropMask.Interior;

            var pxFromSize = UnitsToPx(rcInterior.Width, rcInterior.Height);

            //CroppedBitmap indexes from the upper left of the margin whereas RenderTargetBitmap renders the
            //control exclusive of the margin.  Hence our need to take the margins into account here...

            var pxFromPos = UnitsToPx(rcInterior.Left + margin.Left, rcInterior.Top + margin.Top);
            var pxWhole = UnitsToPx(AdornedElement.RenderSize.Width + margin.Left, AdornedElement.RenderSize.Height + margin.Left);
            pxFromSize.X = Math.Max(Math.Min(pxWhole.X - pxFromPos.X, pxFromSize.X), 0);
            pxFromSize.Y = Math.Max(Math.Min(pxWhole.Y - pxFromPos.Y, pxFromSize.Y), 0);

            if (pxFromSize.X == 0 || pxFromSize.Y == 0)
                return null;

            var rcFrom = new Int32Rect(pxFromPos.X, pxFromPos.Y, pxFromSize.X, pxFromSize.Y);

            var rtb = new RenderTargetBitmap(pxWhole.X, pxWhole.Y, DpiX, DpiY, PixelFormats.Default);
            rtb.Render(AdornedElement);

            return new CroppedBitmap(rtb, rcFrom);
        }

        #endregion

        #region Private Methods

        private void SetThumbs(Rect rc)
        {
            SetPosition(_thumbBottomRight, rc.Right, rc.Bottom);
            SetPosition(_thumbTopLeft, rc.Left, rc.Top);
            SetPosition(_thumbTopRight, rc.Right, rc.Top);
            SetPosition(_thumbBottomLeft, rc.Left, rc.Bottom);
            SetPosition(_thumbTop, rc.Left + rc.Width / 2, rc.Top);
            SetPosition(_thumbBottom, rc.Left + rc.Width / 2, rc.Bottom);
            SetPosition(_thumbLeft, rc.Left, rc.Top + rc.Height / 2);
            SetPosition(_thumbRight, rc.Right, rc.Top + rc.Height / 2);

            //Central thumb, used to drag the whole cropping selection.
            SetPosition(_thumbCenter, rc.Left + 5, rc.Top + 5);
            _thumbCenter.Width = rc.Right - rc.Left;
            _thumbCenter.Height = rc.Bottom - rc.Top;
        }

        private Thickness AdornerMargin()
        {
            var thick = new Thickness(0);

            if (AdornedElement is FrameworkElement element)
                thick = element.Margin;

            return thick;
        }

        private void BuildCorner(ref Thumb thumb, Cursor cursor)
        {
            if (thumb != null) return;

            thumb = new Thumb
            {
                Cursor = cursor,
                Style = (Style)FindResource("ScrollBar.Thumb"),
                Width = ThumbWidth,
                Height = ThumbWidth
            };

            _thumbCanvas.Children.Add(thumb);
        }

        private void BuildCenter(ref Thumb thumb)
        {
            if (thumb != null) return;

            thumb = new Thumb
            {
                Style = (Style)FindResource("ThumbTranparent"),
            };

            _thumbCanvas.Children.Add(thumb);
        }

        private Point UnitsToPx(double x, double y)
        {
            return new Point((int)(x * DpiX / 96), (int)(y * DpiY / 96));
        }

        private void SetPosition(Thumb thumb, double x, double y)
        {
            Canvas.SetTop(thumb, y - ThumbWidth / 2);
            Canvas.SetLeft(thumb, x - ThumbWidth / 2);
        }

        #endregion

        #region Visual Tree Override

        // Override the VisualChildrenCount and GetVisualChild properties to interface with 
        // the adorner's visual collection.
        protected override int VisualChildrenCount => _visualCollection.Count;

        protected override Visual GetVisualChild(int index)
        {
            return _visualCollection[index];
        }

        #endregion
    }
}