using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace HiTest
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 720;
            graphics.PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8;

            IsFixedTimeStep = false;
            graphics.SynchronizeWithVerticalRetrace = false;

            Content.RootDirectory = "Content";

            
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            branchingRenderTarget = new RenderTarget2D(GraphicsDevice, GraphicsDevice.PresentationParameters.BackBufferWidth, GraphicsDevice.PresentationParameters.BackBufferHeight);
            Components.Add(new FrameRateCounter(this));
            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            effectExpensive = Content.Load<Effect>(@"Expensive");
            effectSetDepth = Content.Load<Effect>(@"SetDepth");
            effectClip = Content.Load<Effect>(@"Clip");
            font = Content.Load<SpriteFont>(@"Standard");
            testImage = Content.Load<Texture2D>(@"TestImage");
            testImageObscured = Content.Load<Texture2D>(@"TestImageObscured");

            white = new Texture2D(this.GraphicsDevice, 1, 1);
            Color[] foo = new Color[] { Color.Gray };
            white.SetData(foo);
        }

        private RenderTarget2D branchingRenderTarget;
        private Texture2D testImage;
        private Texture2D testImageObscured;
        private SpriteFont font;
        private Texture2D white;

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            branchingRenderTarget.Dispose();
        }

        KeyboardState previousKeyboardState;
        KeyboardState currentKeyboardState;

        private bool IsKeyPress(Keys key)
        {
            return currentKeyboardState.IsKeyDown(key) && !previousKeyboardState.IsKeyDown(key);
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            currentKeyboardState = Keyboard.GetState();

            if (IsKeyPress(Keys.D))
            {
                setDepth = !setDepth;
            }

            if (IsKeyPress(Keys.P))
            {
                setPerPixelDepth = !setPerPixelDepth;
            }

            if (IsKeyPress(Keys.C))
            {
                alphaTest = !alphaTest;
            }

            if (IsKeyPress(Keys.B))
            {
                obscureViaBranching = !obscureViaBranching;
            }

            if (IsKeyPress(Keys.S))
            {
                int temp = (int)stencilObscure;
                temp++;
                temp %= (int)StencilObscure.Max;
                stencilObscure = (StencilObscure)temp;
            }

            /*
            if (IsKeyPress(Keys.R))
            {
                reloadStencil = !reloadStencil;
            }*/

            if (IsKeyPress(Keys.Space))
            {
                disableExpensiveDrawing = !disableExpensiveDrawing;
            }

            previousKeyboardState = currentKeyboardState;

            base.Update(gameTime);
        }

        private void DrawText(float x, float y)
        {
            float lineHeight = font.MeasureString("M").Y;
            spriteBatch.Begin();
            spriteBatch.DrawString(font, string.Format("(D) Obscure by depth: {0}", setDepth), new Vector2(x, y), Color.White);
            y += lineHeight;
            spriteBatch.DrawString(font, string.Format("(P) Obscure by per pixel depth: {0}", setPerPixelDepth), new Vector2(x, y), Color.White);
            y += lineHeight;
            spriteBatch.DrawString(font, string.Format("(C) Obscure by clip shader: {0}", alphaTest), new Vector2(x, y), Color.White);
            y += lineHeight;
            spriteBatch.DrawString(font, string.Format("(S) Obscure by stencil: {0}", stencilObscure), new Vector2(x, y), Color.White);
            y += lineHeight;
            spriteBatch.DrawString(font, string.Format("(B) Obscure by branching: {0}", obscureViaBranching), new Vector2(x, y), Color.White);
            y += lineHeight;
            spriteBatch.End();
        }

        private Effect effectExpensive;
        private Effect effectSetDepth;
        private Effect effectClip;

        private bool disableExpensiveDrawing;
        private bool setDepth;
        private bool setPerPixelDepth;
        private bool alphaTest;
        private bool obscureViaBranching;
        private StencilObscure stencilObscure;

        enum StencilObscure
        {
            None = 0,
            SpecificValue = 1,
            Increment = 2,
            Max = 3,
        };

        private const int StencilValue = 1;

        // Write a value to the stencil buffer, not affecting other stencil values.
        DepthStencilState replaceStencilState = new DepthStencilState()
        {
            // Don't write to depth
            DepthBufferEnable = false,
            DepthBufferWriteEnable = false,

            // Always set stencil
            StencilEnable = true,
            StencilFunction = CompareFunction.Always,
            StencilPass = StencilOperation.Replace,
            StencilFail = StencilOperation.Keep,
            StencilMask = Int32.MaxValue,
            StencilWriteMask = Int32.MaxValue,
            ReferenceStencil = StencilValue,
        };

        DepthStencilState incrementStencilState = new DepthStencilState()
        {
            // Don't write to depth
            DepthBufferEnable = false,
            DepthBufferWriteEnable = false,

            // Always set stencil
            StencilEnable = true,
            StencilFunction = CompareFunction.Always,
            StencilPass = StencilOperation.Increment,
            StencilFail = StencilOperation.Decrement,
            StencilMask = Int32.MaxValue,
            StencilWriteMask = Int32.MaxValue,
            ReferenceStencil = 1,
        };

        DepthStencilState readStencilStateSpecificValue = new DepthStencilState()
        {
            // Don't write to depth
            DepthBufferEnable = false,           // Don't even read from depth
            DepthBufferWriteEnable = false,

            // Don't touch the stencil buffer:
            StencilPass = StencilOperation.Keep,
            StencilFail = StencilOperation.Keep,
            StencilWriteMask = Int32.MaxValue,

            // But draw stuff where it isn't equal to the value we wrote earlier.
            StencilEnable = true,
            StencilFunction = CompareFunction.NotEqual,
            StencilMask = Int32.MaxValue,
            ReferenceStencil = StencilValue,
        };

        DepthStencilState readStencilStateIncrement = new DepthStencilState()
        {
            // Don't write to depth
            DepthBufferEnable = false,           // Don't even read from depth
            DepthBufferWriteEnable = false,

            // Don't touch the stencil buffer:
            StencilPass = StencilOperation.Keep,
            StencilFail = StencilOperation.Keep,
            StencilWriteMask = Int32.MaxValue,

            // But draw stuff where it is still equal to zero
            StencilEnable = true,
            StencilFunction = CompareFunction.NotEqual,
            StencilMask = Int32.MaxValue,
            ReferenceStencil = 1,
        };

        // Test reload, by zeroing out where we have things. Whereever we have a 0, we'll
        // zero it out. This was a test based on the last post here:
        // http://www.gamedev.net/topic/577169-texkill-is-disabling-early-stencil-in-the-xbox-360/
        // But it had no effect.
        /*
        DepthStencilState reloadHiStencilState = new DepthStencilState()
        {
            // Don't write or read to depth
            DepthBufferEnable = false,
            DepthBufferWriteEnable = false,

            StencilPass = StencilOperation.Replace,
            StencilFail = StencilOperation.Zero,
            StencilWriteMask = Int32.MaxValue,
            StencilEnable = true,
            StencilFunction = CompareFunction.Equal,
            StencilMask = Int32.MaxValue,
            ReferenceStencil = 1,
        };
        private bool reloadStencil;*/

        DepthStencilState writeDepthNoRead = new DepthStencilState()
        {
            DepthBufferEnable = true,
            DepthBufferWriteEnable = true,
            DepthBufferFunction = CompareFunction.Always,
        };

        private const float MainImageDepth = 0.9f;    // Near the back
        private const float ThingsInFrontDepth = 0.5f;

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            if (obscureViaBranching)
            {
                // We don't really need to do this, but it helps simulate what you might need to do to get this working.
                // We're basically pretending to write some values to a render target, which we will then sample to determine
                // if we should branch.
                GraphicsDevice.SetRenderTarget(branchingRenderTarget);
                GraphicsDevice.Clear(Color.CornflowerBlue);
                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, null, null, null, null);
                spriteBatch.Draw(testImageObscured, new Rectangle(0, 0, 1280, 720), null, Color.White, 0f, Vector2.Zero, SpriteEffects.None, ThingsInFrontDepth);
                spriteBatch.End();
                GraphicsDevice.SetRenderTarget(null);
            }

            GraphicsDevice.Clear(ClearOptions.DepthBuffer | ClearOptions.Stencil | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1f, 0);

            // The assumption is that the final image should always be blocked by things in front of it.
            // So we need to ensure that the things that come before write to depth (But not read from it, as we don't want their effect dilluted).
            // In addition, they need to write values less than MainImageDepth.

            if (stencilObscure != StencilObscure.None)
            {
                DepthStencilState dss = (stencilObscure == StencilObscure.SpecificValue) ? replaceStencilState : incrementStencilState;
                // Write all over the stencil buffer.
                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, null, dss, null, null);
                spriteBatch.Draw(testImageObscured, new Rectangle(0, 0, 1280, 720), null, Color.White, 0f, Vector2.Zero, SpriteEffects.None, ThingsInFrontDepth);
                spriteBatch.End();
            }

            if (setDepth)
            {
                // Just set regular depth using geometry
                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, null, writeDepthNoRead, null, null);
                spriteBatch.Draw(testImageObscured, new Rectangle(0, 0, 1280, 720), null, Color.White, 0f, Vector2.Zero, SpriteEffects.None, ThingsInFrontDepth);
                spriteBatch.End();
            }

            if (setPerPixelDepth)
            {
                // Set depth per pixel
                effectSetDepth.Parameters["MaxDepthValue"].SetValue(MainImageDepth - 0.01f);
                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, null, writeDepthNoRead, null, effectSetDepth);
                spriteBatch.Draw(testImageObscured, new Rectangle(0, 0, 1280, 720), null, Color.White); // Doesn't matter what we specify for depth, since this effect sets it.
                spriteBatch.End();
            }

            if (alphaTest)
            {
                // Use clip in the shader
                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, null, writeDepthNoRead, null, effectClip);
                spriteBatch.Draw(testImageObscured, new Rectangle(0, 0, 1280, 720), null, Color.White, 0f, Vector2.Zero, SpriteEffects.None, ThingsInFrontDepth);
                spriteBatch.End();
            }

            /*
            if (reloadStencil)
            {
                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, null, reloadHiStencilState, null, null);
                spriteBatch.Draw(white, new Rectangle(0, 0, 1280, 720), null, Color.White, 0f, Vector2.Zero, SpriteEffects.None, ThingsInFrontDepth);
                spriteBatch.End();
            }*/

            // Now run the expensive shader.
            if (!disableExpensiveDrawing)
            {
                // Needed because we replace the spritebatch vertex shader:
                effectExpensive.Parameters["InverseViewPort"].SetValue(new Vector2(1f / GraphicsDevice.Viewport.Width, 1f / GraphicsDevice.Viewport.Height));

                if (obscureViaBranching)
                {
                    // First, draw our initialize image (in case we didn't do it before)
                    spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, null, null, null, null);
                    spriteBatch.Draw(testImageObscured, new Rectangle(0, 0, 1280, 720), null, Color.White, 0f, Vector2.Zero, SpriteEffects.None, ThingsInFrontDepth);
                    spriteBatch.End();

                    // Now draw the expensive one, but with a dynamic branch in the shader code so that we skip the expensive operations based on the value sampled 
                    // from branchingRenderTarget
                    effectExpensive.Parameters["PseudoStencilTexture"].SetValue(branchingRenderTarget);
                    GraphicsDevice.SamplerStates[1] = SamplerState.PointClamp;
                    effectExpensive.CurrentTechnique = effectExpensive.Techniques["Branching"];
                    spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, null, null, null, effectExpensive);
                    spriteBatch.Draw(testImage, Vector2.Zero, null, Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, MainImageDepth);
                    spriteBatch.End();
                }
                else
                {
                    effectExpensive.CurrentTechnique = effectExpensive.Techniques["Standard"];
                    DepthStencilState dss = DepthStencilState.DepthRead;
                    if (stencilObscure == StencilObscure.SpecificValue)
                    {
                        // We only want to draw where the stencil is not equal to our value.
                        dss = readStencilStateSpecificValue;
                    }
                    else if (stencilObscure == StencilObscure.Increment)
                    {
                        dss = readStencilStateIncrement;
                    }
                    spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, null, dss, null, effectExpensive);
                    spriteBatch.Draw(testImage, Vector2.Zero, null, Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, MainImageDepth);
                    spriteBatch.End();
                }
            }

            // Instructions:
            DrawText(110, 80);

            base.Draw(gameTime);
        }
    }
}
