using System;
using System.Drawing;
using System.Numerics;
using System.Runtime.InteropServices;
using SDL2;
using SlatedGameToolkit.Framework.Exceptions;
using SlatedGameToolkit.Framework.Graphics;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Logging;

namespace SlatedGameToolkit.Framework.Graphics.Window
{
    /// <summary>
    /// A delegate to determine the type of interaction with a window.
    /// The method should be extremely lightweight as it can be called many times.
    /// </summary>
    /// <param name="hitPoint">The window coordinates that are being interacted with.</param>
    /// <returns>The region type.</returns>
    public delegate SDL.SDL_HitTestResult WindowRegionHit(Vector2 hitPoint);
    
    /// <summary>
    /// A delegate that represents a function to be called on a window resize.
    /// </summary>
    /// <param name="width">The new width.</param>
    /// <param name="height">The new height.</param>
    public delegate void WindowResize(int width, int height);

    /// <summary>
    /// A delegate that represents a function to be called when a window event occurs.
    /// </summary>
    public delegate void WindowOperation();

    /// <summary>
    /// A handle for a window.
    /// This object itself is not thread safe.
    /// </summary>
    public sealed class WindowContext : IDisposable
    {
        public GLContext Context { get; private set; }
        /// <summary>
        /// The pointer referencing the SDL window.
        /// </summary>
        private readonly IntPtr windowHandle;

        /// <summary>
        /// The pointer referencing the OpenGL context.
        /// </summary>
        public event WindowOperation focusGainedEvent, focusLostEvent;

        /// <summary>
        /// Invoked when this window resizes.
        /// </summary>
        public event WindowResize resizeEvent;

        /// <summary>
        /// Event for when the window is being interacted with.
        /// </summary>
        public event WindowRegionHit windowRegionHitEvent;
                
        /// <summary>
        /// Whether or not to show this window.
        /// </summary>
        /// <value>True for showing.</value>
        public bool Shown {
            set {
                if (value) {
                    SDL.SDL_ShowWindow(windowHandle);
                } else {
                    SDL.SDL_HideWindow(windowHandle);
                }
            }

            get {
                return ((SDL.SDL_WindowFlags) Enum.Parse(typeof(SDL.SDL_WindowFlags), SDL.SDL_GetWindowFlags(windowHandle).ToString())).HasFlag(SDL.SDL_WindowFlags.SDL_WINDOW_SHOWN);
            }
        }

        /// <summary>
        /// Whether or not to attempt to perform vertical synchronization.
        /// Will use adaptive sync if supported.
        /// If this window is not the current active window, retrieving and setting requires a context switch.
        /// If a context switch was nessecary, will automatically switch back to previous once operation is complete.
        /// If neither normal VSync or adaptive sync are available, enabling will throw an OptionalSDLException.
        /// </summary>
        /// <value></value>
        public bool VSync {
            get {
                WindowContext actual = WindowContextsManager.CurrentWindowContext;
                bool diff = false;
                if (actual != this) {
                    MakeCurrent();
                    diff = true;
                }
                bool vSync = SDL.SDL_GL_GetSwapInterval() != 0;
                if (diff) actual.MakeCurrent();
                return vSync;
            }
            set {
                WindowContext actual = WindowContextsManager.CurrentWindowContext;
                bool diff = false;
                if (actual != this) {
                    MakeCurrent();
                    diff = true;
                }
                if (SDL.SDL_GL_SetSwapInterval(value ? -1 : 0) < 0) {
                    if (SDL.SDL_GL_SetSwapInterval(value ? 1 : 0) < 0) {
                        throw new OptionalSDLException();
                    }
                }
                if (diff) actual.MakeCurrent();
            }
        }

        /// <summary>
        /// The title displayed on the window.
        /// </summary>
        /// <value>A string that represents whats to be displayed as the title of the window.</value>
        public string WindowTitle {
            set {
                SDL.SDL_SetWindowTitle(windowHandle, value);
            }

            get {
                return SDL.SDL_GetWindowTitle(windowHandle);
            }
        }

