using System;
using System.Runtime.InteropServices;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using HandyControl.Controls;
using HandyControl.Tools.Interop;

namespace HandyControl.Data
{
    internal class GlowEdge : HwndWrapper
    {
        private const string GlowEdgeClassName = "HandyControlGlowEdge";

        private const int GlowDepth = 9;

        private const int CornerGripThickness = 18;

        private static ushort _sharedWindowClassAtom;

        // ReSharper disable once NotAccessedField.Local
        private static InteropValues.WndProc _sharedWndProc;

        private readonly GlowBitmap[] _activeGlowBitmaps = new GlowBitmap[16];

        private readonly GlowBitmap[] _inactiveGlowBitmaps = new GlowBitmap[16];

        private readonly Dock _orientation;

        private readonly GlowWindow _targetWindow;

        private Color _activeGlowColor = Colors.Transparent;

        private int _height;

        private Color _inactiveGlowColor = Colors.Transparent;

        private FieldInvalidationTypes _invalidatedValues;

        private bool _isActive;

        private bool _isVisible;

        private int _left;

        private bool _pendingDelayRender;

        private int _top;

        private int _width;

        internal static long CreatedGlowEdges { get; private set; }

        internal static long DisposedGlowEdges { get; private set; }

        internal GlowEdge(GlowWindow owner, Dock orientation)
        {
            _targetWindow = owner ?? throw new ArgumentNullException(nameof(owner));
            _orientation = orientation;
            CreatedGlowEdges += 1L;
        }

        private bool IsDeferringChanges => _targetWindow.DeferGlowChangesCount > 0;

        private static ushort SharedWindowClassAtom
        {
            get
            {
                if (_sharedWindowClassAtom == 0)
                {
                    var wndclass = default(InteropValues.WNDCLASS);

                    wndclass.cbClsExtra = 0;
                    wndclass.cbWndExtra = 0;
                    wndclass.hbrBackground = IntPtr.Zero;
                    wndclass.hCursor = IntPtr.Zero;
                    wndclass.hIcon = IntPtr.Zero;
                    wndclass.lpfnWndProc = _sharedWndProc = InteropMethods.DefWindowProc;
                    wndclass.lpszClassName = GlowEdgeClassName;
                    wndclass.lpszMenuName = null;
                    wndclass.style = 0u;

                    _sharedWindowClassAtom = InteropMethods.RegisterClass(ref wndclass);
                }

                return _sharedWindowClassAtom;
            }
        }

        internal bool IsVisible
        {
            get => _isVisible;
            set => UpdateProperty(ref _isVisible, value, FieldInvalidationTypes.Render | FieldInvalidationTypes.Visibility);
        }

        internal int Left
        {
            get => _left;
            set => UpdateProperty(ref _left, value, FieldInvalidationTypes.Location);
        }

        internal int Top
        {
            get => _top;
            set => UpdateProperty(ref _top, value, FieldInvalidationTypes.Location);
        }

        internal int Width
        {
            get => _width;
            set => UpdateProperty(ref _width, value, FieldInvalidationTypes.Size | FieldInvalidationTypes.Render);
        }

        internal int Height
        {
            get => _height;
            set => UpdateProperty(ref _height, value, FieldInvalidationTypes.Size | FieldInvalidationTypes.Render);
        }

        internal bool IsActive
        {
            get => _isActive;
            set => UpdateProperty(ref _isActive, value, FieldInvalidationTypes.Render);
        }

        internal Color ActiveGlowColor
        {
            get => _activeGlowColor;
            set => UpdateProperty(ref _activeGlowColor, value, FieldInvalidationTypes.ActiveColor | FieldInvalidationTypes.Render);
        }

        internal Color InactiveGlowColor
        {
            get => _inactiveGlowColor;
            set => UpdateProperty(ref _inactiveGlowColor, value, FieldInvalidationTypes.InactiveColor | FieldInvalidationTypes.Render);
        }

        private IntPtr TargetWindowHandle => new WindowInteropHelper(_targetWindow).Handle;

        protected override bool IsWindowSubclassed => true;

        private bool IsPositionValid => (_invalidatedValues & (FieldInvalidationTypes.Location | FieldInvalidationTypes.Size | FieldInvalidationTypes.Visibility)) == FieldInvalidationTypes.None;

        private void UpdateProperty<T>(ref T field, T value, FieldInvalidationTypes invalidatedValues) where T : struct
        {
            if (!field.Equals(value))
            {
                field = value;
                _invalidatedValues |= invalidatedValues;
                if (!IsDeferringChanges) CommitChanges();
            }
        }

