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

namespace DOFerVolumeRenderer
{
	public enum VolumeLoadingOptions
	{
		None,
		FlipX,
		FlipY,
		FlipZ,
	}



	public class Volume
	{
        private static HalfVector4[] m_Scalars;
        private static Vector3[] m_Gradients;

		public static Texture3D CreateUniformSampleData(Game game)
		{
			const int VOLUME_TEX_SIZE = 128;
			int size = VOLUME_TEX_SIZE * VOLUME_TEX_SIZE * VOLUME_TEX_SIZE;
			Color[] data = new Color[size];

			Texture3D tex3d = new Texture3D(game.GraphicsDevice, VOLUME_TEX_SIZE, VOLUME_TEX_SIZE, VOLUME_TEX_SIZE, true, SurfaceFormat.Color);

			for (int x = 0; x < VOLUME_TEX_SIZE; x++)
			{
				for (int y = 0; y < VOLUME_TEX_SIZE; y++)
				{
					for (int z = 0; z < VOLUME_TEX_SIZE; z++)
					{
						data[(x) + (y * VOLUME_TEX_SIZE) + (z * VOLUME_TEX_SIZE * VOLUME_TEX_SIZE)] = new Color(1.0f, 1.0f, 1.0f, 1.0f);
					}
				}
			}

			tex3d.SetData<Color>(data);
			return tex3d;
		}


		public static Texture3D CreateCoolSampleData(Game game)
		{
			const int VOLUME_TEX_SIZE = 128;
			int size = VOLUME_TEX_SIZE * VOLUME_TEX_SIZE * VOLUME_TEX_SIZE ;
			Color[] data = new Color[size];

			Texture3D tex3d = new Texture3D(game.GraphicsDevice, VOLUME_TEX_SIZE, VOLUME_TEX_SIZE, VOLUME_TEX_SIZE, true, SurfaceFormat.Color);

			for (int x = 0; x < VOLUME_TEX_SIZE; x++)
			{
				for (int y = 0; y < VOLUME_TEX_SIZE; y++)
				{
					for (int z = 0; z < VOLUME_TEX_SIZE; z++)
					{
						data[(x) + (y * VOLUME_TEX_SIZE) + (z * VOLUME_TEX_SIZE * VOLUME_TEX_SIZE)] = new Color(z % 250, y % 250, 250, 230);

						Vector3 p = new Vector3(x, y, z) - new Vector3(VOLUME_TEX_SIZE - 20, VOLUME_TEX_SIZE - 30, VOLUME_TEX_SIZE - 30);
						bool test = (p.Length() < 42);
						if (test)
							data[(x) + (y * VOLUME_TEX_SIZE) + (z * VOLUME_TEX_SIZE * VOLUME_TEX_SIZE)].A = 0;

						p = new Vector3(x, y, z) - new Vector3(VOLUME_TEX_SIZE / 2, VOLUME_TEX_SIZE / 2, VOLUME_TEX_SIZE / 2);
						test = (p.Length() < 24);
						if (test)
							data[(x) + (y * VOLUME_TEX_SIZE) + (z * VOLUME_TEX_SIZE * VOLUME_TEX_SIZE)].A = 0;


						if (x > 20 && x < 40 && y > 0 && y < VOLUME_TEX_SIZE && z > 10 && z < 50)
						{
							data[(x) + (y * VOLUME_TEX_SIZE) + (z * VOLUME_TEX_SIZE * VOLUME_TEX_SIZE)] = new Color(100, 250, y % 100, 250);
						}

						if (x > 50 && x < 70 && y > 0 && y < VOLUME_TEX_SIZE && z > 10 && z < 50)
						{

							data[(x) + (y * VOLUME_TEX_SIZE) + (z * VOLUME_TEX_SIZE * VOLUME_TEX_SIZE)] = new Color(250, 250, y % 100, 250);
						}

						if (x > 80 && x < 100 && y > 0 && y < VOLUME_TEX_SIZE && z > 10 && z < 50)
						{
							data[(x) + (y * VOLUME_TEX_SIZE) + (z * VOLUME_TEX_SIZE * VOLUME_TEX_SIZE)] = new Color(250, 70, y % 100, 250);
						}

						p = new Vector3(x, y, z) - new Vector3(24, 24, 24);
						test = (p.Length() < 40);
						if (test)
							data[(x) + (y * VOLUME_TEX_SIZE) + (z * VOLUME_TEX_SIZE * VOLUME_TEX_SIZE)].A = 0;
					}
				}
			}

			tex3d.SetData<Color>(data);
			return tex3d;
		}

