using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using SixLabors.ImageSharp.PixelFormats;
using Xilium.CefGlue;
using static Robust.Client.WebView.Cef.CefKeyCodes.ChromiumKeyboardCode;
using static Robust.Client.Input.Keyboard;

namespace Robust.Client.WebView.Cef
{
    internal partial class WebViewManagerCef
    {
        private readonly List<ControlImpl> _activeControls = new();

        public IWebViewControlImpl MakeControlImpl(WebViewControl owner)
        {
            var shader = _prototypeManager.Index<ShaderPrototype>("bgra");
            var shaderInstance = shader.Instance();
            var impl =  new ControlImpl(this, owner, shaderInstance);
            _dependencyCollection.InjectDependencies(impl);
            return impl;
        }

        private sealed class ControlImpl : IWebViewControlImpl
        {
            private static readonly Dictionary<Key, CefKeyCodes.ChromiumKeyboardCode> KeyMap = new()
            {
                [Key.A] = VKEY_A,
                [Key.B] = VKEY_B,
                [Key.C] = VKEY_C,
                [Key.D] = VKEY_D,
                [Key.E] = VKEY_E,
                [Key.F] = VKEY_F,
                [Key.G] = VKEY_G,
                [Key.H] = VKEY_H,
                [Key.I] = VKEY_I,
                [Key.J] = VKEY_J,
                [Key.K] = VKEY_K,
                [Key.L] = VKEY_L,
                [Key.M] = VKEY_M,
                [Key.N] = VKEY_N,
                [Key.O] = VKEY_O,
                [Key.P] = VKEY_P,
                [Key.Q] = VKEY_Q,
                [Key.R] = VKEY_R,
                [Key.S] = VKEY_S,
                [Key.T] = VKEY_T,
                [Key.U] = VKEY_U,
                [Key.V] = VKEY_V,
                [Key.W] = VKEY_W,
                [Key.X] = VKEY_X,
                [Key.Y] = VKEY_Y,
                [Key.Z] = VKEY_Z,
                [Key.Num0] = VKEY_0,
                [Key.Num1] = VKEY_1,
                [Key.Num2] = VKEY_2,
                [Key.Num3] = VKEY_3,
                [Key.Num4] = VKEY_4,
                [Key.Num5] = VKEY_5,
                [Key.Num6] = VKEY_6,
                [Key.Num7] = VKEY_7,
                [Key.Num8] = VKEY_8,
                [Key.Num9] = VKEY_9,
                [Key.NumpadNum0] = VKEY_NUMPAD0,
                [Key.NumpadNum1] = VKEY_NUMPAD1,
                [Key.NumpadNum2] = VKEY_NUMPAD2,
                [Key.NumpadNum3] = VKEY_NUMPAD3,
                [Key.NumpadNum4] = VKEY_NUMPAD4,
                [Key.NumpadNum5] = VKEY_NUMPAD5,
                [Key.NumpadNum6] = VKEY_NUMPAD6,
                [Key.NumpadNum7] = VKEY_NUMPAD7,
                [Key.NumpadNum8] = VKEY_NUMPAD8,
                [Key.NumpadNum9] = VKEY_NUMPAD9,
                [Key.Escape] = VKEY_ESCAPE,
                [Key.Control] = VKEY_CONTROL,
                [Key.Shift] = VKEY_SHIFT,
                [Key.Alt] = VKEY_MENU,
                [Key.LSystem] = VKEY_LWIN,
                [Key.RSystem] = VKEY_RWIN,
                [Key.LBracket] = VKEY_OEM_4,
                [Key.RBracket] = VKEY_OEM_6,
                [Key.SemiColon] = VKEY_OEM_1,
                [Key.Comma] = VKEY_OEM_COMMA,
                [Key.Period] = VKEY_OEM_PERIOD,
                [Key.Apostrophe] = VKEY_OEM_7,
                [Key.Slash] = VKEY_OEM_2,
                [Key.BackSlash] = VKEY_OEM_5,
                [Key.Tilde] = VKEY_OEM_3,
                [Key.Equal] = VKEY_OEM_PLUS,
                [Key.Space] = VKEY_SPACE,
                [Key.Return] = VKEY_RETURN,
                [Key.BackSpace] = VKEY_BACK,
                [Key.Tab] = VKEY_TAB,
                [Key.PageUp] = VKEY_PRIOR,
                [Key.PageDown] = VKEY_NEXT,
                [Key.End] = VKEY_END,
                [Key.Home] = VKEY_HOME,
                [Key.Insert] = VKEY_INSERT,
                [Key.Delete] = VKEY_DELETE,
                [Key.Minus] = VKEY_OEM_MINUS,
                [Key.NumpadAdd] = VKEY_ADD,
                [Key.NumpadSubtract] = VKEY_SUBTRACT,
                [Key.NumpadDivide] = VKEY_DIVIDE,
                [Key.NumpadMultiply] = VKEY_MULTIPLY,
                [Key.NumpadDecimal] = VKEY_DECIMAL,
                [Key.Left] = VKEY_LEFT,
                [Key.Right] = VKEY_RIGHT,
                [Key.Up] = VKEY_UP,
                [Key.Down] = VKEY_DOWN,
                [Key.F1] = VKEY_F1,
                [Key.F2] = VKEY_F2,
                [Key.F3] = VKEY_F3,
                [Key.F4] = VKEY_F4,
                [Key.F5] = VKEY_F5,
                [Key.F6] = VKEY_F6,
                [Key.F7] = VKEY_F7,
                [Key.F8] = VKEY_F8,
                [Key.F9] = VKEY_F9,
                [Key.F10] = VKEY_F10,
                [Key.F11] = VKEY_F11,
                [Key.F12] = VKEY_F12,
                [Key.F13] = VKEY_F13,
                [Key.F14] = VKEY_F14,
                [Key.F15] = VKEY_F15,
                [Key.Pause] = VKEY_PAUSE,
            };