        /// <summary>
        /// Whether or not to display the default window border.
        /// </summary>
        /// <value>True if displaying borders.</value>
        public bool WindowBordered {
            set {
                SDL.SDL_SetWindowBordered(windowHandle, value ? SDL.SDL_bool.SDL_TRUE : SDL.SDL_bool.SDL_FALSE);
            }

            get {
                int top, bottom, left, right;
                int errorCode = SDL.SDL_GetWindowBordersSize(windowHandle, out top, out left, out bottom, out right);
                if (errorCode < 0) throw new OptionalSDLException();
                return top > 0 || bottom > 0 || left > 0 || right > 0;
            }
        }

        /// <summary>
        /// Whether or not this window should be resizeable.
        /// </summary>
        /// <value>True if resizeable.</value>
        public bool WindowResizable {
            set {
                SDL.SDL_SetWindowResizable(windowHandle, value ? SDL.SDL_bool.SDL_TRUE : SDL.SDL_bool.SDL_FALSE);
            }
            get {
                return ((SDL.SDL_WindowFlags) Enum.Parse(typeof(SDL.SDL_WindowFlags), SDL.SDL_GetWindowFlags(windowHandle).ToString())).HasFlag(SDL.SDL_WindowFlags.SDL_WINDOW_RESIZABLE);
            }
        }

        /// <summary>
        /// The boundaries of the window represent both the bottom left corner relative to the screen,
        /// as well as the current width and height of this window.
        /// </summary>
        /// <value>Rectangle representing the boundaries of the window.</value>
        public Rectangle WindowBoundaries {
            set {
                SDL.SDL_SetWindowPosition(windowHandle, value.X, value.Y);
                SDL.SDL_SetWindowSize(windowHandle, value.Width, value.Height);
            }
            get {
                int x, y, width, height;
                SDL.SDL_GetWindowSize(windowHandle, out x, out y);
                SDL.SDL_GetWindowPosition(windowHandle, out width, out height);
                Rectangle rectangle = new Rectangle();
                rectangle.X = x;
                rectangle.Y = y;
                rectangle.Width = width;
                rectangle.Height = height;
                return rectangle;
            }
        }

        /// <summary>
        /// The maximum size the window can be at any given time.
        /// </summary>
        /// <value>A vector that represents the maximum width and height of the window with the x and y components respectively.</value>
        public Vector2 MaximumSize {
            set {
                SDL.SDL_SetWindowMaximumSize(windowHandle, (int)value.X, (int)value.Y);
            }
            get {
                int width, height;
                SDL.SDL_GetWindowMaximumSize(windowHandle, out width, out height);
                Vector2 maxSize = new Vector2();
                maxSize.X = width;
                maxSize.Y = height;
                return maxSize;
            }
        }

        /// <summary>
        /// The minimum size the window can be at any given time.
        /// </summary>
        /// <value>A vector that represents the minimum width and height of the window with the x and y components respectively.</value>
        public Vector2 MinimumSize {
            set {
                SDL.SDL_SetWindowMinimumSize(windowHandle, (int)value.X, (int)value.Y);
            }
            get {
                int width, height;
                SDL.SDL_GetWindowMinimumSize(windowHandle, out width, out height);
                Vector2 maxSize = new Vector2();
                maxSize.X = width;
                maxSize.Y = height;
                return maxSize;
            }
        }
        
        /// <summary>
        /// The opacity of the window itself.
        /// This is not supported on all targetted platforms, and therefore, can throw an OptionalSDLException for when this is the case.
        /// </summary>
        /// <value>A float with bounds of [0, 1] representing the opacity of the window.</value>
        public float Opacity {
            set {
                int errorCode = SDL.SDL_SetWindowOpacity(windowHandle, value);
                if (errorCode < 0) throw new OptionalSDLException();
            }
            get {
                float value;
                int errorCode = SDL.SDL_GetWindowOpacity(windowHandle, out value);
                if (errorCode < 0) throw new OptionalSDLException();
                return value;
            }
        }

        /// <summary>
        /// Whether or not this window is grabbing the users input by making sure the mouse stays within this windows boundaries.
        /// </summary>
        /// <value>True if grabbing.</value>
        public bool GrabbingInput {
            set {
                SDL.SDL_SetWindowGrab(windowHandle, value ? SDL.SDL_bool.SDL_TRUE : SDL.SDL_bool.SDL_FALSE);
            }
            get {
                return SDL.SDL_GetWindowGrab(windowHandle) == SDL.SDL_bool.SDL_TRUE ? true : false;
            }
        }