		/// <summary>
		/// Loads a 16-bit RAW file.
		/// </summary>
		/// <param name="file"></param>
		private static Texture3D loadRAWFile16(Game game, FileStream file, int width, int height, int depth, VolumeLoadingOptions options)
		{
			BinaryReader reader = new BinaryReader(file);
			if (null == reader)
				return null;

			ushort[] buffer = new ushort[width * height * depth];

			for (int i = 0; i < buffer.Length; i++)
				buffer[i] = reader.ReadUInt16();

			reader.Close();

			Texture3D tex3d = new Texture3D(game.GraphicsDevice, width, height, depth, false, SurfaceFormat.HalfVector4);

			//scale the scalar values to [0, 1]
			//HalfVector4[] scalars = new HalfVector4[buffer.Length];
            m_Scalars = new HalfVector4[buffer.Length];
			switch (options)
			{
				case VolumeLoadingOptions.FlipX:
					for (int z = 0; z < depth; z++)
					{
						for (int y = 0; y < height; y++)
						{
							for (int x = 0; x < width; x++)
							{
                                m_Scalars[width * height * z + height * y + x] = new HalfVector4(0, 0, 0, (float)buffer[width * height * z + height * y + (width - 1 - x)] / ushort.MaxValue);
							}
						}
					}
					break;
				case VolumeLoadingOptions.FlipY:
					for (int z = 0; z < depth; z++)
					{
						for (int y = 0; y < height; y++)
						{
							for (int x = 0; x < width; x++)
							{
                                m_Scalars[width * height * z + height * y + x] = new HalfVector4(0, 0, 0, (float)buffer[width * height * z + height * (height - 1 - y) + x] / ushort.MaxValue);
							}
						}
					}
					break;
				case VolumeLoadingOptions.FlipZ:
					for (int z = 0; z < depth; z++)
					{
						for (int xy = 0; xy < width * height; xy++)
						{
                            m_Scalars[width * height * z + xy] = new HalfVector4(0, 0, 0, (float)buffer[width * height * (depth - 1 - z) + xy] / ushort.MaxValue);
						}
					}
					break;
				default:
					for (int i = 0; i < buffer.Length; i++)
					{
                        m_Scalars[i] = new HalfVector4(0, 0, 0, (float)buffer[i] / ushort.MaxValue);
					}
					break;
			}

            tex3d.SetData<HalfVector4>(m_Scalars);
			return tex3d;
		}


		/// <summary>
		/// Loads a 8-bit RAW file.
		/// </summary>
		/// <param name="file"></param>
		private static Texture3D loadRAWFile8(Game game, FileStream file, int width, int height, int depth, VolumeLoadingOptions options)
		{
			BinaryReader reader = new BinaryReader(file);
			if (null == reader)
				return null;

			byte[] buffer = new byte[width * height * depth];

			for (int i = 0; i < buffer.Length; i++)
				buffer[i] = reader.ReadByte();

			reader.Close();

			//Texture3D tex3d = new Texture3D(game.GraphicsDevice, width, height, depth, false, SurfaceFormat.Alpha8);
            Texture3D tex3d = new Texture3D(game.GraphicsDevice, width, height, depth, false, SurfaceFormat.HalfVector4);

			//scale the scalar values to [0, 1]
			//Alpha8[] scalars = new Alpha8[buffer.Length];
            m_Scalars = new HalfVector4[buffer.Length];
			switch (options)
			{
				case VolumeLoadingOptions.FlipX:
					for (int z = 0; z < depth; z++)
					{
						for (int y = 0; y < height; y++)
						{
							for (int x = 0; x < width; x++)
							{
								//scalars[width * height * z + height * y + x] = new Alpha8((float)buffer[width * height * z + height * y + (width - 1 - x)] / byte.MaxValue);
                                m_Scalars[width * height * z + height * y + x] = new HalfVector4(0, 0, 0, (float)buffer[width * height * z + height * y + (width - 1 - x)] / byte.MaxValue);
							}
						}
					}
					break;
				case VolumeLoadingOptions.FlipY:
					for (int z = 0; z < depth; z++)
					{
						for (int y = 0; y < height; y++)
						{
							for (int x = 0; x < width; x++)
							{
								//scalars[width * height * z + height * y + x] = new Alpha8((float)buffer[width * height * z + height * (height - 1 - y) + x] / byte.MaxValue);
                                m_Scalars[width * height * z + height * y + x] = new HalfVector4(0, 0, 0, (float)buffer[width * height * z + height * (height - 1 - y) + x] / byte.MaxValue);
							}
						}
					}
					break;
				case VolumeLoadingOptions.FlipZ:
					for (int z = 0; z < depth; z++)
					{
						for (int xy = 0; xy < width * height; xy++)
						{
							//scalars[width * height * z + xy] = new Alpha8((float)buffer[width * height * (depth - 1 - z) + xy] / byte.MaxValue);
                            m_Scalars[width * height * z + xy] = new HalfVector4(0, 0, 0, (float)buffer[width * height * (depth - 1 - z) + xy] / byte.MaxValue);
						}
					}
					break;
				default:
					for (int i = 0; i < buffer.Length; i++)
					{
						//scalars[i] = new Alpha8((float)buffer[i] / byte.MaxValue);
                        m_Scalars[i] = new HalfVector4(0, 0, 0, (float)buffer[i] / byte.MaxValue);
					}
					break;
			}

			//tex3d.SetData<Alpha8>(scalars);
            tex3d.SetData<HalfVector4>(m_Scalars);
			return tex3d;
		}