            [Dependency] private IClyde _clyde = default!;
            [Dependency] private IInputManager _inputMgr = default!;

            private readonly WebViewManagerCef _manager;
            public readonly WebViewControl Owner;
            private readonly ShaderInstance _shaderInstance;

            public ControlImpl(WebViewManagerCef manager, WebViewControl owner, ShaderInstance shaderInstance)
            {
                _manager = manager;
                Owner = owner;
                _shaderInstance = shaderInstance;
            }

            private const int ScrollSpeed = 50;

            private bool _textInputActive;

            private readonly RobustRequestHandler _requestHandler = new(Logger.GetSawmill("root"));
            private LiveData? _data;
            private string _startUrl = "about:blank";

            public string Url
            {
                get => _data == null ? _startUrl : _data.Browser.GetMainFrame().Url;
                set
                {
                    if (_data == null)
                        _startUrl = value;
                    else
                        _data.Browser.GetMainFrame().LoadUrl(value);
                }
            }

            public bool IsOpen => _data != null;
            public bool IsLoading => _data?.Browser.IsLoading ?? false;

            public void StartBrowser()
            {
                DebugTools.AssertNull(_data);

                // A funny render handler that will allow us to render to the control.
                var renderer = new ControlRenderHandler(this);

                // A funny web cef client. This can actually be shared by multiple browsers, but I'm not sure how the
                // rendering would work in that case? TODO CEF: Investigate a way to share the web client?
                var client = new RobustCefClient(renderer, _requestHandler, new RobustLoadHandler());

                var info = CefWindowInfo.Create();

                // FUNFACT: If you DO NOT set these below and set info.Width/info.Height instead, you get an external window
                // Good to know, huh? Setup is the same, except you can pass a dummy render handler to the CEF client.
                info.SetAsWindowless(IntPtr.Zero, false); // TODO CEF: Pass parent handle?
                info.WindowlessRenderingEnabled = true;

                var settings = new CefBrowserSettings()
                {
                    WindowlessFrameRate = 60
                };

                // Create the web browser! And by default, we go to about:blank.
                var browser = CefBrowserHost.CreateBrowserSync(info, client, settings, _startUrl);

                var texture = _clyde.CreateBlankTexture<Rgba32>(Vector2i.One);

                _data = new LiveData(texture, client, browser, renderer);
                _manager._activeControls.Add(this);
            }

            public void CloseBrowser()
            {
                DebugTools.AssertNotNull(_data);

                _data!.Texture.Dispose();
                _data.Browser.GetHost().CloseBrowser(true);
                _data = null;

                _manager._activeControls.Remove(this);
            }

