// Color conversion portions of this source file are adapted from the Windows Community Toolkit project.
// (https://github.com/CommunityToolkit/WindowsCommunityToolkit)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.

using System;
using System.Globalization;
using System.Text;
using Avalonia.Utilities;

namespace Avalonia.Media
{
    /// <summary>
    /// Defines a color using the hue/saturation/lightness (HSL) model.
    /// This uses a cylindrical-coordinate representation of a color.
    /// </summary>
#if !BUILDTASK
    public
#endif
    readonly struct HslColor : IEquatable<HslColor>
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="HslColor"/> struct.
        /// </summary>
        /// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
        /// <param name="hue">The Hue component in the range from 0..360.
        /// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
        /// <param name="saturation">The Saturation component in the range from 0..1.</param>
        /// <param name="lightness">The Lightness component in the range from 0..1.</param>
        public HslColor(
            double alpha,
            double hue,
            double saturation,
            double lightness)
        {
            A = MathUtilities.Clamp(alpha,      0.0, 1.0);
            H = MathUtilities.Clamp(hue,        0.0, 360.0);
            S = MathUtilities.Clamp(saturation, 0.0, 1.0);
            L = MathUtilities.Clamp(lightness,  0.0, 1.0);

            // The maximum value of Hue is technically 360 minus epsilon (just below 360).
            // This is because, in a color circle, 360 degrees is equivalent to 0 degrees.
            // However, that is too tricky to work with in code and isn't as intuitive.
            // Therefore, since 360 == 0, just wrap 360 if needed back to 0.
            H = (H == 360.0 ? 0 : H);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="HslColor"/> struct.
        /// </summary>
        /// <remarks>
        /// This constructor exists only for internal use where performance is critical.
        /// Whether or not the component values are in the correct ranges must be known.
        /// </remarks>
        /// <param name="alpha">The Alpha (transparency) component in the range from 0..1.</param>
        /// <param name="hue">The Hue component in the range from 0..360.
        /// Note that 360 is equivalent to 0 and will be adjusted automatically.</param>
        /// <param name="saturation">The Saturation component in the range from 0..1.</param>
        /// <param name="lightness">The Lightness component in the range from 0..1.</param>
        /// <param name="clampValues">Whether to clamp component values to their required ranges.</param>
        internal HslColor(
            double alpha,
            double hue,
            double saturation,
            double lightness,
            bool clampValues)
        {
            if (clampValues)
            {
                A = MathUtilities.Clamp(alpha,      0.0, 1.0);
                H = MathUtilities.Clamp(hue,        0.0, 360.0);
                S = MathUtilities.Clamp(saturation, 0.0, 1.0);
                L = MathUtilities.Clamp(lightness,  0.0, 1.0);

                // See comments in constructor above
                H = (H == 360.0 ? 0 : H);
            }
            else
            {
                A = alpha;
                H = hue;
                S = saturation;
                L = lightness;
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="HslColor"/> struct.
        /// </summary>
        /// <param name="color">The RGB color to convert to HSL.</param>
        public HslColor(Color color)
        {
            var hsl = Color.ToHsl(color);

            A = hsl.A;
            H = hsl.H;
            S = hsl.S;
            L = hsl.L;
        }

        /// <summary>
        /// Gets the Alpha (transparency) component in the range from 0..1 (percentage).
        /// </summary>
        /// <remarks>
        /// <list type="bullet">
        ///   <item>0 is fully transparent.</item>
        ///   <item>1 is fully opaque.</item>
        /// </list>
        /// </remarks>
        public double A { get; }

        /// <summary>
        /// Gets the Hue component in the range from 0..360 (degrees).
        /// This is the color's location, in degrees, on a color wheel/circle from 0 to 360.
        /// Note that 360 is equivalent to 0 and will be adjusted automatically.
        /// </summary>
        /// <remarks>
        /// <list type="bullet">
        ///   <item>0/360 degrees is Red.</item>
        ///   <item>60 degrees is Yellow.</item>
        ///   <item>120 degrees is Green.</item>
        ///   <item>180 degrees is Cyan.</item>
        ///   <item>240 degrees is Blue.</item>
        ///   <item>300 degrees is Magenta.</item>
        /// </list>
        /// </remarks>
        public double H { get; }

        /// <summary>
        /// Gets the Saturation component in the range from 0..1 (percentage).
        /// </summary>
        /// <remarks>
        /// <list type="bullet">
        ///   <item>0 is a shade of gray (no color).</item>
        ///   <item>1 is the full color.</item>
        /// </list>
        /// </remarks>
        public double S { get; }

        /// <summary>
        /// Gets the Lightness component in the range from 0..1 (percentage).
        /// </summary>
        /// <remarks>
        /// <list type="bullet">
        ///   <item>0 is fully black.</item>
        ///   <item>1 is fully white.</item>
        /// </list>
        /// </remarks>
        public double L { get; }

        /// <inheritdoc/>
        public bool Equals(HslColor other)
        {
            return other.A == A &&
                   other.H == H &&
                   other.S == S &&
                   other.L == L;
        }

        /// <inheritdoc/>
        public override bool Equals(object? obj)
        {
            if (obj is HslColor hslColor)
            {
                return Equals(hslColor);
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Gets a hashcode for this object.
        /// Hashcode is not guaranteed to be unique.
        /// </summary>
        /// <returns>The hashcode for this object.</returns>
        public override int GetHashCode()
        {
            // Same algorithm as Color
            // This is used instead of HashCode.Combine() due to .NET Standard 2.0 requirements
            unchecked
            {
                int hashCode = A.GetHashCode();
                hashCode = (hashCode * 397) ^ H.GetHashCode();
                hashCode = (hashCode * 397) ^ S.GetHashCode();
                hashCode = (hashCode * 397) ^ L.GetHashCode();
                return hashCode;
            }
        }

        /// <summary>
        /// Returns the RGB color model equivalent of this HSL color.
        /// </summary>
        /// <returns>The RGB equivalent color.</returns>
        public Color ToRgb()
        {
            // Use the by-component conversion method directly for performance
            return HslColor.ToRgb(H, S, L, A);
        }

        /// <inheritdoc/>
        public override string ToString()
        {
            var sb = new StringBuilder();

            // Use a format similar to CSS. However:
            //   - To ensure precision is never lost, allow decimal places.
            //     This is especially important for round-trip serialization.
            //   - To maintain numerical consistency, do not use percent.
            //
            // Example:
            //
            // hsla(hue, saturation, lightness, alpha)
            // hsla(230, 1.0, 0.5, 1.0)
            //

            sb.Append("hsva(");
            sb.Append(H.ToString(CultureInfo.InvariantCulture));
            sb.Append(", ");
            sb.Append(S.ToString(CultureInfo.InvariantCulture));
            sb.Append(", ");
            sb.Append(L.ToString(CultureInfo.InvariantCulture));
            sb.Append(", ");
            sb.Append(A.ToString(CultureInfo.InvariantCulture));
            sb.Append(')');

            return sb.ToString();
        }

        /// <summary>
        /// Parses an HSL color string.
        /// </summary>
        /// <param name="s">The HSL color string to parse.</param>
        /// <returns>The parsed <see cref="HslColor"/>.</returns>
        public static HslColor Parse(string s)
        {
            if (s is null)
            {
                throw new ArgumentNullException(nameof(s));
            }

            if (TryParse(s, out HslColor hslColor))
            {
                return hslColor;
            }

            throw new FormatException($"Invalid HSL color string: '{s}'.");
        }

        /// <summary>
        /// Parses an HSL color string.
        /// </summary>
        /// <param name="s">The HSL color string to parse.</param>
        /// <param name="hslColor">The parsed <see cref="HslColor"/>.</param>
        /// <returns>True if parsing was successful; otherwise, false.</returns>
        public static bool TryParse(string s, out HslColor hslColor)
        {
            bool prefixMatched = false;

            hslColor = default;

            if (s is null)
            {
                return false;
            }

            string workingString = s.Trim();

            if (workingString.Length == 0 ||
                workingString.IndexOf(",", StringComparison.Ordinal) < 0)
            {
                return false;
            }

            // Note: The length checks are also an important optimization.
            // The shortest possible format is "hsl(0,0,0)", Length = 10.

            if (workingString.Length >= 11 &&
                workingString.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) &&
                workingString.EndsWith(")", StringComparison.Ordinal))
            {
                workingString = workingString.Substring(5, workingString.Length - 6);
                prefixMatched = true;
            }

            if (prefixMatched == false &&
                workingString.Length >= 10 &&
                workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) &&
                workingString.EndsWith(")", StringComparison.Ordinal))
            {
                workingString = workingString.Substring(4, workingString.Length - 5);
                prefixMatched = true;
            }

            if (prefixMatched == false)
            {
                return false;
            }

            string[] components = workingString.Split(',');

            if (components.Length == 3) // HSL
            {
                if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
                    TryInternalParse(components[1], out double saturation) &&
                    TryInternalParse(components[2], out double lightness))
                {
                    hslColor = new HslColor(1.0, hue, saturation, lightness);
                    return true;
                }
            }
            else if (components.Length == 4) // HSLA
            {
                if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
                    TryInternalParse(components[1], out double saturation) &&
                    TryInternalParse(components[2], out double lightness) &&
                    TryInternalParse(components[3], out double alpha))
                {
                    hslColor = new HslColor(alpha, hue, saturation, lightness);
                    return true;
                }
            }

            // Local function to specially parse a double value with an optional percentage sign
            bool TryInternalParse(string inString, out double outDouble)
            {
                // The percent sign, if it exists, must be at the end of the number
                int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);

                if (percentIndex >= 0)
                {
                    var result = double.TryParse(
                        inString.Substring(0, percentIndex),
                        NumberStyles.Number,
                        CultureInfo.InvariantCulture,
                        out double percentage);

                    outDouble = percentage / 100.0;
                    return result;
                }
                else
                {
                    return double.TryParse(
                        inString,
                        NumberStyles.Number,
                        CultureInfo.InvariantCulture,
                        out outDouble);
                }
            }

            return false;
        }

