/*
 * @(#)ColorUtils.java 9/19/2006
 *
 * Copyright 2002 - 2006 JIDE Software Inc. All rights reserved.
 */

package com.jidesoft.utils;

import javax.swing.plaf.ColorUIResource;
import java.awt.*;

/**
 * Several useful methods for Color.
 */
public class ColorUtils {

    /**
     * Gets a derived color from an existing color. The derived color is either lighter or darker version of the given
     * color with the same hue.
     *
     * @param color the given color.
     * @param ratio the ratio. 0.5f if the same color. Any ratio greater than 0.5f will make the result color lighter.
     *              Smaller than 0.5f will make the color darker.
     *
     * @return the derived color.
     */
    public static Color getDerivedColor(Color color, float ratio) {
        if (color != null) {
            float[] hsl = RGBtoHSL(color);
            if (hsl[2] < 0.4) {
                hsl[2] = 0.4f;
            }
            if (ratio > 0.5) {
                hsl[2] += (1f - hsl[2]) * 2 * (ratio - 0.5);
            }
            else {
                hsl[2] -= hsl[2] * 2 * (0.5 - ratio);
            }
            int colorRGB = HSLtoRGB(hsl);
            return new ColorUIResource(colorRGB);
        }
        else {
            return null;
        }
    }

    /**
     * Converts a color from RBG to HSL color space.
     *
     * @param colorRGB the Color.
     *
     * @return color space in HSL.
     */
    public static float[] RGBtoHSL(Color colorRGB) {
        float r, g, b, h, s, l; //this function works with floats between 0 and 1
        r = colorRGB.getRed() / 256.0f;
        g = colorRGB.getGreen() / 256.0f;
        b = colorRGB.getBlue() / 256.0f;

        // Then, minColor and maxColor are defined. Min color is the value of the color component with
        // the smallest value, while maxColor is the value of the color component with the largest value.
        // These two variables are needed because the Lightness is defined as (minColor + maxColor) / 2.

        float maxColor = Math.max(r, Math.max(g, b));
        float minColor = Math.min(r, Math.min(g, b));

        // If minColor equals maxColor, we know that R=G=B and thus the color is a shade of gray.
        // This is a trivial case, hue can be set to anything, saturation has to be set to 0 because
        // only then it's a shade of gray, and lightness is set to R=G=B, the shade of the gray.

        //R == G == B, so it's a shade of gray
        if (r == g && g == b) {
            h = 0.0f; //it doesn't matter what value it has
            s = 0.0f;
            l = r; //doesn't matter if you pick r, g, or b
        }

        // If minColor is not equal to maxColor, we have a real color instead of a shade of gray, so more calculations are needed:

        // Lightness (l) is now set to it's definition of (minColor + maxColor)/2.
        // Saturation (s) is then calculated with a different formula depending if light is in the first half of the second half. This is because the HSL model can be represented as a double cone, the first cone has a black tip and corresponds to the first half of lightness values, the second cone has a white tip and contains the second half of lightness values.
        // Hue (h) is calculated with a different formula depending on which of the 3 color components is the dominating one, and then normalized to a number between 0 and 1.

        else {
            l = (minColor + maxColor) / 2;

            if (l < 0.5) s = (maxColor - minColor) / (maxColor + minColor);
            else s = (maxColor - minColor) / (2.0f - maxColor - minColor);

            if (r == maxColor) h = (g - b) / (maxColor - minColor);
            else if (g == maxColor) h = 2.0f + (b - r) / (maxColor - minColor);
            else h = 4.0f + (r - g) / (maxColor - minColor);

            h /= 6; //to bring it to a number between 0 and 1
            if (h < 0) h++;
        }

        // Finally, H, S and L are calculated out of h, s and l as integers between 0 and 255 and "returned"
        // as the result. Returned, because H, S and L were passed by reference to the function.

        float[] hsl = new float[3];
        hsl[0] = h;
        hsl[1] = s;
        hsl[2] = l;
        return hsl;
    }