            public void MouseMove(GUIMouseMoveEventArgs args)
            {
                if (_data == null)
                    return;

                // Logger.Debug();
                var modifiers = CalcMouseModifiers();
                var mouseEvent = new CefMouseEvent(
                    (int)args.RelativePosition.X, (int)args.RelativePosition.Y,
                    modifiers);

                _data.Browser.GetHost().SendMouseMoveEvent(mouseEvent, false);
            }

            public void MouseExited()
            {
                if (_data == null)
                    return;

                var modifiers = CalcMouseModifiers();

                _data.Browser.GetHost().SendMouseMoveEvent(new CefMouseEvent(0, 0, modifiers), true);
            }

            public void MouseWheel(GUIMouseWheelEventArgs args)
            {
                if (_data == null)
                    return;

                var modifiers = CalcMouseModifiers();
                var mouseEvent = new CefMouseEvent(
                    (int)args.RelativePosition.X, (int)args.RelativePosition.Y,
                    modifiers);

                _data.Browser.GetHost().SendMouseWheelEvent(
                    mouseEvent,
                    (int)args.Delta.X * ScrollSpeed,
                    (int)args.Delta.Y * ScrollSpeed);
            }

            public bool RawKeyEvent(in GuiRawKeyEvent guiRawEvent)
            {
                if (_data == null)
                    return false;

                var host = _data.Browser.GetHost();

                if (guiRawEvent.Key is Key.MouseLeft or Key.MouseMiddle or Key.MouseRight)
                {
                    var key = guiRawEvent.Key switch
                    {
                        Key.MouseLeft => CefMouseButtonType.Left,
                        Key.MouseMiddle => CefMouseButtonType.Middle,
                        Key.MouseRight => CefMouseButtonType.Right,
                        _ => default // not possible
                    };

                    var mouseEvent = new CefMouseEvent(
                        guiRawEvent.MouseRelative.X, guiRawEvent.MouseRelative.Y,
                        CefEventFlags.None);

                    // Logger.Debug($"MOUSE: {guiRawEvent.Action} {guiRawEvent.Key} {guiRawEvent.ScanCode} {key}");

                    // TODO: double click support?
                    host.SendMouseClickEvent(mouseEvent, key, guiRawEvent.Action == RawKeyAction.Up, 1);
                }
                else
                {
                    // TODO: Handle left/right modifier keys??
                    if (!KeyMap.TryGetValue(guiRawEvent.Key, out var vkKey))
                        vkKey = default;

                    // Logger.Debug($"{guiRawEvent.Action} {guiRawEvent.Key} {guiRawEvent.ScanCode} {vkKey}");

#if !MACOS
                    var lParam = 0;
                    lParam |= (guiRawEvent.ScanCode & 0xFF) << 16;
                    if (guiRawEvent.Action != RawKeyAction.Down)
                        lParam |= 1 << 30;

                    if (guiRawEvent.Action == RawKeyAction.Up)
                        lParam |= 1 << 31;
#else
                    var lParam = guiRawEvent.RawCode;
#endif
                    var modifiers = CalcModifiers(guiRawEvent.Key);

                    host.SendKeyEvent(new CefKeyEvent
                    {
                        // Repeats are sent as key downs, I guess?
                        EventType = guiRawEvent.Action == RawKeyAction.Up
                            ? CefKeyEventType.KeyUp
                            : CefKeyEventType.RawKeyDown,
                        NativeKeyCode = lParam,
                        // NativeKeyCode = guiRawEvent.ScanCode,
                        WindowsKeyCode = (int)vkKey,
                        IsSystemKey = false, // TODO
                        Modifiers = modifiers
                    });

                    if (guiRawEvent.Action != RawKeyAction.Up && guiRawEvent.Key == Key.Return)
                    {
                        host.SendKeyEvent(new CefKeyEvent
                        {
                            EventType = CefKeyEventType.Char,
                            WindowsKeyCode = '\b',
                            NativeKeyCode = lParam,
                            Modifiers = modifiers
                        });
                    }
                }

                return true;
            }

            private CefEventFlags CalcModifiers(Key key)
            {
                CefEventFlags modifiers = default;

                if (_inputMgr.IsKeyDown(Key.Control))
                    modifiers |= CefEventFlags.ControlDown;

                if (_inputMgr.IsKeyDown(Key.Alt))
                    modifiers |= CefEventFlags.AltDown;

                if (_inputMgr.IsKeyDown(Key.Shift))
                    modifiers |= CefEventFlags.ShiftDown;

                if (_inputMgr.IsKeyDown(Key.Shift))
                    modifiers |= CefEventFlags.ShiftDown;

                return modifiers;
            }