        /// <summary>
        /// Creates a new <see cref="HslColor"/> from individual color component values.
        /// </summary>
        /// <remarks>
        /// This exists for symmetry with the <see cref="Color"/> struct; however, the
        /// appropriate constructor should commonly be used instead.
        /// </remarks>
        /// <param name="a">The Alpha (transparency) component in the range from 0..1.</param>
        /// <param name="h">The Hue component in the range from 0..360.</param>
        /// <param name="s">The Saturation component in the range from 0..1.</param>
        /// <param name="l">The Lightness component in the range from 0..1.</param>
        /// <returns>A new <see cref="HslColor"/> built from the individual color component values.</returns>
        public static HslColor FromAhsl(double a, double h, double s, double l)
        {
            return new HslColor(a, h, s, l);
        }

        /// <summary>
        /// Creates a new <see cref="HslColor"/> from individual color component values.
        /// </summary>
        /// <remarks>
        /// This exists for symmetry with the <see cref="Color"/> struct; however, the
        /// appropriate constructor should commonly be used instead.
        /// </remarks>
        /// <param name="h">The Hue component in the range from 0..360.</param>
        /// <param name="s">The Saturation component in the range from 0..1.</param>
        /// <param name="l">The Lightness component in the range from 0..1.</param>
        /// <returns>A new <see cref="HslColor"/> built from the individual color component values.</returns>
        public static HslColor FromHsl(double h, double s, double l)
        {
            return new HslColor(1.0, h, s, l);
        }

