using System;
using System.Runtime.InteropServices;
using System.Threading;
using SDL2;
using SlatedGameToolkit.Framework.Exceptions;
using SlatedGameToolkit.Framework.Graphics.Window;
using SlatedGameToolkit.Framework.Input.Devices;
using SlatedGameToolkit.Framework.Logging;
using SlatedGameToolkit.Framework.StateSystem;
using SlatedGameToolkit.Framework.StateSystem.States;

namespace SlatedGameToolkit.Framework {
    /// <summary>
    /// The main engine that will host the game loop.
    /// </summary>
    public static class GameEngine {
        private const int GL_MAJOR_VER = 3, GL_MINOR_VER = 3;
        public static bool Debugging { get; set; }
        private static readonly object ignitionLock = new object();
        private static readonly object deltaUpdateLock = new object();
        private static Thread thread;
        private static volatile bool exit = false, stopped = true;
        private static volatile bool deltaChanged;
        private static TimeSpan updateDeltaTime = TimeSpan.FromSeconds(1d/40d), frameDeltaTime = TimeSpan.FromSeconds(1d/60d);

        /// <summary>
        /// The amount of updates per second.
        /// 
        /// If greater than 0, game loop updates will be fixed (time steps).
        /// More specifically, the amount of updates performed per frame to allow for physics calculations
        /// to be calculated based on a constant time step will be calculated automatically.
        /// 
        /// If set to 0 or less, updates will be as fast as possible.
        /// </summary>
        /// <value>The updates per second.</value>
        public static double UpdatesPerSecond {
            get {
                if (updateDeltaTime.TotalSeconds <= 0) return 0;
                return  1d / updateDeltaTime.TotalSeconds;
            }
            set {
                lock (deltaUpdateLock) {
                    if (value <= 0) {
                        updateDeltaTime = TimeSpan.FromSeconds(0);
                    } else {
                        updateDeltaTime = TimeSpan.FromSeconds(1d/value);
                    }
                }
                deltaChanged = true;
            }
        }

        /// <summary>
        /// The target frames per second. Will not go above this number, but may dip below this number.
        /// Setting this to 0 renders as many frames as the system can handle.
        /// Default is 60.
        /// </summary>
        /// <value>The target frames per second.</value>
        public static double targetFPS {
            get {
                if (frameDeltaTime.TotalSeconds <= 0) return 0;
                return 1 / frameDeltaTime.TotalSeconds;
            }
            set {
                lock (deltaUpdateLock) {
                    if (value <= 0) {
                        frameDeltaTime = TimeSpan.FromSeconds(0);
                    } else {
                        frameDeltaTime = TimeSpan.FromSeconds(1d/value);
                    }
                }
                deltaChanged = true;
            }
        }

        private static void Loop(Object o) {
            if (!(o is IState)) throw new InternalFrameworkException(String.Format("Expected initial state object for asynchronous loop. Got {0}", o));
            StateManager manager = new StateManager();
            IState initialState = (IState) o;
            manager.Initialize(initialState);
            DateTime previousTime = DateTime.Now;
            TimeSpan timePassedFromLastUpdate = TimeSpan.Zero;
            TimeSpan timePassedFromLastRender = TimeSpan.Zero;
            TimeSpan updateDeltaTime = GameEngine.updateDeltaTime;
            TimeSpan frameDeltaTime = GameEngine.frameDeltaTime;
            deltaChanged = true;
            stopped = false;
            Logger.Log("Game engine initiated.");
            while (!exit) {
                //Pull latest deltas.
                if (deltaChanged) {
                    lock (deltaUpdateLock) {
                        updateDeltaTime = GameEngine.updateDeltaTime;
                        frameDeltaTime = GameEngine.frameDeltaTime;
                        Logger.Log(String.Format("Deltas were set. Update Delta: {0}, Render target Delta: {1}", updateDeltaTime.TotalSeconds, frameDeltaTime.TotalSeconds));
                    }
                    deltaChanged = false;
                }
                #region EventHandling
                //Events
                SDL.SDL_Event SDL_Event;
                while (SDL.SDL_PollEvent(out SDL_Event) != 0) {
                    switch (SDL_Event.type) {
                        case SDL.SDL_EventType.SDL_MOUSEMOTION:
                            Mouse.OnMouseMoved(SDL_Event.motion.x, SDL_Event.motion.y);
                        break;
                        case SDL.SDL_EventType.SDL_MOUSEWHEEL:
                            Mouse.OnScroll(SDL_Event.wheel.x, SDL_Event.wheel.y);
                        break;
                        case SDL.SDL_EventType.SDL_MOUSEBUTTONDOWN:
                            if (SDL.SDL_BUTTON_LEFT == SDL_Event.button.button) {
                                Mouse.OnLeftChange(true);
                            } else if (SDL.SDL_BUTTON_RIGHT == SDL_Event.button.button) {
                                Mouse.OnRightChange(true);
                            } else if (SDL.SDL_BUTTON_MIDDLE == SDL_Event.button.button) {
                                Mouse.OnMiddleChange(true);
                            }
                        break;
                        case SDL.SDL_EventType.SDL_MOUSEBUTTONUP:
                            if (SDL.SDL_BUTTON_LEFT == SDL_Event.button.button) {
                                Mouse.OnLeftChange(false);
                            } else if (SDL.SDL_BUTTON_RIGHT == SDL_Event.button.button) {
                                Mouse.OnRightChange(false);
                            } else if (SDL.SDL_BUTTON_MIDDLE == SDL_Event.button.button) {
                                Mouse.OnMiddleChange(false);
                            }
                        break;
                        case SDL.SDL_EventType.SDL_KEYDOWN:
                            Keyboard.OnKeyPressed(SDL_Event.key.keysym.sym);
                        break;
                        case SDL.SDL_EventType.SDL_KEYUP:
                            Keyboard.OnKeyReleased(SDL_Event.key.keysym.sym);
                        break;
                        case SDL.SDL_EventType.SDL_WINDOWEVENT:
                            WindowContext handle = WindowContextsManager.ContextFromWindowID(SDL_Event.window.windowID);
                            switch (SDL_Event.window.windowEvent)
                            {
                                case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED:
                                    handle.OnResize(SDL_Event.window.data1, SDL_Event.window.data2);
                                break;
                                case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_LOST:
                                    handle.OnFocusLost();
                                break;
                                case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_GAINED:
                                    handle.OnFocusGained();
                                break;
                            }
                            break;
                        case SDL.SDL_EventType.SDL_QUIT:
                            Stop();
                        break;
                    }
                }
                #endregion

                DateTime frameStart = DateTime.Now;
                TimeSpan difference = frameStart - previousTime;
                previousTime = frameStart;

                timePassedFromLastUpdate += difference;
                while (timePassedFromLastUpdate > updateDeltaTime) {
                    //Updates.
                    manager.update(updateDeltaTime.TotalSeconds <= 0 ? timePassedFromLastUpdate.TotalSeconds : updateDeltaTime.TotalSeconds);
                    timePassedFromLastUpdate -= updateDeltaTime;
                    if (updateDeltaTime.TotalSeconds <= 0) {
                        timePassedFromLastUpdate = TimeSpan.Zero;
                        break;
                    }
                }

                timePassedFromLastRender += difference;
                if (timePassedFromLastRender > frameDeltaTime) {
                    //Draw calls.
                    manager.render(updateDeltaTime.TotalSeconds <= 0 ? updateDeltaTime.TotalSeconds : (timePassedFromLastUpdate / updateDeltaTime));
                    timePassedFromLastRender = TimeSpan.Zero;
                } else {
                    Thread.Yield();
                }
            }
            stopped = true;
            manager.Dispose();
            SDL.SDL_Quit();
            WindowContextsManager.DisposeAllWindowContexts();
            Logger.Log("Game engine has gracefully stopped.");
            Logger.FlushListeners();
        }