            private CefEventFlags CalcMouseModifiers()
            {
                CefEventFlags modifiers = default;

                if (_inputMgr.IsKeyDown(Key.Control))
                    modifiers |= CefEventFlags.ControlDown;

                if (_inputMgr.IsKeyDown(Key.Alt))
                    modifiers |= CefEventFlags.AltDown;

                if (_inputMgr.IsKeyDown(Key.Shift))
                    modifiers |= CefEventFlags.ShiftDown;

                if (_inputMgr.IsKeyDown(Key.Shift))
                    modifiers |= CefEventFlags.ShiftDown;

                if (_inputMgr.IsKeyDown(Key.MouseLeft))
                    modifiers |= CefEventFlags.LeftMouseButton;

                if (_inputMgr.IsKeyDown(Key.MouseMiddle))
                    modifiers |= CefEventFlags.MiddleMouseButton;

                if (_inputMgr.IsKeyDown(Key.MouseRight))
                    modifiers |= CefEventFlags.RightMouseButton;

                return modifiers;
            }

            public void TextEntered(GUITextEnteredEventArgs args)
            {
                if (_data == null)
                    return;

                var host = _data.Browser.GetHost();

                foreach (var chr in args.Text)
                {
                    host.SendKeyEvent(new CefKeyEvent
                    {
                        EventType = CefKeyEventType.Char,
                        WindowsKeyCode = chr,
                        Character = chr,
                        UnmodifiedCharacter = chr
                    });
                }
            }

            public void Resized()
            {
                if (_data == null)
                    return;

                _data.Browser.GetHost().NotifyMoveOrResizeStarted();
                _data.Browser.GetHost().WasResized();
                _data.Texture.Dispose();
                _data.Texture = _clyde.CreateBlankTexture<Rgba32>((Owner.PixelWidth, Owner.PixelHeight));
                _data.Browser.GetHost().Invalidate(CefPaintElementType.View);
            }

            public void Draw(DrawingHandleScreen handle)
            {
                if (_data == null)
                    return;

                // update texture only when CEF has rendered new content
                if (_data.Renderer.IsDirty)
                {
                    _data.Renderer.IsDirty = false;

                    var bufImg = _data.Renderer.Buffer.Buffer;

                    _data.Texture.SetSubImage(
                        Vector2i.Zero,
                        bufImg,
                        new UIBox2i(
                            0, 0,
                            Math.Min(Owner.PixelWidth, bufImg.Width),
                            Math.Min(Owner.PixelHeight, bufImg.Height)));
                }

                handle.UseShader(_shaderInstance);
                handle.DrawTexture(_data.Texture, Vector2.Zero);
            }

            public void StopLoad()
            {
                if (_data == null)
                    throw new InvalidOperationException();

                _data.Browser.StopLoad();
            }

            public void Reload()
            {
                if (_data == null)
                    throw new InvalidOperationException();

                _data.Browser.Reload();
            }

            public bool GoBack()
            {
                if (_data == null)
                    throw new InvalidOperationException();

                if (!_data.Browser.CanGoBack)
                    return false;

                _data.Browser.GoBack();
                return true;
            }

            public bool GoForward()
            {
                if (_data == null)
                    throw new InvalidOperationException();

                if (!_data.Browser.CanGoForward)
                    return false;

                _data.Browser.GoForward();
                return true;
            }

            public void ExecuteJavaScript(string code)
            {
                if (_data == null)
                    throw new InvalidOperationException();

                // TODO: this should not run until the browser is done loading seriously does this even work?
                _data.Browser.GetMainFrame().ExecuteJavaScript(code, string.Empty, 1);
            }

            public void AddResourceRequestHandler(Action<IRequestHandlerContext> handler)
            {
                _requestHandler.AddResourceRequestHandler(handler);
            }

            public void RemoveResourceRequestHandler(Action<IRequestHandlerContext> handler)
            {
                _requestHandler.RemoveResourceRequestHandler(handler);
            }

            public void AddBeforeBrowseHandler(Action<IBeforeBrowseContext> handler)
            {
                _requestHandler.AddBeforeBrowseHandler(handler);
            }

