using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace Matrix_App
{
    partial class ColorWheel
    {
        /// <summary> 
        /// Erforderliche Designervariable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        private static readonly int WHEEL_PIXEL_RESOLUTION = 256;
        private static readonly int WHEEL_BORDER_SUBDIVISIONS = 6;
        private static readonly float RADIUS = WHEEL_PIXEL_RESOLUTION / (float) WHEEL_BORDER_SUBDIVISIONS * 4 * 0.5f;

        private Bitmap wheel;

        /// transformed triangle vertices in screen space
        private PointF[] transformed = new PointF[3];
        /// triangle verticies in object "local" space
        private PointF[] vertices = {
            new PointF(RADIUS * fcos(0),   RADIUS * fsin(0)  ),
            new PointF(RADIUS * fcos(120), RADIUS * fsin(120)),
            new PointF(RADIUS * fcos(240), RADIUS * fsin(240))
        };

        /// gradient between the fully saturated and brightned tone color and white (desaturated)
        private LinearGradientBrush whiteGradient;
        /// gradient between black and transparent, darkens the corner in which value = 0
        private LinearGradientBrush colorGradient;
        /// gradient between transparent and gray
        private LinearGradientBrush grayGradient;

        // stored HSV values
        private float angle = 180;
        private float saturation = 1f;
        private float value = 1f;

        // current cursor position
        private Point cursor = new Point();
        private Point origCursor = new Point();

        public EventHandler handler;

        private int sRGB = 0;

        private Boolean entered = false;
        private Boolean release = true;

        /// <summary>
        /// Slider types:
        /// <ul>
        /// <li>TRIANGLE: for the center triangle</li>
        /// <li>WHEEL: for the outside "wheel" controlling the tone</li>
        /// </ul>
        /// </summary>
        private enum Slider
        {
            TRIANGLE,
            WHEEL
        }

        private Slider selection = Slider.TRIANGLE;
        private System.Drawing.Drawing2D.Matrix transform;

        private float ctheta;

        #region Math-utils
        /// convert degrees to radians
        /// by mapping [0, 360] to [0, 2pi]
        private static float ToRadians(float degree)
        {
            // about equal to pi / 180
            const float PI_OVER_180 = 0.0174532925199f;

            return degree * PI_OVER_180;
        }

        /// convert radians to degrees
        /// by mapping [0, 2pi] to [0, 360]
        private static float ToDegree(float radians)
        {
            // about equal to 180 / pi
            const float HUNDERETEIGTHY_OVER_PI = 57.2957795131f;

            return radians * HUNDERETEIGTHY_OVER_PI;
        }

        /// Calculates the cosine of degrees as a float
        private static float fcos(float degree)
        {
            return MathF.Cos(ToRadians(degree));
        }

        /// Calculates the sine of degrees as a float
        private static float fsin(float degree)
        {
            return MathF.Sin(ToRadians(degree));
        }

        /// <summary>
        /// Dotproduct of a and b
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        private float dot(in PointF a, in PointF b)
        {
            return a.X * b.X + a.Y + b.Y;
        }

        /// Calculates the hypotenuse of a and b
        /// as sqrt(a² + b²)
        private static float hypot(float a, float b)
        {
            return MathF.Sqrt(a * a + b * b);
        }

        /// Divides the floor of the sum of a and b by 2
        private static int floorMiddle(float a, float b)
        {
            return (int)(a + b) >> 1;
        }

        /// <summary>
        /// Computes the traingle area of the pointes a,b,(x | y)
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        private float triangleArea(PointF a, PointF b, float x, float y)
        {
            return MathF.Abs((a.X - x) * (b.Y - y)
                           - (b.X - x) * (a.Y - y));
        }

        /// <summary>
        /// Tests if the point (x | y) lies inside of the transformed triangle
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <returns></returns>
        private bool triangleIntersect(double x, double y)
        {
            int originalArea = (int)triangleArea(transformed[1], transformed[2], transformed[0].X, transformed[0].Y);

            int area1 = (int)triangleArea(transformed[0], transformed[1], (float)x, (float)y);

            int area2 = (int)triangleArea(transformed[1], transformed[2], (float)x, (float)y);

            int area3 = (int)triangleArea(transformed[2], transformed[0], (float)x, (float)y);

            return Math.Abs(area1 + area2 + area3 - originalArea) < 3;
        }

        /// <summary>
        /// Convert cartesian coordinates (point p) to barycentric coordinates (u,v,w) relative to triangle with verticies a,b,c
        /// </summary>
        /// <param name="p"></param>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <param name="c"></param>
        /// <param name="u"></param>
        /// <param name="v"></param>
        /// <param name="w"></param>
        private void cartesianToBarycnetric(in PointF p, in PointF a, in PointF b, in PointF c, out float u, out float v, out float w)
        {
            var v0 = subtract(b, a);
            var v1 = subtract(c, a);
            var v2 = subtract(p, a);

            float denom = v0.X * v1.Y - v1.X * v0.Y;

            v = (v2.X * v1.Y - v1.X * v2.Y) / denom;
            w = (v0.X * v2.Y - v2.X * v0.Y) / denom;
            u = 1.0f - v - w;
        }

        /// <summary>
        /// Clamp barycentric coordinates of toClamp to the triangle specified by a,b,c, so that for every barycenttric coordinates is:
        /// u + v + w == 0 and none of u,v,w is greater than 1 or less than zero.
        /// </summary>
        /// <param name="toClamp"></param>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <param name="c"></param>
        private void clampToBarycentricCoordinates(ref PointF toClamp, in PointF a, in PointF b, in PointF c)
        {
            cartesianToBarycnetric(toClamp, a, b, c, out float u, out float v, out float w);

            clampBarycentric(ref u, ref v, ref w);

            barycentricToCartesian(ref toClamp, a, b, c, u, v, w);
        }

        /// <summary>
        /// Convert barycentric coordinates u,v,w relative to triangle with verticies a,b,c to cartesian coordinates.
        /// </summary>
        /// <param name="toClamp"></param>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <param name="c"></param>
        /// <param name="u"></param>
        /// <param name="v"></param>
        /// <param name="w"></param>
        private void barycentricToCartesian(ref PointF toClamp, PointF a, PointF b, PointF c, float u, float v, float w)
        {
            toClamp.X = a.X * u + b.X * v + c.X * w;
            toClamp.Y = a.Y * u + b.Y * v + c.Y * w;
        }

        /// <summary>
        /// Clamp x to the range [0, 1]
        /// </summary>
        /// <param name="x"></param>
        /// <returns></returns>
        private float saturate(float x)
        {
            return MathF.Max(MathF.Min(x, 1.0f), 0.0f);
        }

        /// <summary>
        /// Clamp the barycentric coordinates, so that
        /// u + v + w == 1
        /// without any of u,v,w being greater than 1 or less than zero.
        /// </summary>
        /// <param name="u"></param>
        /// <param name="v"></param>
        /// <param name="w"></param>
        private void clampBarycentric(ref float u, ref float v, ref float w)
        {
            u = saturate(u);
            v = saturate(v);
            w = 1 - u - v;

            u = saturate(u);
            w = saturate(w);
            v = 1 - u - w;

            v = saturate(v);
            w = saturate(w);
            u = 1 - v - w;
        }

        /// <summary>
        /// Linearlly interpolate between x and y by factor k
        /// If k == 0 then 100% of x and 0% of y
        /// If k == 1 then 0% of x and 100% of y
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="k"></param>
        /// <returns></returns>
        private float lerp(float x, float y, float k)
        {
            return x * (1 - k) + y * k;
        }

        /// <summary>
        /// Normalize point f, so that its length is 1
        /// </summary>
        /// <param name="b"></param>
        private void normalize(ref PointF b)
        {
            float length = hypot(b.X, b.Y);

            b.X = b.X / length;
            b.Y = b.Y / length;
        }

        /// <summary>
        /// Subtract point a from b, so that the result is (x0 - x1 | y0 - y1)
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        private PointF subtract(in PointF a, in PointF b)
        {
            var result = new PointF();

            result.X = a.X - b.X;
            result.Y = a.Y - b.Y;

            return result;
        }

        /// <summary>
        /// Convert from HSV color space to sRGB. 
        /// s,v are expected to be in range [0, 1]. 
        /// h is expected to be in range [0, 1]. 
        /// Any other values results in undefiened behavior
        /// </summary>
        /// <param name="h"></param>
        /// <param name="s"></param>
        /// <param name="v"></param>
        /// <returns></returns>
        private int sRGBfromHSV(float h, float s, float v)
        {
            float c = v * s;
            float x = c * (1.0f - Math.Abs((h / 60.0f) % 2.0f - 1.0f));
            float m = v - c;

            float r = 0, g = 0, b = 0;

            if (h < 60) { r = c; g = x; b = 0; }
            else if (h < 120) { r = x; g = c; b = 0; }
            else if (h < 180) { r = 0; g = c; b = x; }
            else if (h < 240) { r = 0; g = x; b = c; }
            else if (h < 300) { r = x; g = 0; b = c; }
            else { r = c; g = 0; b = x; }

            return 0xFF << 24 |
                (int)((r + m) * 255.0f) << 16 |
                (int)((g + m) * 255.0f) << 8 |
                (int)((b + m) * 255.0f);
        }

        /// <summary>
        /// Convert sRGB color space to HSV
        /// </summary>
        /// <param name="r"></param>
        /// <param name="g"></param>
        /// <param name="b"></param>
        private void HSVfromRGB(byte r, byte g, byte b)
        {
            const float INV_255 = 0.00392156862745f;

            float R = r * INV_255;
            float G = g * INV_255;
            float B = b * INV_255;

            byte Cmax = Math.Max(Math.Max(r, g), b);
            byte Cmin = Math.Min(Math.Min(r, g), b);

            float delta = (Cmax - Cmin) * INV_255;

            if (delta < 1e-2)
            {
                angle = 0;
            }
            else if (Cmax == r)
            {
                if (G < B)
                {
                    angle = 360 - MathF.Abs(60 * ((G - B) / delta));
                }
                else
                {
                    angle = 60 * ((G - B) / delta + 0f);
                }
            }
            else if (Cmax == g)
            {
                angle = 60 * ((B - R) / delta + 2f);
            }
            else if (Cmax == b)
            {
                angle = 60 * ((R - G) / delta + 4f);
            }

            saturation = delta / Cmax * 255.0f;
            if (Cmax == 0)
            {
                saturation = 0;
            }
            value = Cmax * INV_255;
        }

        #endregion

        protected override void OnCreateControl()
        {
            base.OnCreateControl();

            BackColorChanged += (a, b) => generateWheel();

            grayGradient = new LinearGradientBrush(
                // point A
                new Point((int)vertices[0].X,
                           (int)vertices[0].Y),
                // point B
                new Point(floorMiddle(vertices[2].X, vertices[1].X),
                           floorMiddle(vertices[2].Y, vertices[1].Y)),
                Color.Red,
                Color.Gray
                );
            whiteGradient = new LinearGradientBrush(
                // point A
                new Point((int)vertices[2].X,
                           (int)vertices[2].Y),
                // point B
                new Point(floorMiddle(vertices[0].X, vertices[1].X),
                           floorMiddle(vertices[0].Y, vertices[1].Y)),
                Color.White,
                Color.Transparent
                );
            colorGradient = new LinearGradientBrush(
                // point A
                new Point((int)vertices[1].X,
                           (int)vertices[1].Y),
                // point B
                new Point(floorMiddle(vertices[2].X, vertices[0].X),
                           floorMiddle(vertices[2].Y, vertices[0].Y)),
                Color.Black,
                Color.Transparent
                );

            this.SetStyle(ControlStyles.AllPaintingInWmPaint |
                            ControlStyles.UserPaint |
                            ControlStyles.DoubleBuffer,
                            true);

            this.MouseMove += new System.Windows.Forms.MouseEventHandler(OnMouseMove);
            this.MouseDown += new System.Windows.Forms.MouseEventHandler(OnMouseDown);
            this.MouseUp += new System.Windows.Forms.MouseEventHandler(OnMouseUp);

            generateWheel();

            setRGB(0, 0, 0);
        }
        
        private void OnMouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            release = true;
        }

        private void OnMouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            release = false;
            entered = true;
        }

        private void OnMouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            double u = e.X / (double)this.Width * 2.0 - 1.0;
            double v = e.Y / (double)this.Height * 2.0 - 1.0;

            double theta = Math.Atan2(v, u);
            double radius = Math.Sqrt(u * u + v * v);

            if (entered)
            {
                selection = Slider.WHEEL;
                if (radius < 0.6)
                {
                    selection = Slider.TRIANGLE;
                }

                entered = false;
            }
            else if (!release)
            {
                if (selection == Slider.TRIANGLE)
                {
                    if (transform != null)
                    {
                        PointF tempCursor = new PointF(e.X, e.Y);

                        clampToBarycentricCoordinates(ref tempCursor, transformed[0], transformed[1], transformed[2]);

                        if (true)
                        {
                            cursor.X = (int)tempCursor.X;
                            cursor.Y = (int)tempCursor.Y;

                            origCursor.X = cursor.X;
                            origCursor.Y = cursor.Y;

                            ctheta = angle;

                            cartesianToBarycnetric(cursor, transformed[0], transformed[1], transformed[2], out float bu, out float bv, out float bw);

                            value = MathF.Round(MathF.Max(MathF.Min(1.0f - bv, 1.0f), 0.0f) * 1e2f) / 1e2f;

                            saturation = MathF.Round(MathF.Max(MathF.Min(bu, 1.0f), 0.0f) * 1e2f) / 1e2f;
                        }
                    }
                }
                else
                {
                    angle = ToDegree((float)theta);
                    grayGradient = new LinearGradientBrush(
                        new Point((int)vertices[0].X,
                                   (int)vertices[0].Y),
                        new Point(floorMiddle(vertices[2].X, vertices[1].X),
                                   floorMiddle(vertices[2].Y, vertices[1].Y)
                                   ),
                        Color.FromArgb(sRGBfromHSV(180 + angle, 1, 1)),
                        Color.Gray
                        );

                    PointF[] c = { new PointF(origCursor.X, origCursor.Y) };

                    transform.Reset();
                    transform.RotateAt(angle - ctheta, new Point(this.Width >> 1, this.Height >> 1));
                    transform.TransformPoints(c);

                    cursor.X = (int)c[0].X;
                    cursor.Y = (int)c[0].Y;
                }

                sRGB = sRGBfromHSV(angle + 180, saturation, value);
                this.Refresh();
                handler.Invoke(this, null);
            }
        }

        /// <summary>
        /// Generate the color wheel "wheel" texture
        /// </summary>
        private void generateWheel()
        {
            wheel = new Bitmap(WHEEL_PIXEL_RESOLUTION, WHEEL_PIXEL_RESOLUTION);
            wheel.MakeTransparent();    // make transparent

            var g = Graphics.FromImage(wheel);

            // draw tone gradient by varying hue by the current angle of the pixel relative to the origin
            for (int x = wheel.Width; x-- > -1;)
            {
                float u = x / (float)wheel.Width * 2.0f - 1.0f;

                for (int y = wheel.Height; y-- > -1;)
                {
                    float v = y / (float)wheel.Height * 2.0f - 1.0f;

                    // cartesian to polar
                    float theta = ToDegree(MathF.Atan2(v, u) + MathF.PI);

                    int rgb = sRGBfromHSV(theta, 1.0f, 1.0f);
                    // draw pixel
                    g.FillRectangle(new SolidBrush(Color.FromArgb(rgb)), x, y, 1, 1);
                }
            }
            var step = wheel.Width / WHEEL_BORDER_SUBDIVISIONS;

            var strip = (int)Math.Sqrt(wheel.Width * wheel.Width + wheel.Height * wheel.Height) - wheel.Width;
            // cut out center circle
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
            g.FillEllipse(new SolidBrush(this.BackColor), new Rectangle(step, step, step * (WHEEL_BORDER_SUBDIVISIONS - 2), step * (WHEEL_BORDER_SUBDIVISIONS - 2)));
            g.DrawEllipse(new Pen(new SolidBrush(this.BackColor), strip), new Rectangle(-strip / 2, -strip / 2, wheel.Width + strip, wheel.Height + strip));

            g.Dispose();
        }

        /// <summary> 
        /// Verwendete Ressourcen bereinigen.
        /// </summary>
        /// <param name="disposing">True, wenn verwaltete Ressourcen gelöscht werden sollen; andernfalls False.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
        {
            base.OnPaint(e);
            var g = e.Graphics;

            // adjust for arbitrary component scale
            float scaleX = this.Width / (float) wheel.Width;
            float scaleY = this.Height / (float) wheel.Height;

            // transform local verticies to screen space
            g.ScaleTransform(scaleX, scaleY);
            g.DrawImage(wheel, new Point(0, 0));
            g.ResetTransform();

            g.RotateTransform(angle);

            float step = (wheel.Width / (float) WHEEL_BORDER_SUBDIVISIONS * (WHEEL_BORDER_SUBDIVISIONS - 3));
            float off = WHEEL_PIXEL_RESOLUTION / (float)(WHEEL_BORDER_SUBDIVISIONS - 2) * scaleX;

            g.ScaleTransform(wheel.Width / (float) Width * 4 / 6, wheel.Width / (float) Width * 4 / 6);
            g.TranslateTransform(wheel.Width * scaleX * 0.5f, wheel.Width * scaleX * 0.5f, MatrixOrder.Append);

            transformed[0].X = vertices[0].X;
            transformed[0].Y = vertices[0].Y;
            transformed[1].X = vertices[1].X;
            transformed[1].Y = vertices[1].Y;
            transformed[2].X = vertices[2].X;
            transformed[2].Y = vertices[2].Y;
            g.Transform.TransformPoints(transformed);

            // draw triangle
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
            g.FillPolygon(grayGradient, vertices);
            g.FillPolygon(whiteGradient, vertices);
            g.FillPolygon(colorGradient, vertices);
          
            // paint hue line
            g.DrawLine(new Pen(new SolidBrush(Color.Black), 2.0f), vertices[0].X, vertices[0].Y, vertices[0].X + RADIUS / 2, vertices[0].Y);
           
            // paint bordered shadowed cursor
            g.ResetTransform();
            transform = g.Transform;
            g.FillEllipse(new SolidBrush(Color.FromArgb(-2013265920)), new Rectangle(cursor.X - 10, cursor.Y - 10, 22, 22));
            g.FillEllipse(new SolidBrush(Color.White), new Rectangle(cursor.X - 10, cursor.Y - 10, 20, 20));
            g.FillEllipse(new SolidBrush(Color.FromArgb(sRGB)), new Rectangle(cursor.X - 8, cursor.Y - 8, 16, 16));
        }
        
        #region Getter-Setter
        public int getRGB()
        {
            return sRGB;
        }
     
        public int getRed()
        {
            return sRGB >> 16 & 0xFF;
        }
        public int getGreen()
        {
            return sRGB >> 8 & 0xFF;
        }
        public int getBlue()
        {
            return sRGB & 0xFF;
        }
     
        public float getHue()
        {
            return angle;
        }
        public float getSaturation()
        {
            return saturation;
        }
        public float getValue()
        {
            return value;
        }

        public void setHue(float hue)
        {
            angle = hue;
            showHSV();
        }
        public void setSaturation(float s)
        {
            saturation = s;
            showHSV();
        }
        public void setValue(float v)
        {
            value = v;
            showHSV();
        }

        public void setRGB(byte red, byte green, byte blue)
        {
            HSVfromRGB(red, green, blue);

            showHSV();
        }
        #endregion

        /// <summary>
        /// Show the color wheel and recompute triangle gradient
        /// and calculate new cursor position
        /// </summary>
        private void showHSV()
        {
            // get sRGB color
            sRGB = sRGBfromHSV(angle, saturation, value);

            // recompute correct triangle vertex transformation
            // to screen space
            System.Drawing.Drawing2D.Matrix m = new System.Drawing.Drawing2D.Matrix();

            transformed[0].X = vertices[0].X;
            transformed[0].Y = vertices[0].Y;
            transformed[1].X = vertices[1].X;
            transformed[1].Y = vertices[1].Y;
            transformed[2].X = vertices[2].X;
            transformed[2].Y = vertices[2].Y;

            // adjust for custom component width
            float scaleX = this.Width / (float)wheel.Width;
            float scaleY = this.Height / (float)wheel.Height;

            m.Rotate(angle - 180);

            float step = (wheel.Width / (float)WHEEL_BORDER_SUBDIVISIONS * (WHEEL_BORDER_SUBDIVISIONS - 3));
            float off = WHEEL_PIXEL_RESOLUTION / (float)(WHEEL_BORDER_SUBDIVISIONS - 2) * scaleX;

            m.Scale(wheel.Width / (float)Width * 4 / 6, wheel.Width / (float)Width * 4 / 6);
            m.Translate(wheel.Width * scaleX * 0.5f, wheel.Width * scaleX * 0.5f, MatrixOrder.Append);

            transformed[0].X = vertices[0].X;
            transformed[0].Y = vertices[0].Y;
            transformed[1].X = vertices[1].X;
            transformed[1].Y = vertices[1].Y;
            transformed[2].X = vertices[2].X;
            transformed[2].Y = vertices[2].Y;
            m.TransformPoints(transformed);

            // linearly interpolate cursor location according to saturation
            origCursor.X = (int)lerp(transformed[2].X, transformed[0].X, saturation);
            origCursor.Y = (int)lerp(transformed[2].Y, transformed[0].Y, saturation);
            // linearly interpolate cursor location according to value
            origCursor.X = (int)lerp(transformed[1].X, origCursor.X, value);
            origCursor.Y = (int)lerp(transformed[1].Y, origCursor.Y, value);
            // apply cursor location
            cursor.X = origCursor.X;
            cursor.Y = origCursor.Y;

            // recompute new gradient for mixing fully saturated color with gray
            sRGB = sRGBfromHSV(angle, saturation, value);
            grayGradient = new LinearGradientBrush(new Point((int)vertices[0].X, (int)vertices[0].Y), new Point((int)(vertices[2].X + vertices[1].X) / 2, (int)(vertices[2].Y + vertices[1].Y) / 2), Color.FromArgb(sRGBfromHSV(angle, 1f, 1f)), Color.Gray);

            // adjust angle values
            angle -= 180;
            ctheta = angle;
            // repaint
            this.Refresh();
        }

        /// <summary> 
        /// Erforderliche Methode für die Designerunterstützung. 
        /// Der Inhalt der Methode darf nicht mit dem Code-Editor geändert werden.
        /// </summary>
        private void InitializeComponent()
        {
            components = new System.ComponentModel.Container();
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        }
    }
}