        internal uint WindowID {
            get {
                uint id = SDL.SDL_GetWindowID(windowHandle);
                if (id == 0) throw new FrameworkSDLException();
                return id;
            }
        }

        /// <summary>
        /// Instantiates a window with the given OpenGL context, or a new context.
        /// </summary>
        public WindowContext(string title, int x = -1, int y = -1, int width = 640, int height = 480, bool specialRegions = false, SDL.SDL_WindowFlags options = default(SDL.SDL_WindowFlags)) {
            Logger.Log(String.Format("Starting openGL window with title \"{0}\"", title));
            windowHandle = SDL.SDL_CreateWindow(title, x < 0 ? SDL.SDL_WINDOWPOS_CENTERED : x, y < 0 ? SDL.SDL_WINDOWPOS_CENTERED : y, width, height, SDL.SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS | SDL.SDL_WindowFlags.SDL_WINDOW_OPENGL | SDL.SDL_WindowFlags.SDL_WINDOW_MOUSE_FOCUS | options);
            if (windowHandle == null) {
                throw new FrameworkSDLException();
            }
            this.Context = new GLContext(windowHandle);

            if (specialRegions) {
                if (SDL.SDL_SetWindowHitTest(windowHandle, SpecialRegionHit, IntPtr.Zero) < 0) {
                    throw new OptionalSDLException();
                }
            }

            WindowContextsManager.RegisterWindow(this);
            MakeCurrent();

            Vector2 drawable = GetDrawableDimensions();
            this.Context.Viewport(0, 0, (int)drawable.X, (int)drawable.Y);
            this.Context.Enable(EnableCap.Blend);
            Context.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
        }

        private SDL.SDL_HitTestResult SpecialRegionHit(IntPtr window, IntPtr hitPtr, IntPtr data) {
            SDL.SDL_Point SDLPoint = Marshal.PtrToStructure<SDL.SDL_Point>(hitPtr);
            Vector2 point = new Vector2(SDLPoint.x, SDLPoint.y);
            SDL.SDL_HitTestResult region = windowRegionHitEvent.Invoke(point);
            return region;
        }

        internal void SwapBuffer() {
            SDL.SDL_GL_SwapWindow(windowHandle);
        }


        internal void OnResize(int width, int height) {
            resizeEvent?.Invoke(width, height);
        }

        internal void OnFocusLost() {
            focusLostEvent?.Invoke();
        }

        internal void OnFocusGained() {
            focusGainedEvent?.Invoke();
        }
        
        /// <summary>
        /// Gets the current drawable area of the window.
        /// </summary>
        /// <returns>A vector with x component representing the width and y component representing the height of the drawable area.</returns>
        public Vector2 GetDrawableDimensions() {
            int width, height;
            SDL.SDL_GL_GetDrawableSize(windowHandle, out width, out height);
            return new Vector2(width, height);
        }

        /// <summary>
        /// Makes this window context the active one.
        /// There can only be one window context active at any given time.
        /// </summary>
        public void MakeCurrent() {
            if (WindowContextsManager.CurrentWindowContext != this) {
                SDL.SDL_GL_MakeCurrent(windowHandle, Context.Handle);
                WindowContextsManager.current = this;
            }
        }

        /// <summary>
        /// Attempts to raise the window to the top.
        /// </summary>
        public void RaiseToTop() {
            SDL.SDL_RestoreWindow(windowHandle);
            SDL.SDL_RaiseWindow(windowHandle);
        }

        /// <summary>
        /// Gets the index of the display that this window resides within.
        /// </summary>
        /// <returns>An integer representing the display this window resides within.</returns>
        public int GetDisplayIndex() {
            int index = SDL.SDL_GetWindowDisplayIndex(windowHandle);
            if (index < 0) throw new FrameworkSDLException();
            return index;
        }

        /// <summary>
        /// Disposes of this window and it's associated handles.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public void Dispose(bool disposing) {
            if (disposing) 
            {
            }
            WindowContextsManager.DeregisterWindow(this);
            SDL.SDL_DestroyWindow(windowHandle);
            Context.Dispose();
        }

        ~WindowContext() {
            Dispose(false);
        }
    }
}