using System;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;
using Dragablz.Core;
using Dragablz.Dockablz;
using Dragablz.Referenceless;

namespace Dragablz
{
    /// <summary>
    /// It is not necessary to use a <see cref="DragablzWindow"/> to gain tab dragging features.
    /// What this Window does is allow a quick way to remove the Window border, and support transparency whilst
    /// dragging.  
    /// </summary>
    [TemplatePart(Name = WindowSurfaceGridPartName, Type = typeof(Grid))]
    [TemplatePart(Name = WindowRestoreThumbPartName, Type = typeof(Thumb))]
    [TemplatePart(Name = WindowResizeThumbPartName, Type = typeof(Thumb))]
    public class DragablzWindow : Window
    {
        public const string WindowSurfaceGridPartName = "PART_WindowSurface";
        public const string WindowRestoreThumbPartName = "PART_WindowRestoreThumb";
        public const string WindowResizeThumbPartName = "PART_WindowResizeThumb";
        private readonly SerialDisposable _templateSubscription = new SerialDisposable();

        public static RoutedCommand CloseWindowCommand = new RoutedCommand();
        public static RoutedCommand RestoreWindowCommand = new RoutedCommand();
        public static RoutedCommand MaximizeWindowCommand = new RoutedCommand();
        public static RoutedCommand MinimizeWindowCommand = new RoutedCommand();        

        private const int ResizeMargin = 4;
        private Size _sizeWhenResizeBegan;
        private Point _screenMousePointWhenResizeBegan;
        private Point _windowLocationPointWhenResizeBegan;
        private SizeGrip _resizeType;

        private static SizeGrip[] _leftMode = new[] { SizeGrip.TopLeft, SizeGrip.Left, SizeGrip.BottomLeft };
        private static SizeGrip[] _rightMode = new[] { SizeGrip.TopRight, SizeGrip.Right, SizeGrip.BottomRight };
        private static SizeGrip[] _topMode = new[] { SizeGrip.TopLeft, SizeGrip.Top, SizeGrip.TopRight };
        private static SizeGrip[] _bottomMode = new[] { SizeGrip.BottomLeft, SizeGrip.Bottom, SizeGrip.BottomRight };

        private static double _xScale = 1;
        private static double _yScale = 1;
        private static bool _dpiInitialized = false;