        protected override ushort CreateWindowClassCore() => SharedWindowClassAtom;

        protected override void DestroyWindowClassCore()
        {
        }

        protected override IntPtr CreateWindowCore()
        {
            return InteropMethods.CreateWindowEx(
                524416,
                new IntPtr(WindowClassAtom),
                string.Empty,
                -2046820352,
                0,
                0,
                0,
                0,
                new WindowInteropHelper(_targetWindow).Owner,
                IntPtr.Zero,
                IntPtr.Zero,
                IntPtr.Zero);
        }

        internal void ChangeOwner(IntPtr newOwner) => InteropMethods.SetWindowLongPtr(Handle, InteropValues.GWLP.HWNDPARENT, newOwner);

        protected override IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam)
        {
            if (msg <= 70)
            {
                if (msg == 6) return IntPtr.Zero;
                if (msg == 70)
                {
                    var windowpos = (InteropValues.WINDOWPOS) Marshal.PtrToStructure(lParam, typeof(InteropValues.WINDOWPOS));
                    windowpos.flags |= 16u;
                    Marshal.StructureToPtr(windowpos, lParam, true);
                }
            }
            else
            {
                if (msg != 126)
                {
                    if (msg == 132) return new IntPtr(WmNcHitTest(lParam));
                    switch (msg)
                    {
                        case 161:
                        case 163:
                        case 164:
                        case 166:
                        case 167:
                        case 169:
                        case 171:
                        case 173:
                            {
                                var targetWindowHandle = TargetWindowHandle;
                                InteropMethods.SendMessage(targetWindowHandle, 6, new IntPtr(2), IntPtr.Zero);
                                InteropMethods.SendMessage(targetWindowHandle, msg, wParam, IntPtr.Zero);
                                return IntPtr.Zero;
                            }
                    }
                }
                else
                {
                    if (IsVisible) RenderLayeredWindow();
                }
            }

            return base.WndProc(hwnd, msg, wParam, lParam);
        }

        private int WmNcHitTest(IntPtr lParam)
        {
            var xLParam = InteropMethods.GetXLParam(lParam.ToInt32());
            var yLParam = InteropMethods.GetYLParam(lParam.ToInt32());
            InteropMethods.GetWindowRect(Handle, out var rect);
            switch (_orientation)
            {
                case Dock.Left:
                    if (yLParam - CornerGripThickness < rect.Top) return 13;
                    if (yLParam + CornerGripThickness > rect.Bottom) return 16;
                    return 10;
                case Dock.Top:
                    if (xLParam - CornerGripThickness < rect.Left) return 13;
                    if (xLParam + CornerGripThickness > rect.Right) return 14;
                    return 12;
                case Dock.Right:
                    if (yLParam - CornerGripThickness < rect.Top) return 14;
                    if (yLParam + CornerGripThickness > rect.Bottom) return 17;
                    return 11;
                default:
                    if (xLParam - CornerGripThickness < rect.Left) return 16;
                    if (xLParam + CornerGripThickness > rect.Right) return 17;
                    return 15;
            }
        }

        internal void CommitChanges()
        {
            InvalidateCachedBitmaps();
            UpdateWindowPosCore();
            UpdateLayeredWindowCore();
            _invalidatedValues = FieldInvalidationTypes.None;
        }

        private void InvalidateCachedBitmaps()
        {
            if (_invalidatedValues.HasFlag(FieldInvalidationTypes.ActiveColor)) ClearCache(_activeGlowBitmaps);
            if (_invalidatedValues.HasFlag(FieldInvalidationTypes.InactiveColor)) ClearCache(_inactiveGlowBitmaps);
        }

        private void UpdateWindowPosCore()
        {
            if (_invalidatedValues.HasFlag(FieldInvalidationTypes.Location) ||
                _invalidatedValues.HasFlag(FieldInvalidationTypes.Size) ||
                _invalidatedValues.HasFlag(FieldInvalidationTypes.Visibility))
            {
                var num = 532;
                if (_invalidatedValues.HasFlag(FieldInvalidationTypes.Visibility))
                {
                    if (IsVisible)
                        num |= 64;
                    else
                        num |= 131;
                }

                if (!_invalidatedValues.HasFlag(FieldInvalidationTypes.Location)) num |= 2;
                if (!_invalidatedValues.HasFlag(FieldInvalidationTypes.Size)) num |= 1;
                InteropMethods.SetWindowPos(Handle, IntPtr.Zero, Left, Top, Width, Height, num);
            }
        }

