using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Voile.Audio;
using Voile.Input;
using Voile.Rendering;
using Voile.Resources;
using Voile.VFS;

namespace Voile
{
    public interface IGame
    {
        string Name { get; }
        /// <summary>
        /// Resource root path for this game.
        /// </summary>
        string ResourceRoot { get; }
        void Start();
        void Run();
        void Shutdown();
    }

    /// <summary>
    /// Entry point for the Voile game.
    /// </summary>
    public abstract class Game : IGame
    {
        public double UpdateTimeStep { get; set; } = 1.0 / 60.0;
        public TimeSpan UpdateFrameTime { get; private set; }
        public TimeSpan RenderFrameTime { get; private set; }

        /// <summary>
        /// The ResourceManager associated with this game.
        /// </summary>
        protected ResourceManager ResourceManager { get; private set; } = new();
        /// <summary>
        /// The InputHandler associated with this game. Uses <see cref="RaylibInputSystem"/> by default.
        /// </summary>
        protected InputSystem? Input { get; set; }

        /// <summary>
        /// The Renderer associated with this game. Uses <see cref="RaylibRenderSystem"/> by default.
        /// </summary>
        protected RenderSystem? Renderer { get; set; }

        protected AudioSystem? AudioSystem { get; set; }

        /// <summary>
        /// Name of this game. Also used as a default window title.
        /// </summary>
        public virtual string Name => "Voile Game";
        public virtual string ResourceRoot => "Resources/";

        /// <summary>
        /// Default game constructor.
        /// </summary>
        public Game()
        {
            ResourceManager = new ResourceManager();
            Initialize();
        }

        /// <summary>
        /// Game constructor with custom systems.
        /// </summary>
        /// <param name="inputSystem"></param>
        /// <param name="renderSystem"></param>
        public Game(InputSystem inputSystem, RenderSystem renderSystem)
        {
            Input = inputSystem;
            Renderer = renderSystem;
        }

        /// <summary>
        /// Starts the game application.
        /// This involves initializing the required subsystems, loading resources from the disk, and then preparing the subsystems for running in the main loop.
        /// </summary>
        public void Start()
        {
            if (Renderer is null)
            {
                throw new NullReferenceException("No renderer provided.");
            }

            if (Input is null)
            {
                throw new NullReferenceException("No input system provided.");
            }

            if (ResourceManager is null)
            {
                throw new NullReferenceException("No ResourceManager provided.");
            }

            Mount();
            LoadResources();
            Ready();
            Run();
            Shutdown();
        }

        /// <summary>
        /// Initializes systems using default types and settings.
        /// </summary>
        public void InitializeSystemsDefault()
        {
            ResourceManager.ResourceRoot = ResourceRoot;

            if (Renderer is null)
            {
                Renderer = new RaylibRenderSystem();
            }

            if (Input is null)
            {
                Input = new RaylibInputSystem();
            }

            Input.Start();
            InitializeRenderer();
        }

        public void ShutdownDefault()
        {
            Input?.Dispose();
            Renderer?.Dispose();
            ResourceManager.Dispose();
        }

        /// <summary>
        /// Adds a system that will start together with the game.
        /// </summary>
        /// <param name="system"></param>
        public void AddSystem([DisallowNull] IStartableSystem system)
        {
            _startableSystems.Add(system);
        }

        /// <summary>
        /// Adds a system to the Update step.
        /// </summary>
        /// <param name="system"></param>
        public void AddSystemToUpdate([DisallowNull] IUpdatableSystem system)
        {
            _updatableSystems.Add(system);
        }

        /// <summary>
        /// Adds a system to the render step.
        /// </summary>
        /// <param name="system"></param>
        public void AddSystemToRender([DisallowNull] IUpdatableSystem system)
        {
            _renderableSystems.Add(system);
        }

        /// <summary>
        /// Called when everything has been readied to start the main loop.
        /// </summary>
        public void Run()
        {
            if (Renderer is null)
            {
                throw new NullReferenceException("No renderer provided.");
            }

            if (Input is null)
            {
                throw new NullReferenceException("No input system provided.");
            }

            Stopwatch stopwatch = Stopwatch.StartNew();
            double previousTime = stopwatch.Elapsed.TotalSeconds;

            while (Renderer.ShouldRun)
            {
                double currentTime = stopwatch.Elapsed.TotalSeconds;
                double elapsedTime = currentTime - previousTime;
                previousTime = currentTime;

                _accumulator += elapsedTime;

                Input.Poll();

                while (_accumulator >= UpdateTimeStep)
                {
                    foreach (var system in _updatableSystems)
                    {
                        system.Update(UpdateTimeStep);
                    }

                    Update(UpdateTimeStep);
                    _accumulator -= UpdateTimeStep;
                }

                Renderer.BeginFrame();

                foreach (var system in _renderableSystems)
                {
                    system.Update(Renderer.FrameTime);
                }

                Render(Renderer.FrameTime);
                Renderer.EndFrame();

                RenderFrameTime = TimeSpan.FromSeconds(Renderer.FrameTime);
            }
        }

        /// <summary>
        /// Called when it's time to initialize the subsystems, or modify default game settings or system settings.
        /// </summary>
        public abstract void Initialize();
        /// <summary>
        /// Called when it's safe to manipulate the resources or/and systems.
        /// You can safely create game objects, scenes, etc. in this method.
        /// </summary>
        protected abstract void Ready();

        /// <summary>
        /// Called when it's time to load the game's resources, such as images or sounds.
        /// </summary>
        protected abstract void LoadResources();

        /// <summary>
        /// Triggered on each fixed timestep. Update your game's state here.
        /// </summary>
        /// <param name="deltaTime"></param>
        protected abstract void Update(double deltaTime);
        /// <summary>
        /// Triggered when the renderer is ready to render the next frame.
        /// </summary>
        /// <param name="deltaTime"></param>
        protected abstract void Render(double deltaTime);

        /// <summary>
        /// Called when the application quits and it's safe to clean up.
        /// </summary>
        public abstract void Shutdown();

        private void InitializeRenderer()
        {
            var windowSettings = WindowSettings.Default;

            if (!string.IsNullOrWhiteSpace(Name))
                windowSettings.Title = Name;

            var renderSettings = RendererSettings.Default;
            renderSettings.WindowSettings = windowSettings;

            Renderer?.Start(renderSettings);
        }

        private void Mount()
        {
            var resourceRootMount = new FileSystemMountPoint(ResourceRoot);
            VirtualFileSystem.Mount(resourceRootMount);
        }

        private List<IStartableSystem> _startableSystems = new();
        private List<IUpdatableSystem> _updatableSystems = new();
        private List<IUpdatableSystem> _renderableSystems = new();

        private double _accumulator;
    }
}