        static DragablzWindow()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(DragablzWindow), new FrameworkPropertyMetadata(typeof(DragablzWindow)));            
        }

        public DragablzWindow()
        {
            AddHandler(DragablzItem.DragStarted, new DragablzDragStartedEventHandler(ItemDragStarted), true);
            AddHandler(DragablzItem.DragCompleted, new DragablzDragCompletedEventHandler(ItemDragCompleted), true);
            CommandBindings.Add(new CommandBinding(CloseWindowCommand, CloseWindowExecuted));
            CommandBindings.Add(new CommandBinding(MaximizeWindowCommand, MaximizeWindowExecuted));
            CommandBindings.Add(new CommandBinding(MinimizeWindowCommand, MinimizeWindowExecuted));
            CommandBindings.Add(new CommandBinding(RestoreWindowCommand, RestoreWindowExecuted));                        
        }

        private static readonly DependencyPropertyKey IsWindowBeingDraggedByTabPropertyKey =
            DependencyProperty.RegisterReadOnly(
                "IsBeingDraggedByTab", typeof (bool), typeof (DragablzWindow),
                new PropertyMetadata(default(bool)));

        public static readonly DependencyProperty IsBeingDraggedByTabProperty =
            IsWindowBeingDraggedByTabPropertyKey.DependencyProperty;

        public bool IsBeingDraggedByTab
        {
            get { return (bool) GetValue(IsBeingDraggedByTabProperty); }
            private set { SetValue(IsWindowBeingDraggedByTabPropertyKey, value); }
        }        

        private void ItemDragCompleted(object sender, DragablzDragCompletedEventArgs e)
        {            
            IsBeingDraggedByTab = false;
        }

        private void ItemDragStarted(object sender, DragablzDragStartedEventArgs e)
        {
            var sourceOfDragItemsControl = ItemsControl.ItemsControlFromItemContainer(e.DragablzItem) as DragablzItemsControl;
            if (sourceOfDragItemsControl == null) return;

            var sourceTab = TabablzControl.GetOwnerOfHeaderItems(sourceOfDragItemsControl);
            if (sourceTab == null) return;

            if (sourceOfDragItemsControl.Items.Count != 1
                || (sourceTab.InterTabController != null && !sourceTab.InterTabController.MoveWindowWithSolitaryTabs)
                || Layout.IsContainedWithinBranch(sourceOfDragItemsControl))
                return;

            IsBeingDraggedByTab = true;
        }

        public override void OnApplyTemplate()
        {
            var windowSurfaceGrid = GetTemplateChild(WindowSurfaceGridPartName) as Grid;
            var windowRestoreThumb = GetTemplateChild(WindowRestoreThumbPartName) as Thumb;
            var windowResizeThumb = GetTemplateChild(WindowResizeThumbPartName) as Thumb;

            _templateSubscription.Disposable = Disposable.Create(() =>
            {
                if (windowSurfaceGrid != null)
                {
                    windowSurfaceGrid.MouseLeftButtonDown -= WindowSurfaceGridOnMouseLeftButtonDown;
                }

                if (windowRestoreThumb != null)
                {
                    windowRestoreThumb.DragDelta -= WindowMoveThumbOnDragDelta;
                    windowRestoreThumb.MouseDoubleClick -= WindowRestoreThumbOnMouseDoubleClick;
                }

                if (windowResizeThumb == null) return;

                windowResizeThumb.MouseMove -= WindowResizeThumbOnMouseMove;
                windowResizeThumb.DragStarted -= WindowResizeThumbOnDragStarted;
                windowResizeThumb.DragDelta -= WindowResizeThumbOnDragDelta;
                windowResizeThumb.DragCompleted -= WindowResizeThumbOnDragCompleted;
            });

            base.OnApplyTemplate();

            if (windowSurfaceGrid != null)
            {
                windowSurfaceGrid.MouseLeftButtonDown += WindowSurfaceGridOnMouseLeftButtonDown;
            }

            if (windowRestoreThumb != null)
            {
                windowRestoreThumb.DragDelta += WindowMoveThumbOnDragDelta;
                windowRestoreThumb.MouseDoubleClick += WindowRestoreThumbOnMouseDoubleClick;
            }

            if (windowResizeThumb == null) return;

            windowResizeThumb.MouseMove += WindowResizeThumbOnMouseMove;                
            windowResizeThumb.DragStarted += WindowResizeThumbOnDragStarted;
            windowResizeThumb.DragDelta += WindowResizeThumbOnDragDelta;
            windowResizeThumb.DragCompleted += WindowResizeThumbOnDragCompleted;
        }

        protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            var resizeThumb = GetTemplateChild(WindowResizeThumbPartName) as Thumb;
            if (resizeThumb != null)
            {
                var outerRectangleGeometry = new RectangleGeometry(new Rect(sizeInfo.NewSize));
                var innerRectangleGeometry =
                    new RectangleGeometry(new Rect(ResizeMargin, ResizeMargin, sizeInfo.NewSize.Width - ResizeMargin * 2, sizeInfo.NewSize.Height - ResizeMargin*2));
                resizeThumb.Clip = new CombinedGeometry(GeometryCombineMode.Exclude, outerRectangleGeometry,
                    innerRectangleGeometry);
            }

            base.OnRenderSizeChanged(sizeInfo);
        }

        protected IntPtr CriticalHandle
        {
            get
            {
                var value = typeof (Window).GetProperty("CriticalHandle", BindingFlags.NonPublic | BindingFlags.Instance)
                    .GetValue(this, new object[0]);
                return (IntPtr) value;
            }
        }

        private void WindowSurfaceGridOnMouseLeftButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
        {
            if (mouseButtonEventArgs.ChangedButton != MouseButton.Left) return;
            if (mouseButtonEventArgs.ClickCount == 1)
                DragMove();
            if (mouseButtonEventArgs.ClickCount == 2)
                WindowState = WindowState.Maximized;
        }

        private static void WindowResizeThumbOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
        {
            var thumb = (Thumb)sender;
            var mousePositionInThumb = Mouse.GetPosition(thumb);
            thumb.Cursor = SelectCursor(SelectSizingMode(mousePositionInThumb, thumb.RenderSize));
        }

        private void WindowRestoreThumbOnMouseDoubleClick(object sender, MouseButtonEventArgs mouseButtonEventArgs)
        {            
            WindowState = WindowState.Normal;            
        }

        private void WindowResizeThumbOnDragCompleted(object sender, DragCompletedEventArgs dragCompletedEventArgs)
        {
            Cursor = Cursors.Arrow;
        }

        private void WindowResizeThumbOnDragDelta(object sender, DragDeltaEventArgs dragDeltaEventArgs)
        {
            var mousePositionInWindow = Mouse.GetPosition(this);
            var currentScreenMousePoint = PointToScreen(mousePositionInWindow);

            var width = _sizeWhenResizeBegan.Width;
            var height = _sizeWhenResizeBegan.Height;
            var left = _windowLocationPointWhenResizeBegan.X;
            var top = _windowLocationPointWhenResizeBegan.Y;

            if (_leftMode.Contains(_resizeType))
            {
                var diff = currentScreenMousePoint.X - _screenMousePointWhenResizeBegan.X;
                diff /= _xScale;
                var suggestedWidth = width + -diff;
                left += diff;
                width = suggestedWidth;
            }
            if (_rightMode.Contains(_resizeType))
            {
                var diff = currentScreenMousePoint.X - _screenMousePointWhenResizeBegan.X;
                diff /= _xScale;
                width += diff;
            }
            if (_topMode.Contains(_resizeType))
            {
                var diff = currentScreenMousePoint.Y - _screenMousePointWhenResizeBegan.Y;
                diff /= _yScale;
                height += -diff;
                top += diff;
            }
            if (_bottomMode.Contains(_resizeType))
            {
                var diff = currentScreenMousePoint.Y - _screenMousePointWhenResizeBegan.Y;
                diff /= _yScale; 
                height += diff;
            }

            width = Math.Max(MinWidth, width);
            height = Math.Max(MinHeight, height);   
            //TODO must try harder.
            left = Math.Min(left, _windowLocationPointWhenResizeBegan.X + _sizeWhenResizeBegan.Width - ResizeMargin*4);
            //TODO must try harder.
            top = Math.Min(top, _windowLocationPointWhenResizeBegan.Y + _sizeWhenResizeBegan.Height - ResizeMargin * 4);
            SetCurrentValue(WidthProperty, width);
            SetCurrentValue(HeightProperty, height);
            SetCurrentValue(LeftProperty, left);
            SetCurrentValue(TopProperty, top);
        }

        private void GetDPI()
        {
            if (_dpiInitialized)
            {
                return;
            }

            Matrix m = PresentationSource.FromVisual(this).CompositionTarget.TransformToDevice;
            _xScale = m.M11;
            _yScale = m.M22;
            _dpiInitialized = true;
        }

        private void WindowResizeThumbOnDragStarted(object sender, DragStartedEventArgs dragStartedEventArgs)
        {
            _sizeWhenResizeBegan = new Size(ActualWidth, ActualHeight);
            _windowLocationPointWhenResizeBegan = new Point(Left, Top);
            var mousePositionInWindow = Mouse.GetPosition(this);
            _screenMousePointWhenResizeBegan = PointToScreen(mousePositionInWindow);

            var thumb = (Thumb)sender;
            var mousePositionInThumb = Mouse.GetPosition(thumb);
            _resizeType = SelectSizingMode(mousePositionInThumb, thumb.RenderSize);

            GetDPI();
        }

        private static SizeGrip SelectSizingMode(Point mousePositionInThumb, Size thumbSize)
        {
            if (mousePositionInThumb.X <= ResizeMargin)
            {
                if (mousePositionInThumb.Y <= ResizeMargin)
                    return SizeGrip.TopLeft;
                if (mousePositionInThumb.Y >= thumbSize.Height - ResizeMargin)
                    return SizeGrip.BottomLeft;
                return SizeGrip.Left;
            }

            if (mousePositionInThumb.X >= thumbSize.Width - ResizeMargin)
            {
                if (mousePositionInThumb.Y <= ResizeMargin)
                    return SizeGrip.TopRight;
                if (mousePositionInThumb.Y >= thumbSize.Height - ResizeMargin)
                    return SizeGrip.BottomRight;
                return SizeGrip.Right;
            }

            if (mousePositionInThumb.Y <= ResizeMargin)
                return SizeGrip.Top;

            return SizeGrip.Bottom;
        }

        private static Cursor SelectCursor(SizeGrip sizeGrip)
        {
            switch (sizeGrip)
            {                
                case SizeGrip.Left:
                    return Cursors.SizeWE;
                case SizeGrip.TopLeft:
                    return Cursors.SizeNWSE;
                case SizeGrip.Top:
                    return Cursors.SizeNS;
                case SizeGrip.TopRight:
                    return Cursors.SizeNESW;
                case SizeGrip.Right:
                    return Cursors.SizeWE;
                case SizeGrip.BottomRight:
                    return Cursors.SizeNWSE;
                case SizeGrip.Bottom:
                    return Cursors.SizeNS;
                case SizeGrip.BottomLeft:
                    return Cursors.SizeNESW;
                default:
                    return Cursors.Arrow;
            }
        }

        private void WindowMoveThumbOnDragDelta(object sender, DragDeltaEventArgs dragDeltaEventArgs)
        {
            if (WindowState != WindowState.Maximized ||
                (!(Math.Abs(dragDeltaEventArgs.HorizontalChange) > 2) &&
                 !(Math.Abs(dragDeltaEventArgs.VerticalChange) > 2))) return;

            var cursorPos = Native.GetRawCursorPos();
            WindowState = WindowState.Normal;

            GetDPI();

            Top = cursorPos.Y / _yScale - 2;
            Left = cursorPos.X / _xScale - RestoreBounds.Width / 2;
            
            var lParam = (int)(uint)cursorPos.X | (cursorPos.Y << 16);
            Native.SendMessage(CriticalHandle, WindowMessage.WM_LBUTTONUP, (IntPtr)HitTest.HT_CAPTION,
                (IntPtr)lParam);
            Native.SendMessage(CriticalHandle, WindowMessage.WM_SYSCOMMAND, (IntPtr)SystemCommand.SC_MOUSEMOVE,
                IntPtr.Zero);            
        }

        private void RestoreWindowExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            Native.PostMessage(new WindowInteropHelper(this).Handle, WindowMessage.WM_SYSCOMMAND, (IntPtr)SystemCommand.SC_RESTORE, IntPtr.Zero);
        }

        private void MinimizeWindowExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            Native.PostMessage(new WindowInteropHelper(this).Handle, WindowMessage.WM_SYSCOMMAND, (IntPtr)SystemCommand.SC_MINIMIZE, IntPtr.Zero);
        }

        private void MaximizeWindowExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            Native.PostMessage(new WindowInteropHelper(this).Handle, WindowMessage.WM_SYSCOMMAND, (IntPtr)SystemCommand.SC_MAXIMIZE, IntPtr.Zero);
        }

        private void CloseWindowExecuted(object sender, ExecutedRoutedEventArgs executedRoutedEventArgs)
        {
            Native.PostMessage(new WindowInteropHelper(this).Handle, WindowMessage.WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
        }             
    }
}
