using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

namespace CodeImp.DoomBuilder.ColorPicker 
{
	public sealed class ColorWheel : IDisposable 
	{

		// These resources should be disposed
		// of when you're done with them.
		private Graphics g;
		private Region colorRegion;
		private Region brightnessRegion;
		private Bitmap colorImage;

		public delegate void ColorChangedEventHandler(object sender, ColorChangedEventArgs e);
		public ColorChangedEventHandler ColorChanged;

		// Keep track of the current mouse state. 
		private enum MouseState 
		{
			MouseUp,
			ClickOnColor,
			DragInColor,
			ClickOnBrightness,
			DragInBrightness,
			ClickOutsideRegion,
			DragOutsideRegion,
		}
		private MouseState currentState = MouseState.MouseUp;

		// The code needs to convert back and forth between 
		// degrees and radians. There are 2*PI radians in a 
		// full circle, and 360 degrees. This constant allows
		// you to convert back and forth.
		private const float DEGREES_PER_RADIAN = 180.0f / (float)Math.PI;

		// COLOR_COUNT represents the number of distinct colors
		// used to create the circular gradient. Its value 
		// is somewhat arbitrary -- change this to 6, for 
		// example, to see what happens. 1536 (6 * 256) seems 
		// a good compromise -- it's enough to get a full 
		// range of colors, but it doesn't overwhelm the processor
		// attempting to generate the image. The color wheel
		// contains 6 sections, and each section displays 
		// 256 colors. Seems like a reasonable compromise.
		private const int COLOR_COUNT = 6 * 256;

		private Point centerPoint;
		private int radius;

		private Rectangle colorRectangle;
		private Rectangle brightnessRectangle;
		private int brightnessX;
		private float brightnessScaling;

		// selectedColor is the actual value selected by the user. fullColor is the same color, 
		// with its brightness set to 255.
		private Color selectedColor = Color.White;
		private Color fullColor;

		private ColorHandler.RGB RGB;
		private ColorHandler.HSV HSV;

		// Locations for the two "pointers" on the form.
		private Point colorPoint;
		private Point brightnessPoint;

		private int brightness;
		private int brightnessMin;
		private int brightnessMax;

		public Color Color { get { return selectedColor; } }

		public ColorWheel(Rectangle colorRectangle, Rectangle brightnessRectangle) 
		{

			// Caller must provide locations for color wheel
			// (colorRectangle), brightness "strip" (brightnessRectangle)
			// and location to display selected color (selectedColorRectangle).

			using(GraphicsPath path = new GraphicsPath()) 
			{
				// Store away locations for later use. 
				this.colorRectangle = colorRectangle;
				this.brightnessRectangle = brightnessRectangle;
				//this.selectedColorRectangle = selectedColorRectangle;

				// Calculate the center of the circle.
				// Start with the location, then offset
				// the point by the radius.
				// Use the smaller of the width and height of
				// the colorRectangle value.
				this.radius = Math.Min(colorRectangle.Width, colorRectangle.Height) / 2;
				this.centerPoint = colorRectangle.Location;
				this.centerPoint.Offset(radius, radius);

				// Start the pointer in the center.
				this.colorPoint = this.centerPoint;

				// Create a region corresponding to the color circle.
				// Code uses this later to determine if a specified
				// point is within the region, using the IsVisible 
				// method.
				path.AddEllipse(colorRectangle);
				colorRegion = new Region(path);

				// set { the range for the brightness selector.
				this.brightnessMin = this.brightnessRectangle.Top;
				this.brightnessMax = this.brightnessRectangle.Bottom;

				// Create a region corresponding to the
				// brightness rectangle, with a little extra 
				// "breathing room". 

				path.AddRectangle(new Rectangle(brightnessRectangle.Left, brightnessRectangle.Top - 10, brightnessRectangle.Width + 10, brightnessRectangle.Height + 20));
				// Create region corresponding to brightness
				// rectangle. Later code uses this to 
				// determine if a specified point is within
				// the region, using the IsVisible method.
				brightnessRegion = new Region(path);

				// Set the location for the brightness indicator "marker".
				// Also calculate the scaling factor, scaling the height
				// to be between 0 and 255. 
				brightnessX = brightnessRectangle.Left + brightnessRectangle.Width;
				brightnessScaling = 255.0f / (brightnessMax - brightnessMin);

				// Calculate the location of the brightness
				// pointer. Assume it's at the highest position.
				brightnessPoint = new Point(brightnessX, brightnessMax);

				// Create the bitmap that contains the circular gradient.
				CreateGradient();
			}
		}

