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

namespace DOFerVolumeRenderer
{
	public class ControlPoint : ICloneable<ControlPoint>
	{
		public double _position = 0.0;

		public ControlPoint()
		{
		}

		public ControlPoint(double pos, Color color)
		{
			_position = pos;
			Color = color;
			IsFixed = false;
		}

		public ControlPoint(ControlPoint other)
		{
			_position = other._position;
			Color = other.Color;
		}

		public ControlPoint Clone()
		{
			return new ControlPoint(this);
		}

		/// <summary>
		/// Position of that control-point in percent, this value will always be between 0.0 and 1.0
		/// </summary>
		public double Position 
		{
			get 
			{ 
				return _position; 
			}
			set 
			{  
				if (!IsFixed)
					_position = Math.Max(0.0, Math.Min(1.0, value)); 
			}
		}

		public Color Color { get; set; }

		public bool IsFixed { get; set; }
	}



	public class TransferFunction
	{
		/// <summary>
		/// Can set a control point every 1.2% of the complete transfer-function length
		/// </summary>
		private const double CONTROLPOINTS_MIN_DIST = 0.012;
		private const double EPSILON = 0.00001;
		private Game _game = null;
		private Texture2D _texture = null;
		private int _resolutionX = 1;

		private List<ControlPoint> _controlPoints = new List<ControlPoint>();

		public TransferFunction(Game game, int resolutionX) 
			: this(game, resolutionX, new ControlPoint[] 
			{
				new ControlPoint(0.0, new Color(0f, 0f, 0f, 0f)),
				new ControlPoint(0.4, new Color(1f, 0f, 1f, 0f)),
				new ControlPoint(0.5, new Color(0f, 1f, 1f, 1f)),
				new ControlPoint(0.6, new Color(0f, 1f, 1f, 1f)),
				new ControlPoint(0.7, new Color(1f, 0f, 1f, 0f)),
				new ControlPoint(1.0, new Color(0f, 0f, 0f, 0f)),
			})
		{
		}

		public TransferFunction(Game game, int resolutionX, IEnumerable<ControlPoint> controlPoints)
		{
			_game = game;
			_resolutionX = resolutionX;

			// create control points. first and last are fixed.
			foreach (var cp in controlPoints)
			{
				AddControlPoint(cp);
			}

			// control points have been inserted in correct order => set the first and the last fixed
			if (Math.Abs(_controlPoints.First().Position) <= CONTROLPOINTS_MIN_DIST)
			{
				_controlPoints.First().Position = 0.0;
				_controlPoints.First().IsFixed = true;
			}
			else
			{
				var startCp = CreateControlPointAtPosition(0.0, 0f);
				startCp.IsFixed = true;
			}

			if (Math.Abs(1.0 - _controlPoints.Last().Position) <= CONTROLPOINTS_MIN_DIST)
			{
				_controlPoints.Last().Position = 1.0;
				_controlPoints.Last().IsFixed = true;
			}
			else
			{
				var endCp = CreateControlPointAtPosition(1.0, 0f);
				endCp.IsFixed = true;
			}
		}

		public Texture2D Texture
		{
			get { return _texture; }
		}

		public IEnumerable<ControlPoint> ControlPoints
		{
			get
			{
				foreach (var cp in _controlPoints)
				{
					yield return cp;
				}
			}
		}



		public void MoveControlPointToPosition(ControlPoint cp, double pos)
		{
			int index = _controlPoints.FindIndex(x => x == cp);
			if (-1 != index)
			{
				if (_controlPoints[index].IsFixed)
					return;

				if (pos < 0.0 && pos > 1.0)
					return;

				if (index >= 1)
				{
					if (pos < _controlPoints[index - 1].Position + CONTROLPOINTS_MIN_DIST)
						return;
				}

				if (index < _controlPoints.Count - 1)
				{
					if (pos > _controlPoints[index + 1].Position - CONTROLPOINTS_MIN_DIST)
						return;
				}

				// all check passed
				cp.Position = pos;
			}
		}


		public bool CanAddControlPointAtPosition(double pos)
		{
			int index;
			return CanAddControlPointAtPosition(pos, out index);
		}

