using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

namespace DOFerVolumeRenderer
{
    public class DOFComponent : Microsoft.Xna.Framework.DrawableGameComponent
    {
        Game m_game;
        IRenderTargetProvider m_renderTargetProvider;
        ITransferFunctionProvider m_transferFunctionProvider;
        IVolumeDataProvider m_volumeDataProvider;
        IInputHelper m_inputHelper;
        ICamera m_camera = null;
        private IVolumeRendererSettings m_settings = null;

        // Render Targets
        RenderTarget2D[] m_accBufferFrontToFocus = new RenderTarget2D[2];
        RenderTarget2D[] m_accBufferBackToFocus = new RenderTarget2D[2];
        RenderTarget2D m_sliceBuffer;

        // Slice
        Slicer m_slicer = null;
        Plane m_slice;
        CustomVertex1[] m_sliceVertices = new CustomVertex1[18];
        Vector3[] m_slicePoints = new Vector3[18];
        int m_pointCnt = 0;
        VertexBuffer m_sliceVb;

        // Shader
        Effect m_sliceFX = null;
        Effect m_dofCombinerFX = null;


        const float STEPSIZE = 0.005f; // ~ 200 - 350 slices
		static Vector4 DXFIX = new Vector4(-1.0f / 512.0f, 1.0f / 512.0f, 0.0f, 0.0f);
        static Color BLACKTRANSPARENT = new Color(0, 0, 0, 0);

        float m_testDistance = 0.0f;


        // Cube and Plane Model
        Cube m_cube = null;
        YourPlane m_plane = null;

        // Blur
        float R = 1.0f;
        Vector3 m_focusPoint = new Vector3(0.5f, 0.5f, 0.5f);
        bool m_ShowFocusPoint = false;
        bool m_UseGradients = false;
        Matrix m_TextureSpaceMatrix = new Matrix(
            0.5f, 0.0f, 0.0f, 0.0f,
            0.0f, 0.5f, 0.0f, 0.0f,
            0.0f, 0.0f, 0.0f, 0.0f,
            0.5f, 0.5f, 0.0f, 1.0f);
        Matrix m_ScaleMatrix = Matrix.Identity;
        Vector4 m_c = Vector4.Zero;
        Vector2 m_ct = Vector2.Zero;

        float m_blurBlendFactor = 1f;
		int m_blurFactor = 1;


        public DOFComponent(Game game)
            : base(game)
        {
            m_game = game;
        }




        private void SetUpCubeVertices()
        {
            m_cube = new Cube();
            m_cube.GenerateVertexBuffer(m_game);
            m_plane = new YourPlane();
            m_plane.GenerateVertexBuffer(m_game);
        }



        protected override void LoadContent()
        {
            SetUpCubeVertices();

            m_sliceFX = m_game.Content.Load<Effect>("Shaders/DOFSlice");
            m_dofCombinerFX = m_game.Content.Load<Effect>("Shaders/DOFCombiner");
            m_slice = new Plane(Vector3.Forward, m_testDistance);
            m_sliceVb = new VertexBuffer(
                m_game.GraphicsDevice,
                typeof(CustomVertex1),
                18,
                BufferUsage.None
                );
        }



		private float DistanceToFocusPlane
		{
			get
			{
				return Vector3.Distance(m_focusPoint, m_camera.Position);
			}
		}

		private void MoveFocusPlane(float dist)
		{
			var direction = m_camera.FrontVector; // (m_focusPoint - m_camera.Position);
			direction.Normalize();
			m_focusPoint += direction * dist;
		}



        /// <summary>
        /// Allows the game component to perform any initialization it needs to before starting
        /// to run.  This is where it can query for any required services and load content.
        /// </summary>
        public override void Initialize()
        {
            m_renderTargetProvider = m_game.Services.GetService(typeof(IRenderTargetProvider)) as IRenderTargetProvider;
            m_transferFunctionProvider = m_game.Services.GetService(typeof(ITransferFunctionProvider)) as ITransferFunctionProvider;
            m_volumeDataProvider = m_game.Services.GetService(typeof(IVolumeDataProvider)) as IVolumeDataProvider;
            m_inputHelper = m_game.Services.GetService(typeof(IInputHelper)) as IInputHelper;
            m_camera = m_game.Services.GetService(typeof(ICamera)) as ICamera;
            m_settings = m_game.Services.GetService(typeof(IVolumeRendererSettings)) as IVolumeRendererSettings;

            m_accBufferFrontToFocus[0] = m_renderTargetProvider.FloatingTarget1;
            m_accBufferFrontToFocus[1] = m_renderTargetProvider.FloatingTarget2;
            m_accBufferBackToFocus[0] = m_renderTargetProvider.FloatingTarget3;
            m_accBufferBackToFocus[1] = m_renderTargetProvider.FloatingTarget4;
            m_sliceBuffer = m_renderTargetProvider.FloatingTarget5;

            m_slicer = new Slicer(m_camera);

            base.Initialize();
        }