		private void OnColorChanged(ColorHandler.RGB RGB, ColorHandler.HSV HSV) 
		{
			ColorChangedEventArgs e = new ColorChangedEventArgs(RGB, HSV);
			ColorChanged(this, e);
		}

		void IDisposable.Dispose() 
		{
			// Dispose of graphic resources
			if(colorImage != null) colorImage.Dispose();
			if(colorRegion != null) colorRegion.Dispose();
			if(brightnessRegion != null) brightnessRegion.Dispose();
			if(g != null) g.Dispose();
		}

		public void SetMouseUp() 
		{
			// Indicate that the user has
			// released the mouse.
			currentState = MouseState.MouseUp;
		}

		public void Draw(Graphics g, ColorHandler.RGB RGB) 
		{
			// Given RGB values, calculate HSV and then update the screen.
			this.g = g;
			this.HSV = ColorHandler.RGBtoHSV(RGB);
			CalcCoordsAndUpdate(this.HSV);
			UpdateDisplay();
		}

		public void Draw(Graphics g, Point mousePoint) 
		{
			// You've moved the mouse. 
			// Now update the screen to match.

			// Keep track of the previous color pointer point, 
			// so you can put the mouse there in case the 
			// user has clicked outside the circle.
			Point newColorPoint = colorPoint;
			Point newBrightnessPoint = brightnessPoint;

			// Store this away for later use.
			this.g = g;

			if(currentState == MouseState.MouseUp) 
			{
				if(!mousePoint.IsEmpty) 
				{
					if(colorRegion.IsVisible(mousePoint)) 
					{
						// Is the mouse point within the color circle?
						// If so, you just clicked on the color wheel.
						currentState = MouseState.ClickOnColor;
					} 
					else if(brightnessRegion.IsVisible(mousePoint)) 
					{
						// Is the mouse point within the brightness area?
						// You clicked on the brightness area.
						currentState = MouseState.ClickOnBrightness;
					} 
					else 
					{
						// Clicked outside the color and the brightness
						// regions. In that case, just put the 
						// pointers back where they were.
						currentState = MouseState.ClickOutsideRegion;
					}
				}
			}

			switch(currentState) 
			{
				case MouseState.ClickOnBrightness:
				case MouseState.DragInBrightness:
					// Calculate new color information
					// based on the brightness, which may have changed.
					Point newPoint = mousePoint;
					if(newPoint.Y < brightnessMin) 
					{
						newPoint.Y = brightnessMin;
					} 
					else if(newPoint.Y > brightnessMax) 
					{
						newPoint.Y = brightnessMax;
					}
					newBrightnessPoint = new Point(brightnessX, newPoint.Y);
					brightness = (int)((brightnessMax - newPoint.Y) * brightnessScaling);
					HSV.value = brightness;
					RGB = ColorHandler.HSVtoRGB(HSV);
					break;

				case MouseState.ClickOnColor:
				case MouseState.DragInColor:
					// Calculate new color information
					// based on selected color, which may have changed.
					newColorPoint = mousePoint;

					// Calculate x and y distance from the center,
					// and then calculate the angle corresponding to the
					// new location.
					Point delta = new Point(mousePoint.X - centerPoint.X, mousePoint.Y - centerPoint.Y);
					int degrees = CalcDegrees(delta);

					// Calculate distance from the center to the new point 
					// as a fraction of the radius. Use your old friend, 
					// the Pythagorean theorem, to calculate this value.
					float distance = (float)Math.Sqrt(delta.X * delta.X + delta.Y * delta.Y) / radius;

					if(currentState == MouseState.DragInColor) 
					{
						if(distance > 1) 
						{
							// Mouse is down, and outside the circle, but you 
							// were previously dragging in the color circle. 
							// What to do?
							// In that case, move the point to the edge of the 
							// circle at the correct angle.
							distance = 1;
							newColorPoint = GetPoint(degrees, radius, centerPoint);
						}
					}

					// Calculate the new HSV and RGB values.
					HSV.Hue = degrees * 255 / 360;
					HSV.Saturation = (int)(distance * 255);
					HSV.value = brightness;
					RGB = ColorHandler.HSVtoRGB(HSV);
					fullColor = ColorHandler.HSVtoColor(HSV.Hue, HSV.Saturation, 255);
					break;
			}
			selectedColor = ColorHandler.HSVtoColor(HSV);

			// Raise an event back to the parent form,
			// so the form can update any UI it's using 
			// to display selected color values.
			OnColorChanged(RGB, HSV);

			// On the way out, set the new state.
			switch(currentState) 
			{
				case MouseState.ClickOnBrightness:
					currentState = MouseState.DragInBrightness;
					break;
				case MouseState.ClickOnColor:
					currentState = MouseState.DragInColor;
					break;
				case MouseState.ClickOutsideRegion:
					currentState = MouseState.DragOutsideRegion;
					break;
			}

			// Store away the current points for next time.
			colorPoint = newColorPoint;
			brightnessPoint = newBrightnessPoint;

			// Draw the gradients and points. 
			UpdateDisplay();
		}