        private void UpdateLayeredWindowCore()
        {
            if (IsVisible && _invalidatedValues.HasFlag(FieldInvalidationTypes.Render))
            {
                if (IsPositionValid)
                {
                    BeginDelayedRender();
                    return;
                }

                CancelDelayedRender();
                RenderLayeredWindow();
            }
        }

        private void BeginDelayedRender()
        {
            if (!_pendingDelayRender)
            {
                _pendingDelayRender = true;
                CompositionTarget.Rendering += CommitDelayedRender;
            }
        }

        private void CancelDelayedRender()
        {
            if (_pendingDelayRender)
            {
                _pendingDelayRender = false;
                CompositionTarget.Rendering -= CommitDelayedRender;
            }
        }

        private void CommitDelayedRender(object sender, EventArgs e)
        {
            CancelDelayedRender();
            if (IsVisible) RenderLayeredWindow();
        }

        private void RenderLayeredWindow()
        {
            using var glowDrawingContext = new GlowDrawingContext(Width, Height);
            if (glowDrawingContext.IsInitialized)
            {
                switch (_orientation)
                {
                    case Dock.Left:
                        DrawLeft(glowDrawingContext);
                        break;
                    case Dock.Top:
                        DrawTop(glowDrawingContext);
                        break;
                    case Dock.Right:
                        DrawRight(glowDrawingContext);
                        break;
                    default:
                        DrawBottom(glowDrawingContext);
                        break;
                }

                var point = new InteropValues.POINT
                {
                    X = Left,
                    Y = Top
                };
                var size = new InteropValues.SIZE
                {
                    cx = Width,
                    cy = Height
                };
                var point2 = new InteropValues.POINT
                {
                    X = 0,
                    Y = 0
                };

                InteropMethods.UpdateLayeredWindow(
                    Handle,
                    glowDrawingContext.ScreenDC,
                    ref point,
                    ref size,
                    glowDrawingContext.WindowDC,
                    ref point2,
                    0u,
                    ref glowDrawingContext.Blend,
                    2u);
            }
        }

        private GlowBitmap GetOrCreateBitmap(GlowDrawingContext drawingContext, GlowBitmapPart bitmapPart)
        {
            GlowBitmap[] array;
            Color color;
            if (IsActive)
            {
                array = _activeGlowBitmaps;
                color = ActiveGlowColor;
            }
            else
            {
                array = _inactiveGlowBitmaps;
                color = InactiveGlowColor;
            }

            return array[(int) bitmapPart] ?? (array[(int) bitmapPart] = GlowBitmap.Create(drawingContext, bitmapPart, color));
        }

        private void ClearCache(GlowBitmap[] cache)
        {
            for (var i = 0; i < cache.Length; i++)
                using (cache[i])
                {
                    cache[i] = null;
                }
        }

        protected override void DisposeManagedResources()
        {
            ClearCache(_activeGlowBitmaps);
            ClearCache(_inactiveGlowBitmaps);
        }

        protected override void DisposeNativeResources()
        {
            base.DisposeNativeResources();
            DisposedGlowEdges += 1L;
        }

