using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using RecrownedAthenaeum.Render;
using System;
using System.Diagnostics;

namespace RecrownedAthenaeum.ScreenSystem
{
    /// <summary>
    /// Called when the first screen is being shown.
    /// </summary>
    /// <param name="screen">The screen to show after the loading screen.</param>
    public delegate void NeedNextScreen(Screen screen);

    /// <summary>
    /// A manager for screens. Helps with transitions and updating screens as well as resizes.
    /// </summary>
    public class ScreenManager : IDisposable
    {
        bool disposed = false;

        /// <summary>
        /// Called when the first loading screen is done, and needs to show the landing screen.
        /// </summary>
        public event NeedNextScreen NeedNextScreen;

        /// <summary>
        /// The settings this manager will use to begin a sprite batch.
        /// </summary>
        private GraphicsDeviceManager graphics;
        private Screen previousScreen;
        private RenderTarget2D previousScreenRenderTarget;
        private Screen currentScreen;
        private bool firstScreenChangeComplete;
        private bool resizing;
        /// <summary>
        /// Currently displayed screen.
        /// </summary>
        public Screen Screen
        {
            get
            {
                return currentScreen;
            }
            set
            {
                previousScreen = currentScreen;
                currentScreen = value;
                if (Screen.width != graphics.PreferredBackBufferWidth || Screen.height != graphics.PreferredBackBufferHeight)
                {
                    Screen.ApplySize(graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);
                }
                if (previousScreen != null && previousScreenRenderTarget == null && previousScreen.UseRenderTargetForExitTransition)
                {
                    previousScreenRenderTarget = new RenderTarget2D(graphics.GraphicsDevice, graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);
                }
                graphics.GraphicsDevice.SetRenderTarget(previousScreenRenderTarget);
                graphics.GraphicsDevice.Clear(Color.Black);

                Debug.WriteLine("Showing " + value.GetType().Name);
                Screen.Show();
                previousScreen?.Hide();
            }
        }

        /// <summary>
        /// Creates a screen manager that helps update, draw, and manage multiple screens and their transitions. Uses its own <see cref="SpriteBatch"/>. 
        /// </summary>
        /// <param name="graphicsDeviceManager">The graphics device manager to be used.</param>
        public ScreenManager(GraphicsDeviceManager graphicsDeviceManager)
        {
            graphics = graphicsDeviceManager ?? throw new ArgumentNullException("Graphics device manager argument cannot be null.");
        }

        /// <summary>
        /// Updates the screens. Should be called once every frame.
        /// </summary>
        /// <param name="gameTime">Contains the time that has passed from the last frame.</param>
        /// <param name="waiting">Whether or not there is something a transition should be waiting for. Usually used to wait for assets to complete loading.</param>
        public void UpdateCurrentScreen(GameTime gameTime, bool waiting)
        {
            waiting = !waiting;
            switch (Screen.State)
            {
                case ScreenState.EnterTransition:
                    if (previousScreen != null && previousScreen.UseRenderTargetForExitTransition)
                    {
                        previousScreen.UpdateTransition(gameTime.ElapsedGameTime.TotalSeconds, waiting);
                    }
                    Screen.UpdateTransition(gameTime.ElapsedGameTime.TotalSeconds, waiting);
                    break;
                case ScreenState.ExitTransition:
                    if (Screen.UseRenderTargetForExitTransition || Screen.UpdateTransition(gameTime.ElapsedGameTime.TotalSeconds, waiting))
                    {
                        if (!firstScreenChangeComplete)
                        {
                            firstScreenChangeComplete = true;
                            OnNeedNextScreen(Screen);
                        }
                        if (Screen.NextScreen != null)
                        {
                            Debug.WriteLine("Changing to the next given screen.");
                            Screen = Screen.NextScreen;
                        }
                        else if (previousScreen != null)
                        {
                            Debug.WriteLine("Changing to previous screen.");
                            Screen = previousScreen;
                        }
                        else
                        {
                            throw new InvalidOperationException("No next screen provided and no previous screen to return to.");
                        }
                    }
                    break;
            }
            Screen.Update(gameTime);
        }

        /// <summary>
        /// Renders screen into window.
        /// Starts and ends the given <paramref name="consistentSpriteBatch"/>.
        /// </summary>
        /// <param name="consistentSpriteBatch">The consistent sprite batch to use. Needs to be consistent as changing render targets requires ending and beginning the sprite batch.</param>
        public void DrawScreen(ConsistentSpriteBatch consistentSpriteBatch)
        {
            if (consistentSpriteBatch == null)
            {
                throw new ArgumentNullException(nameof(consistentSpriteBatch));
            }
            if (Screen == null) return;

            graphics.GraphicsDevice.Clear(Screen.BackgroundColor);
            if (Screen.State == ScreenState.EnterTransition && previousScreen != null && previousScreen.UseRenderTargetForExitTransition)
            {
                graphics.GraphicsDevice.SetRenderTarget(previousScreenRenderTarget);
                graphics.GraphicsDevice.Clear(previousScreen.BackgroundColor);

                consistentSpriteBatch.Begin();
                previousScreen.Draw(consistentSpriteBatch);
                consistentSpriteBatch.End();

                graphics.GraphicsDevice.SetRenderTarget(null);
                Screen.UpdatePreviousScreenFrame(previousScreenRenderTarget);
            }

            consistentSpriteBatch.Begin();
            Screen.Draw(consistentSpriteBatch);
            consistentSpriteBatch.End();
        }

        /// <summary>
        /// Should be called when resize is occurring to change to a loading screen.
        /// This will notify the screen of the status of the assets, change the screen to a loading screen, and dispose of the previous screen buffer.
        /// </summary>
        /// <param name="loadingScreen">The loading screen to change to.</param>
        public void Resize(LoadingScreen loadingScreen)
        {
            if (resizing) throw new InvalidOperationException("Already resizing.");
            resizing = true;
            Screen.AssetLoadStateChange(false);
            Screen = loadingScreen;
            previousScreenRenderTarget.Dispose();
            previousScreenRenderTarget = null;
        }

        /// <summary>
        /// Notifies all screen that assets have completed being loaded after a resize.
        /// </summary>
        public void PostResize()
        {
            if (!resizing) throw new InvalidOperationException("Was never resizing.");
            Screen.AssetLoadStateChange(true);
        }

        private void OnNeedNextScreen(Screen screen)
        {
            NeedNextScreen?.Invoke(screen);
        }

        /// <summary>
        /// Disposes this.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// An overridable dispose.
        /// </summary>
        /// <param name="disposing">True of user invoked dispose called.</param>
        public virtual void Dispose(bool disposing)
        {
            if (disposed) throw new ObjectDisposedException(GetType().Name);
            if (disposing)
            {
                previousScreenRenderTarget?.Dispose();
            }
            disposed = true;
        }

        /// <summary>
        /// Destructor.
        /// </summary>
        ~ScreenManager()
        {
            Dispose(false);
        }
    }
}