        public static Texture3D LoadFromFile(Game game, string filepath, int width, int height, int depth, VolumeLoadingOptions options)
        {
            return LoadFromFile(game, filepath, width, height, depth, options, false);
        }

		/// <summary>
		/// Loads a RAW file into a Texture3D
		/// </summary>
		/// <param name="filepath"></param>
		/// <param name="width"></param>
		/// <param name="height"></param>
		/// <param name="depth"></param>
		/// <returns>Texture3D containing teh volume data, or null if failed</returns>
		public static Texture3D LoadFromFile(Game game, string filepath, int width, int height, int depth, VolumeLoadingOptions options, bool useGradients)
		{
			FileStream file = new FileStream(filepath, FileMode.Open);
			if (null == file)
				return null;

			long length = file.Length;

			Texture3D result = null;
			if (length > width * height * depth)
			{
				result = loadRAWFile16(game, file, width, height, depth, options);
			}
			else
			{
				result = loadRAWFile8(game, file, width, height, depth, options);
			}

            if (useGradients)
            {
                string gradEnd = filepath.Substring(filepath.LastIndexOf('.'));
                string gradFile = filepath.Replace(gradEnd, ".grad");
                if (File.Exists(gradFile))
                {
                    result = loadGradientsFromFile(game, gradFile, width, height, depth);
                    file.Close();
                    return result;
                }

                m_Gradients = new Vector3[result.Width * result.Height * result.Depth];
                generateGradients(1, result.Depth, result.Height, result.Width);
                filterNxNxN(3, result.Depth, result.Height, result.Width);
                //Combine data
                HalfVector4[] gradients = new HalfVector4[m_Gradients.Length];
                for (int i = 0; i < m_Gradients.Length; i++)
                {
                    gradients[i] = new HalfVector4(m_Gradients[i].X, m_Gradients[i].Y, m_Gradients[i].Z, m_Scalars[i].ToVector4().W);
                }
                result.SetData<HalfVector4>(gradients);

                //save gradients in binary format
                //TODO: we could cut down file sizes by saving 16-bit floats (HalfVector) instead of 32-bit floats
                string extension = filepath.Substring(filepath.LastIndexOf('.'));
                FileStream fileS = new FileStream(filepath.Replace(extension, ".grad"), FileMode.OpenOrCreate);
                BinaryWriter writer = new BinaryWriter(fileS);

                for (int i = 0; i < m_Gradients.Length; i++)
                {
                    writer.Write(m_Gradients[i].X);
                    writer.Write(m_Gradients[i].Y);
                    writer.Write(m_Gradients[i].Z);
                }

                writer.Close();

            }



			file.Close();
			return result;
		}




        // *************** Gradients ********************