        /// <summary>
        /// Allows the game component to update itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Update(GameTime gameTime)
        {
            KeyboardState keyState = Keyboard.GetState();
            if (keyState.IsKeyDown(Keys.I))
                m_testDistance += 0.01f;
            if (keyState.IsKeyDown(Keys.J))
                m_testDistance -= 0.01f;
            if (keyState.IsKeyDown(Keys.K))
                m_testDistance = 0.0f;



			float focusPlaneMoveSpeed = (float)(0.0001 * gameTime.ElapsedGameTime.TotalMilliseconds);
			if (keyState.IsKeyDown(Keys.PageDown))
            {
				MoveFocusPlane(-focusPlaneMoveSpeed);
				Console.WriteLine("m_focusPoint({0}, {1}, {2})", m_focusPoint.X, m_focusPoint.Y, m_focusPoint.Z);
            }
			if (keyState.IsKeyDown(Keys.PageUp))
            {
				MoveFocusPlane(focusPlaneMoveSpeed);
				Console.WriteLine("m_focusPoint({0}, {1}, {2})", m_focusPoint.X, m_focusPoint.Y, m_focusPoint.Z);
            }



			R = m_settings.BlurFactor;
			m_blurFactor = m_settings.BlurEnabled ? 1 : 0;


			//if (m_inputHelper.IsNewKeyPress(Keys.B))
			//{
			//    m_blurFactor++;
			//    Console.WriteLine("BLUR " + m_blurFactor);
			//}
			//if (m_inputHelper.IsNewKeyPress(Keys.N))
			//{
			//    m_blurFactor--;
			//    Console.WriteLine("BLUR " + m_blurFactor);
			//}



            if (keyState.IsKeyDown(Keys.O))
            {
                m_blurBlendFactor -= 0.001f;
                Console.WriteLine(m_blurBlendFactor);
            }
            if (keyState.IsKeyDown(Keys.L))
            {
                m_blurBlendFactor += 0.001f;
                Console.WriteLine(m_blurBlendFactor);
            }

            if (m_inputHelper.IsNewKeyPress(Keys.F))
                m_ShowFocusPoint = !m_ShowFocusPoint;

            
			m_UseGradients = m_settings.GradientsEnabled;



        }