		private Point CalcBrightnessPoint(int brightness) 
		{
			// Take the value for brightness (0 to 255), scale to the 
			// scaling used in the brightness bar, then add the value 
			// to the bottom of the bar. return the correct point at which 
			// to display the brightness pointer.
			return new Point(brightnessX, (int)(brightnessMax - brightness / brightnessScaling));
		}

		private void UpdateDisplay() 
		{
			// Update the gradients, and place the pointers correctly based on colors and 
			// brightness.
			CreateGradient();

			// Draw the saved color wheel image.
			g.DrawImage(colorImage, colorRectangle);

			// Draw the "brightness" rectangle.
			DrawLinearGradient(fullColor);
			// Draw the two pointers.
			DrawColorPointer(colorPoint);
			DrawBrightnessPointer(brightnessPoint);
		}

		private void CalcCoordsAndUpdate(ColorHandler.HSV HSV) 
		{
			// Convert color to real-world coordinates and then calculate
			// the various points. HSV.Hue represents the degrees (0 to 360), 
			// HSV.Saturation represents the radius. 
			// This procedure doesn't draw anything--it simply 
			// updates class-level variables. The UpdateDisplay
			// procedure uses these values to update the screen.

			// Given the angle (HSV.Hue), and distance from 
			// the center (HSV.Saturation), and the center, 
			// calculate the point corresponding to 
			// the selected color, on the color wheel.
			colorPoint = GetPoint((float)HSV.Hue / 255 * 360,
				(float)HSV.Saturation / 255 * radius, centerPoint);

			// Given the brightness (HSV.value), calculate the 
			// point corresponding to the brightness indicator.
			brightnessPoint = CalcBrightnessPoint(HSV.value);

			// Store information about the selected color.
			brightness = HSV.value;
			selectedColor = ColorHandler.HSVtoColor(HSV);
			RGB = ColorHandler.HSVtoRGB(HSV);

			// The full color is the same as HSV, except that the 
			// brightness is set to full (255). This is the top-most
			// color in the brightness gradient.
			fullColor = ColorHandler.HSVtoColor(HSV.Hue, HSV.Saturation, 255);
		}

		private void DrawLinearGradient(Color TopColor) 
		{
			// Given the top color, draw a linear gradient
			// ranging from black to the top color. Use the 
			// brightness rectangle as the area to fill.
			using(LinearGradientBrush lgb = new LinearGradientBrush(brightnessRectangle, TopColor,
							 Color.Black, LinearGradientMode.Vertical)) 
			{
				g.FillRectangle(lgb, brightnessRectangle);
			}
		}

