using System;
using System.Runtime.InteropServices;
using System.Threading;
using SDL2;
using Serilog;
using Serilog.Core;
using SlatedGameToolkit.Framework.Exceptions;
using SlatedGameToolkit.Framework.Graphics.Textures;
using SlatedGameToolkit.Framework.Graphics.Window;
using SlatedGameToolkit.Framework.Input.Devices;
using SlatedGameToolkit.Framework.StateSystem;
using SlatedGameToolkit.Framework.StateSystem.States;
using SlatedGameToolkit.Framework.Utilities;
namespace SlatedGameToolkit.Framework {
    /// 
    /// The main engine that will host the game loop.
    /// 
    public static class GameEngine {
        private const int GL_MAJOR_VER = 3, GL_MINOR_VER = 3;
        public static bool Debugging { get; set; }
        public static Logger Logger { get; private 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);
        /// 
        /// 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.
        /// 
        /// The updates per second.
        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;
            }
        }
        /// 
        /// 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.
        /// 
        /// The target frames per second.
        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();
            try {
                IState initialState = (IState) o;
                manager.Initialize(initialState);
                DateTime currentTime = DateTime.Now;
                TimeSpan timePassedFromLastUpdate = TimeSpan.Zero;
                TimeSpan timePassedFromLastRender = TimeSpan.Zero;
                TimeSpan updateDeltaTime = GameEngine.updateDeltaTime;
                TimeSpan frameDeltaTime = GameEngine.frameDeltaTime;
                deltaChanged = true;
                stopped = false;
                Logger.Information("Game engine initiated.");
                while (!exit) {
                    //Pull latest deltas.
                    if (deltaChanged) {
                        lock (deltaUpdateLock) {
                            updateDeltaTime = GameEngine.updateDeltaTime;
                            frameDeltaTime = GameEngine.frameDeltaTime;
                            Logger.Information(String.Format("Deltas were set. Update Delta: {0}, Render target Delta: {1}", updateDeltaTime.TotalSeconds, frameDeltaTime.TotalSeconds));
                        }
                        deltaChanged = false;
                    }
                    //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;
                        }
                    }
                    DateTime frameStart = DateTime.Now;
                    TimeSpan difference = frameStart - currentTime;
                    currentTime = frameStart;
                    timePassedFromLastUpdate += difference;
                    while (timePassedFromLastUpdate > updateDeltaTime) {
                        //Updates.
                        manager.update(updateDeltaTime.TotalSeconds <= 0 ? timePassedFromLastUpdate.TotalSeconds : updateDeltaTime.TotalSeconds);
                        timePassedFromLastUpdate -= updateDeltaTime;
                        if (updateDeltaTime.TotalSeconds <= 0) break;
                    }
                    timePassedFromLastRender += difference;
                    if (timePassedFromLastRender > frameDeltaTime) {
                        //Draw calls.
                        manager.render(updateDeltaTime.TotalSeconds <= 0 ? updateDeltaTime.TotalSeconds : (timePassedFromLastUpdate / updateDeltaTime));
                        timePassedFromLastRender = TimeSpan.Zero;
                    }
                }
            } catch (Exception e) {
              Logger.Fatal(e.ToString());
            } finally {
                stopped = true;
                manager.Dispose();
                SDL.SDL_Quit();
                Logger.Information("Game engine has stopped.");
                Logger.Dispose();
                WindowContextsManager.DisposeAllWindowContexts();
                Logger = null;
            }
        }
        /// 
        /// 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.
        /// 
        /// False if the stop request has already been made.
        public static bool Stop() {
            lock (ignitionLock) {
                if (exit) return false;
                exit = true;
                return true;
            }
        }
        /// 
        /// Requests to start the game engine. 
        /// 
        /// True iff the engine is not already running.
        public static bool Ignite(IState initialState) {
            lock (ignitionLock) {
                if (initialState == null) throw new ArgumentNullException("initialState");
                if (!stopped) {
                    Logger.Warning("Engine is already running.");
                    return false;
                }
                GameEngine.Logger = new LoggerConfiguration().MinimumLevel.Debug().
                WriteTo.File("SlatedGameToolKit.log", fileSizeLimitBytes: 1048576, rollOnFileSizeLimit: true).
                CreateLogger();
                SDL.SDL_version SDLVersion;
                SDL.SDL_version SDLBuiltVersion;
                SDL.SDL_GetVersion(out SDLVersion);
                SDL.SDL_VERSION(out SDLBuiltVersion);
                Logger.Information(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.Warning(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));
                    if (SDLVersion.major < 2) {
                        Logger.Error("This engine was designed to work with SDL2. The version you're currently running is severely outdated.");
                        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;
        }
    }
}