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 DOFerVolumeRenderer
{
	/// <summary>
	/// This is a game component that implements IUpdateable.
	/// </summary>
	public class SliceBasedComponent : Microsoft.Xna.Framework.DrawableGameComponent
	{
		Game m_game;
		IRenderTargetProvider m_renderTargetProvider;
		ITransferFunctionProvider m_transferFunctionProvider;
		IVolumeDataProvider m_volumeDataProvider;
		IInputHelper m_inputHelper;
		ICamera m_camera = null;

		Slicer m_slicer = null;

		// Slicing
		RenderTarget2D m_FrontBuffer; // accumulate slices in the Front of the focal plane
		RenderTarget2D m_BackBuffer; // accumulate slices at the Back of the focal plane
		Plane m_slice;
		CustomVertex1[] m_sliceVertices = new CustomVertex1[18];
		Vector3[] m_slicePoints = new Vector3[18];
		int m_pointCnt = 0;
		VertexBuffer m_sliceVb;
		Vector3 m_focusPoint;

		const float STEPSIZE = 0.025f; // ~ 200 - 350 slices
		static Color BLACKTRANSPARENT = new Color(0, 0, 0, 0);

		float m_testDistance = 0.0f;


		Effect m_sliceFX = null;

		Cube m_cube = null;



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




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



		protected override void LoadContent()
		{
			SetUpCubeVertices();

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



		/// <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_FrontBuffer = m_renderTargetProvider.RGBTarget1;
			m_BackBuffer = m_renderTargetProvider.RGBTarget2;

			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;
		}


		/// <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; // -10.0 ... 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; // -10.0 ... 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);

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




			GraphicsDevice.RasterizerState = new RasterizerState() { CullMode = CullMode.None };
			GraphicsDevice.BlendState = BlendState.Additive;
			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));


			// ********************* RENDER BACK TO FOCUS-PLANE *******************

			GraphicsDevice.SetRenderTarget(m_BackBuffer); // render to texture
			GraphicsDevice.Clear(ClearOptions.Target, Color.Black, 1.0f, 0);

			for (int j = 0; j < numSlicesBackToFocus; j++) 
			{
				m_slice.D = -farDist + STEPSIZE * j + m_testDistance;
				m_slicer.planeBoxIntersection(m_slice, m_cube.MinCoordinates, m_cube.MaxCoordinates, ref m_slicePoints, out m_pointCnt);
				// upload m_slicePoints to vertex-buffer and draw
				DrawSlice(numTotalSlices);				
			}


			// ********************* RENDER FRONT TO FOCUS-PLANE *******************

			GraphicsDevice.SetRenderTarget(m_FrontBuffer); // render to texture
			GraphicsDevice.Clear(ClearOptions.Target, Color.Black, 1.0f, 0);

			for (int j = 0; j < numSlicesFrontToFocus; j++) 
			{
				m_slice.D = -nearDist - STEPSIZE * j + m_testDistance;
				m_slicer.planeBoxIntersection(m_slice, m_cube.MinCoordinates, m_cube.MaxCoordinates, ref m_slicePoints, out m_pointCnt);
				// upload m_slicePoints to vertex-buffer and draw
				DrawSlice(numTotalSlices);
			}

			

			// Render Result
			GraphicsDevice.SetRenderTarget(m_renderTargetProvider.ResultScreenTarget);
			using (SpriteBatch sprite = new SpriteBatch(GraphicsDevice))
			{
				sprite.Begin(SpriteSortMode.Deferred, BlendState.Opaque);
				sprite.Draw(m_FrontBuffer, new Rectangle(514, 0, 512, 512), Color.Black);
				sprite.End();

				sprite.Begin(SpriteSortMode.Deferred, BlendState.Additive);
				sprite.Draw(m_BackBuffer, new Rectangle(514, 0, 512, 512), Color.White);
				sprite.Draw(m_FrontBuffer, new Rectangle(514, 0, 512, 512), Color.White);
				sprite.End();
			}
		}


		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_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["PixelSize"].SetValue(new Vector2(1.0f / GraphicsDevice.PresentationParameters.BackBufferWidth, 1.0f / GraphicsDevice.PresentationParameters.BackBufferHeight));
				m_sliceFX.Parameters["Iterations"].SetValue(iterations);

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