        /// <summary>
        /// Loads gradients from a file
        /// </summary>
        private static Texture3D loadGradientsFromFile(Game game, string gradFile, int width, int height, int depth)
        {
            FileStream file = new FileStream(gradFile, FileMode.Open);
            BinaryReader reader = new BinaryReader(file);

            HalfVector4[] gradients = new HalfVector4[width * height * depth];

            Vector4 temp = Vector4.Zero;
            //Vector3 temp = Vector3.Zero;
            char[] sep = { ' ' };
            int index = 0;
            for (int i = 0; i < gradients.Length; i++)
            {
                temp.X = reader.ReadSingle();
                temp.Y = reader.ReadSingle();
                temp.Z = reader.ReadSingle();
                temp.W = m_Scalars[index].ToVector4().W;

                gradients[index] = new HalfVector4(temp.X, temp.Y, temp.Z, temp.W);
                index++;
            }

            reader.Close();
            file.Close();
            Texture3D tex3d = new Texture3D(game.GraphicsDevice, width, height, depth, false, SurfaceFormat.HalfVector4);

            tex3d.SetData<HalfVector4>(gradients);
            return tex3d;
        }
        /// <summary>
        /// Generates gradients using a central differences scheme.
        /// </summary>
        /// <param name="sampleSize">The size/radius of the sample to take.</param>
        private static void generateGradients(int sampleRange, int depth, int height, int width)
        {
            int n = sampleRange;
            Vector3 normal = Vector3.Zero;
            Vector3 s1, s2;

            int index = 0;
            for (int z = 0; z < depth; z++)
            {
                for (int y = 0; y < height; y++)
                {
                    for (int x = 0; x < width; x++)
                    {
                        s1.X = sampleVolume(x - n, y, z, depth, height, width);
                        s2.X = sampleVolume(x + n, y, z, depth, height, width);
                        s1.Y = sampleVolume(x, y - n, z, depth, height, width);
                        s2.Y = sampleVolume(x, y + n, z, depth, height, width);
                        s1.Z = sampleVolume(x, y, z - n, depth, height, width);
                        s2.Z = sampleVolume(x, y, z + n, depth, height, width);

                        m_Gradients[index++] = Vector3.Normalize(s2 - s1);
                        if (float.IsNaN(m_Gradients[index - 1].X))
                            m_Gradients[index - 1] = Vector3.Zero;
                    }
                }
            }
        }

        /// <summary>
        /// Applies an NxNxN filter to the gradients. 
        /// Should be an odd number of samples. 3 is a Good Value.
        /// </summary>
        /// <param name="n"></param>
        private static void filterNxNxN(int n, int depth, int height, int width)
        {
            int index = 0;
            for (int z = 0; z < depth; z++)
            {
                for (int y = 0; y < height; y++)
                {
                    for (int x = 0; x < width; x++)
                    {
                        m_Gradients[index++] = sampleNxNxN(x, y, z, n, depth, height, width);
                    }
                }
            }
        }

        /// <summary>
        /// Samples the sub-volume graident volume and returns the average.
        /// Should be an odd number of samples.
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="z"></param>
        /// <param name="n"></param>
        /// <returns></returns>
        private static Vector3 sampleNxNxN(int x, int y, int z, int n, int depth, int height, int width)
        {
            n = (n - 1) / 2;

            Vector3 average = Vector3.Zero;
            int num = 0;

            for (int k = z - n; k <= z + n; k++)
            {
                for (int j = y - n; j <= y + n; j++)
                {
                    for (int i = x - n; i <= x + n; i++)
                    {
                        if (isInBounds(i, j, k, depth, height, width))
                        {
                            average += sampleGradients(i, j, k, depth, height, width);
                            num++;
                        }
                    }
                }
            }

            average /= (float)num;
            if (average.X != 0.0f && average.Y != 0.0f && average.Z != 0.0f)
                average.Normalize();

            return average;
        }

        /// <summary>
        /// Samples the scalar volume
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="z"></param>
        /// <returns></returns>
        private static float sampleVolume(int x, int y, int z, int depth, int height, int width)
        {
            x = (int)MathHelper.Clamp(x, 0, width - 1);
            y = (int)MathHelper.Clamp(y, 0, height - 1);
            z = (int)MathHelper.Clamp(z, 0, depth - 1);

            return (float)m_Scalars[x + (y * width) + (z * width * height)].ToVector4().W;
        }

        /// <summary>
        /// Samples the gradient volume
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="z"></param>
        /// <returns></returns>
        private static Vector3 sampleGradients(int x, int y, int z, int depth, int height, int width)
        {
            return m_Gradients[x + (y * width) + (z * width * height)];
        }

        /// <summary>
        /// Checks whether the input is in the bounds of the volume data array
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="z"></param>
        /// <returns></returns>
        private static bool isInBounds(int x, int y, int z, int depth, int height, int width)
        {
            return ((x >= 0 && x < width) &&
                    (y >= 0 && y < height) &&
                    (z >= 0 && z < depth));
        }
        



	}
}