    /**
     * Converts from HSL color space to RGB color.
     *
     * @param hsl the hsl values.
     *
     * @return the RGB color.
     */
    public static int HSLtoRGB(float[] hsl) {
        float r, g, b, h, s, l; //this function works with floats between 0 and 1
        float temp1, temp2, tempr, tempg, tempb;
        h = hsl[0];
        s = hsl[1];
        l = hsl[2];

        // Then follows a trivial case: if the saturation is 0, the color will be a grayscale color,
        // and the calculation is then very simple: r, g and b are all set to the lightness.

        //If saturation is 0, the color is a shade of gray
        if (s == 0) {
            r = g = b = l;
        }
        // If the saturation is higher than 0, more calculations are needed again.
        // red, green and blue are calculated with the formulas defined in the code.
        // If saturation > 0, more complex calculations are needed
        else {
            //Set the temporary values
            if (l < 0.5) temp2 = l * (1 + s);
            else temp2 = (l + s) - (l * s);
            temp1 = 2 * l - temp2;
            tempr = h + 1.0f / 3.0f;
            if (tempr > 1) tempr--;
            tempg = h;
            tempb = h - 1.0f / 3.0f;
            if (tempb < 0) tempb++;

            //Red
            if (tempr < 1.0 / 6.0) r = temp1 + (temp2 - temp1) * 6.0f * tempr;
            else if (tempr < 0.5) r = temp2;
            else if (tempr < 2.0 / 3.0) r = temp1 + (temp2 - temp1) * ((2.0f / 3.0f) - tempr) * 6.0f;
            else r = temp1;

            //Green
            if (tempg < 1.0 / 6.0) g = temp1 + (temp2 - temp1) * 6.0f * tempg;
            else if (tempg < 0.5) g = temp2;
            else if (tempg < 2.0 / 3.0) g = temp1 + (temp2 - temp1) * ((2.0f / 3.0f) - tempg) * 6.0f;
            else g = temp1;

            //Blue
            if (tempb < 1.0 / 6.0) b = temp1 + (temp2 - temp1) * 6.0f * tempb;
            else if (tempb < 0.5) b = temp2;
            else if (tempb < 2.0 / 3.0) b = temp1 + (temp2 - temp1) * ((2.0f / 3.0f) - tempb) * 6.0f;
            else b = temp1;
        }

        // And finally, the results are returned as integers between 0 and 255.

        int result = 0;
        result += ((int) (r * 255) & 0xFF) << 16;
        result += ((int) (g * 255) & 0xFF) << 8;
        result += ((int) (b * 255) & 0xFF);

        return result;
    }

    static final float OFFSET_180 = 180f;
    static final float OFFSET_100 = 100f;

    public static int[] calculateDifferent(float[] from, float[] to) {
        int[] diff = new int[3];
        diff[0] = floatToInteger(from[0], to[0], OFFSET_180, true);
        diff[1] = floatToInteger(from[1], to[1], OFFSET_100, false);
        diff[2] = floatToInteger(from[2], to[2], OFFSET_100, false);
        return diff;
    }

    public static float[] applyDifference(float[] from, int[] diff) {
        float[] to = new float[3];
        to[0] = integerToFloat(from[0], diff[0], OFFSET_180, true);
        to[1] = integerToFloat(from[1], diff[1], OFFSET_100, false);
        to[2] = integerToFloat(from[2], diff[2], OFFSET_100, false);
        return to;
    }

    private static int floatToInteger(float f, float f2, float offset, boolean rotate) {
        if (rotate) {
            int i = (int) ((f2 - f) * 2 * offset);
            if (i > offset) {
                return i - (int) (2 * offset);
            }
            else if (i < -offset) {
                return i + (int) (2 * offset);
            }
            else {
                return i;
            }
        }
        else {
            if (f != 0.0f) {
                return (int) ((f2 - f) * offset / f);
            }
            else {
                return (int) ((f2 - f) * offset);
            }
        }
    }

    private static float integerToFloat(float f, int i, float offset, boolean rotate) {
        if (rotate) {
            float v = f + i / (2 * offset);
            if (v < 0.0f) {
                return v + 1.0f;
            }
            else if (v > 1.0f) {
                return v - 1.0f;
            }
            else {
                return v;
            }
        }
        else {
            if (i > 0) {
                return f + (1.0f - f) * i / offset;
            }
            else {
                return f + f * i / offset;
            }
        }
    }

    /**
     * Simply calls new Color(color, hasalpha) for each color in colors and returns all of them.
     *
     * @param hasAlpha true to consider the alpha when creating the Color.
     * @param colors   the color value.
     *
     * @return the colors with alpha added.
     */
    public static Color[] toColors(boolean hasAlpha, int... colors) {
        Color[] result = new Color[colors.length];
        for (int i = 0; i < colors.length; i++) {
            result[i] = new Color(colors[i], hasAlpha);
        }
        return result;
    }

    /**
     * Converts from a color to gray scale color.
     *
     * @param c a color.
     *
     * @return a color in gray scale.
     */
    public static Color toGrayscale(Color c) {
        int gray = (int) (c.getRed() * 0.3 + c.getGreen() * 0.59 + c.getBlue() * 0.11);
        return new Color(gray, gray, gray);
    }
}