		public bool CanAddControlPointAtPosition(double pos, out int index)
		{
			index = -1;

			if (pos < 0.0 || pos > 1.0)
				return false;

			// find position to insert and test whether allowed to insert
			double dist = 0.0;
			double prevDist = 0.0;
			for (int i = 0; i < _controlPoints.Count; i++)
			{
				dist = pos - _controlPoints[i].Position;
				if (dist < 0.0) // when this is true the first time => found position to insert
				{
					if (-dist < CONTROLPOINTS_MIN_DIST)
						return false;

					// prevDist is guaranteed to be > 0
					if (prevDist < CONTROLPOINTS_MIN_DIST)
						return false;

					// otherwise => ok to insert here!
					index = i;
					return true;
				}
				prevDist = dist;
			}

			// otherwise the point is to be inserted at the end of the list
			if (_controlPoints.Count > 0 && pos - _controlPoints.Last().Position < CONTROLPOINTS_MIN_DIST)
				return false;

			// everything ok, point can be appended
			index = _controlPoints.Count;
			return true;
		}

		public bool AddControlPoint(ControlPoint cp)
		{
			int indexToInsert;
			if (CanAddControlPointAtPosition(cp.Position, out indexToInsert))
			{
				_controlPoints.Insert(indexToInsert, cp);
				return true;
			}
			return false;
		}


		public bool GetControlPointAtPosition(double pos, out ControlPoint controlPoint)
		{
			for (int i = 0; i < _controlPoints.Count; i++)
			{
				if (Math.Abs(pos - _controlPoints[i].Position) < CONTROLPOINTS_MIN_DIST)
				{
					controlPoint = _controlPoints[i];
					return true;
				}
			}

			controlPoint = null;
			return false;
		}


		public void DeleteControlPoint(ControlPoint controlPoint)
		{
			if (!controlPoint.IsFixed)
				_controlPoints.Remove(controlPoint);
		}


		public ControlPoint CreateControlPointAtPosition(double pos, float alpha)
		{
			int index;
			if (CanAddControlPointAtPosition(pos, out index))
			{
				Debug.Assert(index > 0 && index <= _controlPoints.Count);
				ControlPoint newCp = _controlPoints[index - 1].Clone();
				newCp.IsFixed = false;
				newCp.Position = pos;
				newCp.Color = new Color(newCp.Color.R, newCp.Color.G, newCp.Color.B, alpha);
				_controlPoints.Insert(index, newCp);
				return newCp;
			}
			return null;
		}


		/// <summary>
		/// Calculates the interpolated color at the specified position
		/// </summary>
		/// <param name="pos">in percent - range 0.0 to 1.0</param>
		/// <returns></returns>
		public Color GetColorAtPosition(double pos)
		{
			for (int i = 1; i < _controlPoints.Count; i++)
			{
				double dist = pos - _controlPoints[i].Position;
				if (dist < 0.0)
				{
					// interpolate between previous and current control-point
					var prev_cp = _controlPoints[i - 1];
					var cur_cp = _controlPoints[i];
					var cur_prev_dist = cur_cp.Position - prev_cp.Position;
					var prev_offset = pos - prev_cp.Position;
					var weight = (float)(prev_offset / cur_prev_dist);
					return new Color(
						((float)prev_cp.Color.R / byte.MaxValue) * (1.0f - weight) + ((float)cur_cp.Color.R / byte.MaxValue) * weight,
						((float)prev_cp.Color.G / byte.MaxValue) * (1.0f - weight) + ((float)cur_cp.Color.G / byte.MaxValue) * weight,
						((float)prev_cp.Color.B / byte.MaxValue) * (1.0f - weight) + ((float)cur_cp.Color.B / byte.MaxValue) * weight,
						((float)prev_cp.Color.A / byte.MaxValue) * (1.0f - weight) + ((float)cur_cp.Color.A / byte.MaxValue) * weight);
				}
			}

			if (Math.Abs(pos - _controlPoints.First().Position) < EPSILON)
				return _controlPoints.First().Color;
			if (Math.Abs(pos - _controlPoints.Last().Position) < EPSILON)
				return _controlPoints.Last().Color;

			throw new Exception("WHAAAAAT?");
		}


		/// <summary>
		/// Generates the transfer-function in size resolutionX-by-1
		/// </summary>
		/// <param name="resoltuionX">pass some power-of-2 value, like 64, 128, or 256</param>
		/// <returns></returns>
		public Color[] Generate()
		{
			Color[] transfer = new Color[_resolutionX];

			for (int i = 0; i < _resolutionX; i++)
			{
				transfer[i] = GetColorAtPosition((double)i / _resolutionX);
			}

			return transfer;
		}


		/// <summary>
		/// Calls Generate to create the updated transfer-function, and uploads the new data to the tansfer-texture
		/// </summary>
		public void Update()
		{
			Color[] updatedData = Generate();

			if (null != _texture)
				_texture.Dispose();

			// create the texture
			_texture = new Texture2D(_game.GraphicsDevice, _resolutionX, 1, false, SurfaceFormat.Color);
			_texture.SetData<Color>(updatedData);
		}
	}
}