        /// <summary>
        /// Converts the given HSL color to its RGB color equivalent.
        /// </summary>
        /// <param name="hslColor">The color in the HSL color model.</param>
        /// <returns>A new RGB <see cref="Color"/> equivalent to the given HSLA values.</returns>
        public static Color ToRgb(HslColor hslColor)
        {
            return HslColor.ToRgb(hslColor.H, hslColor.S, hslColor.L, hslColor.A);
        }

        /// <summary>
        /// Converts the given HSLA color component values to their RGB color equivalent.
        /// </summary>
        /// <param name="hue">The Hue component in the HSL color model in the range from 0..360.</param>
        /// <param name="saturation">The Saturation component in the HSL color model in the range from 0..1.</param>
        /// <param name="lightness">The Lightness component in the HSL color model in the range from 0..1.</param>
        /// <param name="alpha">The Alpha component in the range from 0..1.</param>
        /// <returns>A new RGB <see cref="Color"/> equivalent to the given HSLA values.</returns>
        public static Color ToRgb(
            double hue,
            double saturation,
            double lightness,
            double alpha = 1.0)
        {
            // Note: Conversion code is originally based on ColorHelper in the Windows Community Toolkit (licensed MIT)
            // https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs
            // It has been modified to ensure input ranges and not throw exceptions.

            // We want the hue to be between 0 and 359,
            // so we first ensure that that's the case.
            while (hue >= 360.0)
            {
                hue -= 360.0;
            }

            while (hue < 0.0)
            {
                hue += 360.0;
            }

            // We similarly clamp saturation, lightness and alpha between 0 and 1.
            saturation = saturation < 0.0 ? 0.0 : saturation;
            saturation = saturation > 1.0 ? 1.0 : saturation;

            lightness = lightness < 0.0 ? 0.0 : lightness;
            lightness = lightness > 1.0 ? 1.0 : lightness;

            alpha = alpha < 0.0 ? 0.0 : alpha;
            alpha = alpha > 1.0 ? 1.0 : alpha;

            double chroma = (1 - Math.Abs((2 * lightness) - 1)) * saturation;
            double h1 = hue / 60;
            double x = chroma * (1 - Math.Abs((h1 % 2) - 1));
            double m = lightness - (0.5 * chroma);
            double r1, g1, b1;

            if (h1 < 1)
            {
                r1 = chroma;
                g1 = x;
                b1 = 0;
            }
            else if (h1 < 2)
            {
                r1 = x;
                g1 = chroma;
                b1 = 0;
            }
            else if (h1 < 3)
            {
                r1 = 0;
                g1 = chroma;
                b1 = x;
            }
            else if (h1 < 4)
            {
                r1 = 0;
                g1 = x;
                b1 = chroma;
            }
            else if (h1 < 5)
            {
                r1 = x;
                g1 = 0;
                b1 = chroma;
            }
            else
            {
                r1 = chroma;
                g1 = 0;
                b1 = x;
            }

            return Color.FromArgb(
                (byte)Math.Round(255 * alpha),
                (byte)Math.Round(255 * (r1 + m)),
                (byte)Math.Round(255 * (g1 + m)),
                (byte)Math.Round(255 * (b1 + m)));
        }

        /// <summary>
        /// Indicates whether the values of two specified <see cref="HslColor"/> objects are equal.
        /// </summary>
        /// <param name="left">The first object to compare.</param>
        /// <param name="right">The second object to compare.</param>
        /// <returns>True if left and right are equal; otherwise, false.</returns>
        public static bool operator ==(HslColor left, HslColor right)
        {
            return left.Equals(right);
        }

        /// <summary>
        /// Indicates whether the values of two specified <see cref="HslColor"/> objects are not equal.
        /// </summary>
        /// <param name="left">The first object to compare.</param>
        /// <param name="right">The second object to compare.</param>
        /// <returns>True if left and right are not equal; otherwise, false.</returns>
        public static bool operator !=(HslColor left, HslColor right)
        {
            return !(left == right);
        }

        /// <summary>
        /// Explicit conversion from an <see cref="HslColor"/> to a <see cref="Color"/>.
        /// </summary>
        /// <param name="hslColor">The <see cref="HslColor"/> to convert.</param>
        public static explicit operator Color(HslColor hslColor)
        {
            return hslColor.ToRgb();
        }
    }
}