            public void RemoveBeforeBrowseHandler(Action<IBeforeBrowseContext> handler)
            {
                _requestHandler.RemoveBeforeBrowseHandler(handler);
            }

            public void FocusEntered()
            {
                if (_textInputActive)
                    Owner.Root?.Window?.TextInputStart();
            }

            public void FocusExited()
            {
                if (_textInputActive)
                    Owner.Root?.Window?.TextInputStop();
            }

            public void TextInputStart()
            {
                _textInputActive = true;
                if (Owner.HasKeyboardFocus())
                    Owner.Root?.Window?.TextInputStart();
            }

            public void TextInputStop()
            {
                _textInputActive = false;
                if (Owner.HasKeyboardFocus())
                    Owner.Root?.Window?.TextInputStop();
            }

            private sealed class LiveData
            {
                public OwnedTexture Texture;
                public readonly RobustCefClient Client;
                public readonly CefBrowser Browser;
                public readonly ControlRenderHandler Renderer;

                public LiveData(
                    OwnedTexture texture,
                    RobustCefClient client,
                    CefBrowser browser,
                    ControlRenderHandler renderer)
                {
                    Texture = texture;
                    Client = client;
                    Browser = browser;
                    Renderer = renderer;
                }
            }
        }

        private sealed class ControlRenderHandler : CefRenderHandler
        {
            public ImageBuffer Buffer { get; }
            private ControlImpl _control;
            internal volatile bool IsDirty;

            internal ControlRenderHandler(ControlImpl control)
            {
                Buffer = new ImageBuffer();
                _control = control;
            }

            protected override CefAccessibilityHandler? GetAccessibilityHandler() => null;

            protected override void GetViewRect(CefBrowser browser, out CefRectangle rect)
            {
                if (_control.Owner.Disposed)
                {
                    rect = new CefRectangle();
                    return;
                }

                // TODO CEF: Do we need to pass real screen coords? Cause what we do already works...
                //var screenCoords = _control.ScreenCoordinates;
                //rect = new CefRectangle((int) screenCoords.X, (int) screenCoords.Y, (int)Math.Max(_control.Size.X, 1), (int)Math.Max(_control.Size.Y, 1));

                // We do the max between size and 1 because it will LITERALLY CRASH WITHOUT AN ERROR otherwise.
                rect = new CefRectangle(
                    0, 0,
                    (int)Math.Max(_control.Owner.Size.X, 1), (int)Math.Max(_control.Owner.Size.Y, 1));
            }

            protected override bool GetScreenInfo(CefBrowser browser, CefScreenInfo screenInfo)
            {
                if (_control.Owner.Disposed)
                    return false;

                screenInfo.DeviceScaleFactor = _control.Owner.UIScale;

                return true;
            }

            protected override void OnPopupSize(CefBrowser browser, CefRectangle rect)
            {
                if (_control.Owner.Disposed)
                    return;
            }

            protected override void OnPaint(CefBrowser browser, CefPaintElementType type, CefRectangle[] dirtyRects,
                IntPtr buffer, int width, int height)
            {
                if (_control.Owner.Disposed)
                    return;

                foreach (var dirtyRect in dirtyRects)
                {
                    Buffer.UpdateBuffer(width, height, buffer, dirtyRect);
                }

                IsDirty = true;
            }

            protected override void OnAcceleratedPaint(
                CefBrowser browser,
                CefPaintElementType type,
                CefRectangle[] dirtyRects,
                in CefAcceleratedPaintInfo info)
            {
                // Unused, but we're forced to implement it so.. NOOP.
            }

            protected override void OnScrollOffsetChanged(CefBrowser browser, double x, double y)
            {
                if (_control.Owner.Disposed)
                    return;
            }

            protected override void OnImeCompositionRangeChanged(CefBrowser browser, CefRange selectedRange,
                CefRectangle[] characterBounds)
            {
                if (_control.Owner.Disposed)
                    return;
            }

            protected override void OnVirtualKeyboardRequested(CefBrowser browser, CefTextInputMode inputMode)
            {
                base.OnVirtualKeyboardRequested(browser, inputMode);

                // Treat virtual keyboard requests as a guide for whether we should accept text input.

                if (inputMode == CefTextInputMode.None)
                {
                    _control.TextInputStop();
                }
                else
                {
                    _control.TextInputStart();
                }
            }
        }
    }
}