        private void DrawLeft(GlowDrawingContext drawingContext)
        {
            var orCreateBitmap = GetOrCreateBitmap(drawingContext, GlowBitmapPart.CornerTopLeft);
            var orCreateBitmap2 = GetOrCreateBitmap(drawingContext, GlowBitmapPart.LeftTop);
            var orCreateBitmap3 = GetOrCreateBitmap(drawingContext, GlowBitmapPart.Left);
            var orCreateBitmap4 = GetOrCreateBitmap(drawingContext, GlowBitmapPart.LeftBottom);
            var orCreateBitmap5 = GetOrCreateBitmap(drawingContext, GlowBitmapPart.CornerBottomLeft);
            var height = orCreateBitmap.Height;
            var num = height + orCreateBitmap2.Height;
            var num2 = drawingContext.Height - orCreateBitmap5.Height;
            var num3 = num2 - orCreateBitmap4.Height;
            var num4 = num3 - num;
            InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap.Handle);
            InteropMethods.AlphaBlend(drawingContext.WindowDC, 0, 0, orCreateBitmap.Width, orCreateBitmap.Height,
                drawingContext.BackgroundDC, 0, 0, orCreateBitmap.Width, orCreateBitmap.Height,
                drawingContext.Blend);
            InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap2.Handle);
            InteropMethods.AlphaBlend(drawingContext.WindowDC, 0, height, orCreateBitmap2.Width,
                orCreateBitmap2.Height, drawingContext.BackgroundDC, 0, 0, orCreateBitmap2.Width,
                orCreateBitmap2.Height, drawingContext.Blend);
            if (num4 > 0)
            {
                InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap3.Handle);
                InteropMethods.AlphaBlend(drawingContext.WindowDC, 0, num, orCreateBitmap3.Width, num4,
                    drawingContext.BackgroundDC, 0, 0, orCreateBitmap3.Width, orCreateBitmap3.Height,
                    drawingContext.Blend);
            }

            InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap4.Handle);
            InteropMethods.AlphaBlend(drawingContext.WindowDC, 0, num3, orCreateBitmap4.Width,
                orCreateBitmap4.Height, drawingContext.BackgroundDC, 0, 0, orCreateBitmap4.Width,
                orCreateBitmap4.Height, drawingContext.Blend);
            InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap5.Handle);
            InteropMethods.AlphaBlend(drawingContext.WindowDC, 0, num2, orCreateBitmap5.Width,
                orCreateBitmap5.Height, drawingContext.BackgroundDC, 0, 0, orCreateBitmap5.Width,
                orCreateBitmap5.Height, drawingContext.Blend);
        }

        private void DrawRight(GlowDrawingContext drawingContext)
        {
            var orCreateBitmap = GetOrCreateBitmap(drawingContext, GlowBitmapPart.CornerTopRight);
            var orCreateBitmap2 = GetOrCreateBitmap(drawingContext, GlowBitmapPart.RightTop);
            var orCreateBitmap3 = GetOrCreateBitmap(drawingContext, GlowBitmapPart.Right);
            var orCreateBitmap4 = GetOrCreateBitmap(drawingContext, GlowBitmapPart.RightBottom);
            var orCreateBitmap5 = GetOrCreateBitmap(drawingContext, GlowBitmapPart.CornerBottomRight);
            var height = orCreateBitmap.Height;
            var num = height + orCreateBitmap2.Height;
            var num2 = drawingContext.Height - orCreateBitmap5.Height;
            var num3 = num2 - orCreateBitmap4.Height;
            var num4 = num3 - num;
            InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap.Handle);
            InteropMethods.AlphaBlend(drawingContext.WindowDC, 0, 0, orCreateBitmap.Width, orCreateBitmap.Height,
                drawingContext.BackgroundDC, 0, 0, orCreateBitmap.Width, orCreateBitmap.Height,
                drawingContext.Blend);
            InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap2.Handle);
            InteropMethods.AlphaBlend(drawingContext.WindowDC, 0, height, orCreateBitmap2.Width,
                orCreateBitmap2.Height, drawingContext.BackgroundDC, 0, 0, orCreateBitmap2.Width,
                orCreateBitmap2.Height, drawingContext.Blend);
            if (num4 > 0)
            {
                InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap3.Handle);
                InteropMethods.AlphaBlend(drawingContext.WindowDC, 0, num, orCreateBitmap3.Width, num4,
                    drawingContext.BackgroundDC, 0, 0, orCreateBitmap3.Width, orCreateBitmap3.Height,
                    drawingContext.Blend);
            }

            InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap4.Handle);
            InteropMethods.AlphaBlend(drawingContext.WindowDC, 0, num3, orCreateBitmap4.Width,
                orCreateBitmap4.Height, drawingContext.BackgroundDC, 0, 0, orCreateBitmap4.Width,
                orCreateBitmap4.Height, drawingContext.Blend);
            InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap5.Handle);
            InteropMethods.AlphaBlend(drawingContext.WindowDC, 0, num2, orCreateBitmap5.Width,
                orCreateBitmap5.Height, drawingContext.BackgroundDC, 0, 0, orCreateBitmap5.Width,
                orCreateBitmap5.Height, drawingContext.Blend);
        }

        private void DrawTop(GlowDrawingContext drawingContext)
        {
            var orCreateBitmap = GetOrCreateBitmap(drawingContext, GlowBitmapPart.TopLeft);
            var orCreateBitmap2 = GetOrCreateBitmap(drawingContext, GlowBitmapPart.Top);
            var orCreateBitmap3 = GetOrCreateBitmap(drawingContext, GlowBitmapPart.TopRight);
            var num2 = GlowDepth + orCreateBitmap.Width;
            var num3 = drawingContext.Width - GlowDepth - orCreateBitmap3.Width;
            var num4 = num3 - num2;
            InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap.Handle);
            InteropMethods.AlphaBlend(drawingContext.WindowDC, GlowDepth, 0, orCreateBitmap.Width, orCreateBitmap.Height,
                drawingContext.BackgroundDC, 0, 0, orCreateBitmap.Width, orCreateBitmap.Height,
                drawingContext.Blend);
            if (num4 > 0)
            {
                InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap2.Handle);
                InteropMethods.AlphaBlend(drawingContext.WindowDC, num2, 0, num4, orCreateBitmap2.Height,
                    drawingContext.BackgroundDC, 0, 0, orCreateBitmap2.Width, orCreateBitmap2.Height,
                    drawingContext.Blend);
            }

            InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap3.Handle);
            InteropMethods.AlphaBlend(drawingContext.WindowDC, num3, 0, orCreateBitmap3.Width,
                orCreateBitmap3.Height, drawingContext.BackgroundDC, 0, 0, orCreateBitmap3.Width,
                orCreateBitmap3.Height, drawingContext.Blend);
        }

        private void DrawBottom(GlowDrawingContext drawingContext)
        {
            var orCreateBitmap = GetOrCreateBitmap(drawingContext, GlowBitmapPart.BottomLeft);
            var orCreateBitmap2 = GetOrCreateBitmap(drawingContext, GlowBitmapPart.Bottom);
            var orCreateBitmap3 = GetOrCreateBitmap(drawingContext, GlowBitmapPart.BottomRight);
            var num2 = GlowDepth + orCreateBitmap.Width;
            var num3 = drawingContext.Width - GlowDepth - orCreateBitmap3.Width;
            var num4 = num3 - num2;
            InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap.Handle);
            InteropMethods.AlphaBlend(drawingContext.WindowDC, GlowDepth, 0, orCreateBitmap.Width, orCreateBitmap.Height,
                drawingContext.BackgroundDC, 0, 0, orCreateBitmap.Width, orCreateBitmap.Height,
                drawingContext.Blend);
            if (num4 > 0)
            {
                InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap2.Handle);
                InteropMethods.AlphaBlend(drawingContext.WindowDC, num2, 0, num4, orCreateBitmap2.Height,
                    drawingContext.BackgroundDC, 0, 0, orCreateBitmap2.Width, orCreateBitmap2.Height,
                    drawingContext.Blend);
            }

            InteropMethods.SelectObject(drawingContext.BackgroundDC, orCreateBitmap3.Handle);
            InteropMethods.AlphaBlend(drawingContext.WindowDC, num3, 0, orCreateBitmap3.Width,
                orCreateBitmap3.Height, drawingContext.BackgroundDC, 0, 0, orCreateBitmap3.Width,
                orCreateBitmap3.Height, drawingContext.Blend);
        }

        internal void UpdateWindowPos()
        {
            var targetWindowHandle = TargetWindowHandle;
            InteropMethods.GetWindowRect(targetWindowHandle, out var rect);
            InteropMethods.GetWindowPlacement(targetWindowHandle);
            if (IsVisible)
                switch (_orientation)
                {
                    case Dock.Left:
                        Left = rect.Left - GlowDepth;
                        Top = rect.Top - GlowDepth;
                        Width = GlowDepth;
                        Height = rect.Height + CornerGripThickness;
                        return;
                    case Dock.Top:
                        Left = rect.Left - GlowDepth;
                        Top = rect.Top - GlowDepth;
                        Width = rect.Width + CornerGripThickness;
                        Height = GlowDepth;
                        return;
                    case Dock.Right:
                        Left = rect.Right;
                        Top = rect.Top - GlowDepth;
                        Width = GlowDepth;
                        Height = rect.Height + CornerGripThickness;
                        return;
                    default:
                        Left = rect.Left - GlowDepth;
                        Top = rect.Bottom;
                        Width = rect.Width + CornerGripThickness;
                        Height = GlowDepth;
                        break;
                }
        }

        [Flags]
        private enum FieldInvalidationTypes
        {
            None = 0,
            Location = 1,
            Size = 2,
            ActiveColor = 4,
            InactiveColor = 8,
            Render = 16,
            Visibility = 32
        }
    }
}