        /// <summary>
        /// slicelol
        /// </summary>
        /// <param name="gameTime"></param>
        public override void Draw(GameTime gameTime)
        {

            m_slice.Normal = m_camera.FrontVector; // Update m_slice normal for calculations
            m_slice.D = 0; // Reset Distance

            float nearDist = Vector3.Dot(Vector3.Normalize(m_slice.Normal), m_cube.Corners[0]) + 2.0f; // ... just make sure the plane is outside of the "unit cube" to compare the distances
            Vector3 bbNear = m_cube.Corners[0];
            Vector3 bbFar = m_cube.Corners[0];
            float farDist = nearDist;
            float tmpDist = 0.0f;
            for (int i = 1; i < 8; i++)
            {
                tmpDist = Vector3.Dot(Vector3.Normalize(m_slice.Normal), m_cube.Corners[i]) + 2.0f; // ... just make sure the plane is outside of the "unit cube" to compare the distances
                if (tmpDist < nearDist)
                {
                    bbNear = m_cube.Corners[i];
                    nearDist = tmpDist;
                }

                if (tmpDist > farDist)
                {
                    bbFar = m_cube.Corners[i];
                    farDist = tmpDist;
                }
            }

            float distPlaneZero = 0; // Plane to (0/0/0) distance
            float distPlaneFar = (Vector3.Dot(Vector3.Normalize(m_slice.Normal), bbFar));// Plane to furthest Point distance
            float distZeroFar = distPlaneZero - distPlaneFar;

            farDist = (Vector3.Dot(Vector3.Normalize(m_slice.Normal), bbFar));
            nearDist = (Vector3.Dot(Vector3.Normalize(m_slice.Normal), bbNear));

            float marchingDistance = Math.Abs(farDist - nearDist);

            float m_focusDist = (Vector3.Dot(Vector3.Normalize(m_slice.Normal), m_focusPoint));



            // jetzt do wirds intARessant auf OPAK



            GraphicsDevice.RasterizerState = new RasterizerState() { CullMode = CullMode.None };
            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.DepthStencilState = DepthStencilState.None; // disable depth test




            int numTotalSlices = (int)(Math.Round(marchingDistance / STEPSIZE));

            // calc #slices from farthest distance to focus-plane
            int numSlicesBackToFocus = (int)(Math.Abs(farDist - m_focusDist) / STEPSIZE);

            // calc #slices from nearest distance to focus-plane
            int numSlicesFrontToFocus = numTotalSlices - numSlicesBackToFocus;

            numSlicesBackToFocus += 1;

            //Console.WriteLine(string.Format("backSlices[{0}], frontSlices[{1}], total[{2}]", numSlicesBackToFocus, numSlicesFrontToFocus, numTotalSlices));


			#region back_to_focus
            // ********************* RENDER BACK TO FOCUS-PLANE *******************
            GraphicsDevice.SetRenderTarget(m_accBufferFrontToFocus[0]);
            GraphicsDevice.Clear(Color.Black);
            GraphicsDevice.SetRenderTarget(m_accBufferFrontToFocus[1]);
            GraphicsDevice.Clear(Color.Black);
            GraphicsDevice.SetRenderTarget(m_accBufferBackToFocus[0]);
            GraphicsDevice.Clear(Color.Black);
            GraphicsDevice.SetRenderTarget(m_accBufferBackToFocus[1]);
            GraphicsDevice.Clear(Color.Black);


            SpriteBatch sb = new SpriteBatch(GraphicsDevice);

            var lolrect = new Rectangle(0, 0, 512, 512);
			//Vector2 PixelSize = new Vector2((1f / 512.0f) * (float)(m_blurFactor), (1f / 512.0f) * (float)(m_blurFactor));

            Vector3 focusPointViewSpace = Vector3.Transform(m_focusPoint, m_camera.ViewMatrix);

            int idx0b = 0;
            int idx1b = 1;

            for (int i = 0; i < numSlicesBackToFocus; i++)
            {

                // switch buffers
                idx0b = 1 - idx0b;
                idx1b = 1 - idx1b;

                m_slice.D = -farDist + STEPSIZE * i + m_testDistance;
                m_slicer.planeBoxIntersection(m_slice, m_cube.MinCoordinates, m_cube.MaxCoordinates, ref m_slicePoints, out m_pointCnt);

                if (m_pointCnt > 0)
                {

                    // Set slice Buffer
                    GraphicsDevice.SetRenderTarget(m_sliceBuffer);
                    GraphicsDevice.Clear(Color.Black);
                    GraphicsDevice.BlendState = BlendState.Opaque;

                    CalcCircleOfConfusion(m_slicePoints[0].Z, focusPointViewSpace.Z);

                    // Draw current slice
                    DrawSlice(numTotalSlices);


                    // Blende AccBuffer mit Aktuellem Slice TODO: Blend shader usw
                    GraphicsDevice.SetRenderTarget(m_accBufferBackToFocus[idx0b]);
                    GraphicsDevice.BlendState = BlendState.NonPremultiplied;
					//float texelOffset[] = {-1.0f/512.0f, 1.0f/512.0f, 0.0f, 0.0f};
					//device->SetVertexShaderConstantF(0, texelOffset, 1);

                    // combine with previous slice and write in intermediate result buffer
                    m_game.GraphicsDevice.SetVertexBuffer(m_plane.VertexBuffer);
                    //GraphicsDevice.Clear(Color.Black);

                    m_dofCombinerFX.Parameters["curSlice"].SetValue(m_sliceBuffer);
                    //m_dofCombinerFX.Parameters["prevSlice"].SetValue(m_accBufferFrontToFocus[idx1]);
                    m_dofCombinerFX.Parameters["prevAcc"].SetValue(m_accBufferBackToFocus[idx1b]);
					//m_dofCombinerFX.Parameters["PixelSize"].SetValue(PixelSize * m_ct);
                    m_dofCombinerFX.Parameters["Ct"].SetValue(m_ct);
					m_dofCombinerFX.Parameters["DoBlur"].SetValue(m_blurFactor > 0 ? 1 : 0);
					m_dofCombinerFX.Parameters["texel_offset"].SetValue(DXFIX);
                    foreach (EffectPass pass in m_dofCombinerFX.CurrentTechnique.Passes)
                    {
                        pass.Apply();
                        GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, m_plane.Vertices.Length / 3);
                    }
                }
			}
			#endregion