		private static int CalcDegrees(Point pt) 
		{
			int degrees;

			if(pt.X == 0) 
			{
				// The point is on the y-axis. Determine whether it's above or below the x-axis, and return the 
				// corresponding angle. Note that the orientation of the y-coordinate is backwards. That is, 
				// A positive Y value indicates a point BELOW the x-axis.
				degrees = pt.Y > 0 ? 270 : 90;
			} 
			else 
			{
				// This value needs to be multiplied by -1 because the y-coordinate
				// is opposite from the normal direction here.
				// That is, a y-coordinate that's "higher" on the form has a lower y-value, in this coordinate
				// system. So everything's off by a factor of -1 when performing the ratio calculations.
				degrees = (int)(-Math.Atan((float)pt.Y / pt.X) * DEGREES_PER_RADIAN);

				// If the x-coordinate of the selected point is to the left of the center of the circle, you 
				// need to add 180 degrees to the angle. ArcTan only gives you a value on the right-hand side 
				// of the circle.
				if(pt.X < 0) degrees += 180;

				// Ensure that the return value is between 0 and 360.
				degrees = (degrees + 360) % 360;
			}
			return degrees;
		}

		private void CreateGradient() 
		{
			// Create a new PathGradientBrush, supplying an array of points created by calling
			// the GetPoints method.
			using(PathGradientBrush pgb = new PathGradientBrush(GetPoints(radius, new Point(radius, radius)))) 
			{
				// Set the various properties. Note the SurroundColors property, which contains an array of points, 
				// in a one-to-one relationship with the points that created the gradient.
				pgb.CenterColor = Color.White;
				pgb.CenterPoint = new PointF(radius, radius);
				pgb.SurroundColors = GetColors();

				// Create a new bitmap containing the color wheel gradient, so the 
				// code only needs to do all this work once. Later code uses the bitmap
				// rather than recreating the gradient.
				colorImage = new Bitmap(colorRectangle.Width, colorRectangle.Height, PixelFormat.Format32bppArgb);

				using(Graphics newGraphics = Graphics.FromImage(colorImage)) 
				{
					newGraphics.FillEllipse(pgb, 0, 0, colorRectangle.Width, colorRectangle.Height);
				}
			}
		}

		private Color[] GetColors() 
		{
			// Create an array of COLOR_COUNT colors, looping through all the hues between 0 and 255, broken
			// into COLOR_COUNT intervals. HSV is particularly well-suited for this, because the only value 
			// that changes as you create colors is the Hue.
			Color[] Colors = new Color[COLOR_COUNT];

			for(int i = 0; i < COLOR_COUNT; i++)
				Colors[i] = ColorHandler.HSVtoColor((int)((float)(i * 255) / COLOR_COUNT), 255, HSV.value);
			return Colors;
		}

		private static Point[] GetPoints(float radius, Point centerPoint) 
		{
			// Generate the array of points that describe the locations of the COLOR_COUNT colors to be 
			// displayed on the color wheel.
			Point[] Points = new Point[COLOR_COUNT];

			for(int i = 0; i < COLOR_COUNT; i++)
				Points[i] = GetPoint((float)(i * 360) / COLOR_COUNT, radius, centerPoint);
			return Points;
		}

		private static Point GetPoint(float degrees, float radius, Point centerPoint) 
		{
			// Given the center of a circle and its radius, along
			// with the angle corresponding to the point, find the coordinates. 
			// In other words, conver  t from polar to rectangular coordinates.
			float radians = degrees / DEGREES_PER_RADIAN;

			return new Point((int)(centerPoint.X + Math.Floor(radius * Math.Cos(radians))),
				(int)(centerPoint.Y - Math.Floor(radius * Math.Sin(radians))));
		}

		private void DrawColorPointer(Point pt) 
		{
			// Given a point, draw the color selector. The constant SIZE represents half
			// the width -- the square will be twice this value in width and height.
			const int SIZE = 3;
			g.DrawRectangle(Pens.Black, pt.X - SIZE, pt.Y - SIZE, SIZE * 2, SIZE * 2);
		}

		private void DrawBrightnessPointer(Point pt) 
		{
			// Draw a triangle for the brightness indicator that "points" at the provided point.
			const int HEIGHT = 10;
			const int WIDTH = 7;

			Point[] Points = new Point[3];
			Points[0] = pt;
			Points[1] = new Point(pt.X + WIDTH, pt.Y + HEIGHT / 2);
			Points[2] = new Point(pt.X + WIDTH, pt.Y - HEIGHT / 2);
			g.FillPolygon(Brushes.Black, Points);
		}
	}
}