        /// <summary>
        /// Requests to stop the game engine.
        /// Game engine will finish the frame its currently in.
        /// This needs to be called to perform proper clean up.
        /// </summary>
        /// <returns>False if the stop request has already been made.</returns>
        public static bool Stop() {
            lock (ignitionLock) {
                if (exit) return false;
                exit = true;
                return true;
            }
        }

        /// <summary>
        /// Requests to start the game engine. 
        /// </summary>
        /// <returns>True iff the engine is not already running.</returns>
        public static bool Ignite(IState initialState) {
            lock (ignitionLock) {
                if (initialState == null) throw new ArgumentNullException("initialState");
                if (!stopped) {
                    Logger.Log("Engine is already running.", LogLevel.WARNING);
                    return false;
                }
                SDL.SDL_version SDLVersion;
                SDL.SDL_version SDLBuiltVersion;
                SDL.SDL_GetVersion(out SDLVersion);
                SDL.SDL_VERSION(out SDLBuiltVersion);
                Logger.Log(String.Format("Attempting to initiate game engine with SDL version: {0}.{1}.{2}", SDLVersion.major, SDLVersion.minor, SDLVersion.patch));
                if (!SDL.SDL_VERSION_ATLEAST(SDLBuiltVersion.major, SDLBuiltVersion.minor, SDLBuiltVersion.patch)) {
                    Logger.Log(String.Format("Engine was designed with SDL version {0}.{1}.{2}, currently running on {3}.{4}.{5}", SDLBuiltVersion.major, SDLBuiltVersion.minor, SDLBuiltVersion.patch, SDLVersion.major, SDLVersion.minor, SDLVersion.patch), LogLevel.WARNING);
                    if (SDLVersion.major < 2) {
                        Logger.Log("This engine was designed to work with SDL2. The version you're currently running is severely outdated.", LogLevel.FATAL);
                        throw new FrameworkUsageException("Outdated SDL binaries.");
                    }
                }
                exit = false;
                if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO) != 0) {
                    throw new FrameworkSDLException();
                }
                if (SDL.SDL_Init(SDL.SDL_INIT_AUDIO) != 0) {
                    throw new FrameworkSDLException();
                }

                if (SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, GL_MAJOR_VER) < 0 || 
                SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, GL_MINOR_VER) < 0 || 
                SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, SDL.SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_CORE) < 0) 
                    throw new FrameworkSDLException(string.Format("Unable to load correct OpenGL version {0}.{1}.0 Core.", GL_MINOR_VER, GL_MAJOR_VER));
                
                thread = new Thread(Loop);
                thread.Name = "SGTK-Engine";
                thread.Priority = ThreadPriority.AboveNormal;
                thread.Start(initialState);
                return true;
            }
        }

        public static bool IsRunning() {
            return !stopped;
        }
    }
}