            #region front_to_focus
            // ********************* RENDER FRONT TO FOCUS-PLANE *******************


			int idx0f = 0;
			int idx1f = 1;

            for (int i = 0; i < numSlicesFrontToFocus; i++)
            {
                // switch buffers
                idx0f = 1 - idx0f;
                idx1f = 1 - idx1f;

                m_slice.D = -nearDist - STEPSIZE * i + m_testDistance;
                m_slicer.planeBoxIntersection(m_slice, m_cube.MinCoordinates, m_cube.MaxCoordinates, ref m_slicePoints, out m_pointCnt);
                if (m_pointCnt > 0)
                {

                    // draw the current slice
                    GraphicsDevice.SetRenderTarget(m_sliceBuffer);
                    GraphicsDevice.Clear(Color.Black);
                    GraphicsDevice.BlendState = BlendState.Opaque;

					CalcCircleOfConfusion(m_slicePoints[0].Z, focusPointViewSpace.Z);
                    DrawSlice(numTotalSlices);

                    // Blende AccBuffer mit Aktuellem Slice TODO: Blend shader usw
                    GraphicsDevice.SetRenderTarget(m_accBufferFrontToFocus[idx0f]);
                    GraphicsDevice.BlendState = BlendState.NonPremultiplied;

                    // combine with previous slice and write in intermediate result buffer
                    m_game.GraphicsDevice.SetVertexBuffer(m_plane.VertexBuffer);
                    //GraphicsDevice.Clear(Color.Black);

                    m_dofCombinerFX.Parameters["curSlice"].SetValue(m_sliceBuffer);
                    //m_dofCombinerFX.Parameters["prevSlice"].SetValue(m_accBufferFrontToFocus[idx1]);
                    m_dofCombinerFX.Parameters["prevAcc"].SetValue(m_accBufferFrontToFocus[idx1f]);
					//m_dofCombinerFX.Parameters["PixelSize"].SetValue(PixelSize * m_ct);
					m_dofCombinerFX.Parameters["Ct"].SetValue(m_ct);
					m_dofCombinerFX.Parameters["DoBlur"].SetValue(m_blurFactor > 0 ? 1 : 0);
					m_dofCombinerFX.Parameters["texel_offset"].SetValue(DXFIX);
                    foreach (EffectPass pass in m_dofCombinerFX.CurrentTechnique.Passes)
                    {
                        pass.Apply();
                        GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, m_plane.Vertices.Length / 3);
                    }
                }
            }
            #endregion

            sb.Dispose();

            #region focus_point
            if (m_ShowFocusPoint)
            {
                GraphicsDevice.SetRenderTarget(m_renderTargetProvider.RGBTarget1);
                GraphicsDevice.Clear(Color.Black);
                // Render Focus Point:
                m_game.GraphicsDevice.SetVertexBuffer(m_cube.VertexBuffer);
                Matrix transMat = Matrix.Identity;
                transMat.Translation = m_focusPoint;
                transMat.M11 = 0.05f;
                transMat.M22 = 0.05f;
                transMat.M33 = 0.05f;
                BasicEffect basicFX = new BasicEffect(m_game.GraphicsDevice);
                basicFX.World = transMat;
                basicFX.View = m_camera.ViewMatrix;
                basicFX.Projection = m_camera.ProjectionMatrix;
                basicFX.DiffuseColor = new Vector3(1, 1, 1);
                foreach (EffectPass pass in basicFX.CurrentTechnique.Passes)
                {
                    pass.Apply();
                    //GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, 1, VertexPositionColor.VertexDeclaration);
                    GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, m_cube.Vertices.Length / 3);

                }
            }
            #endregion

            #region results
            GraphicsDevice.SetRenderTarget(m_renderTargetProvider.RGBTarget2);
            GraphicsDevice.Clear(Color.Black);
            // Render Result
            GraphicsDevice.SetRenderTarget(m_renderTargetProvider.ResultScreenTarget);
            GraphicsDevice.BlendState = BlendState.Opaque;



            using (SpriteBatch sprite = new SpriteBatch(GraphicsDevice))
            {

                sprite.Begin(SpriteSortMode.Deferred, BlendState.Opaque, SamplerState.PointClamp, DepthStencilState.Default, RasterizerState.CullNone);
                //Black BG
				const int resultSize = 512;
				var resultRect = new Rectangle(692, 2, resultSize, resultSize);
				sprite.Draw(m_renderTargetProvider.RGBTarget2, resultRect, Color.White);
                // Front Slice and Back slice
				var backRect = new Rectangle(434, 2, 255, 255);
                sprite.Draw(m_accBufferBackToFocus[idx0b], backRect, Color.White);
				var frontRect = new Rectangle(434, 260, 255, 254);
                sprite.Draw(m_accBufferFrontToFocus[idx0f], frontRect, Color.White);
                sprite.End();

                //result
                sprite.Begin(SpriteSortMode.Deferred, BlendState.Additive, SamplerState.PointClamp, DepthStencilState.Default, RasterizerState.CullNone);
				sprite.Draw(m_accBufferBackToFocus[idx0b], resultRect, Color.White);
                if (m_ShowFocusPoint)
                {
					sprite.Draw(m_renderTargetProvider.RGBTarget1, resultRect, Color.White);
					sprite.Draw(m_renderTargetProvider.RGBTarget1, backRect, Color.White);
					sprite.Draw(m_renderTargetProvider.RGBTarget1, frontRect, Color.White);
                }
				sprite.Draw(m_accBufferFrontToFocus[idx0f], resultRect, Color.White);
                sprite.End();
            }
            #endregion
        }


		/// <summary>
		/// Calculate radius of circle of confusion
		/// </summary>
		/// <param name="sliceZ"></param>
		/// <returns></returns>
        private float CalcRadius(float z, float zf)
        {
			// d = |zf - z|
            float d = Math.Abs(zf - z);
            return R * d;
        }

		/// <summary>
		/// Calculate circle of confusion matrix in texture-space
		/// </summary>
		/// <param name="sliceZ"></param>
        private void CalcCircleOfConfusion(float z, float zf)
        {
            //float radius = CalcRadius(sliceZ, focusZ);
            float radius = CalcRadius(z, zf);
            m_c.X = radius;
            m_c.Y = radius;
            m_c.Z = Math.Abs(z); // immer positiv sein muss?
            m_c.W = 1.0f;
            m_c = Vector4.Transform(m_c, m_camera.ProjectionMatrix);
            m_c = Vector4.Transform(m_c, m_TextureSpaceMatrix);

            m_ct.X = (m_c.X / m_c.W);
            m_ct.Y = (m_c.Y / m_c.W);
			m_ct = m_ct / 512f;

			//m_ScaleMatrix.M11 = m_ct.X;
			//m_ScaleMatrix.M22 = m_ct.Y;
        }


		private void DrawSlice(int iterations)
        {
            if (m_pointCnt > 0)
            {
                for (int i = 0; i < m_pointCnt * 3; i++)
                {
                    //m_sliceVertices[i].Position = Vector3.Transform(m_slicePoints[i], m_ScaleMatrix);
                    //m_sliceVertices[i].Position = Vector3.Transform(m_sliceVertices[i].Position, m_camera.InverseViewMatrix);
                    m_sliceVertices[i].Position = Vector3.Transform(m_slicePoints[i], m_camera.InverseViewMatrix);
                }

                // update vertices in vertex-buffer: unbind => upload vertex-data => bind
                m_game.GraphicsDevice.SetVertexBuffer(null);
                m_sliceVb.SetData<CustomVertex1>(m_sliceVertices);
                m_game.GraphicsDevice.SetVertexBuffer(m_sliceVb);

                m_sliceFX.Parameters["World"].SetValue(Matrix.Identity);
                m_sliceFX.Parameters["View"].SetValue(m_camera.ViewMatrix);
                m_sliceFX.Parameters["Projection"].SetValue(m_camera.ProjectionMatrix);
                m_sliceFX.Parameters["VolumeTexture"].SetValue(m_volumeDataProvider.GetVolume());
                m_sliceFX.Parameters["TransferTexture"].SetValue(m_transferFunctionProvider.GetTransferFunction().Texture);
                m_sliceFX.Parameters["Iterations"].SetValue(iterations);
                m_sliceFX.Parameters["AlphaFactor"].SetValue(m_settings.AlphaFactor);
                m_sliceFX.Parameters["UseGradients"].SetValue(m_UseGradients);
                m_sliceFX.Parameters["L"].SetValue(m_camera.Position);


                foreach (EffectPass pass in m_sliceFX.CurrentTechnique.Passes)
                {
                    pass.Apply();
                    GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, m_pointCnt);
                }
            }
        }

    }
}
