/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.ui.helpers;

import lombok.experimental.UtilityClass;

import net.ranides.assira.math.MathUtils;

import java.awt.*;
import java.awt.color.ColorSpace;
import java.util.Locale;

/**
 * Static methods for working with colors (especially with AWT colors).
 */
@UtilityClass
public final class ColorUtils {

    /**
     * Returns inverted RGB color.
     * @param color input
     * @return new color
     */
    public static Color invert(Color color) {
        int r = 255 - color.getRed();
        int g = 255 - color.getGreen();
        int b = 255 - color.getBlue();
        int a = color.getAlpha();
        return new Color(r,g,b,a);
    }

    /**
     * Returns new color with the same RGB values and changed alpha channel
     * @param color input
     * @param alpha new alpha value
     * @return new color
     */
    public static Color alpha(Color color, int alpha) {
        return new Color( (color.getRGB()&0x00FFFFFF)|(MathUtils.clip(alpha,0,255)<<24), true);
    }

    /**
     * Merges two specified colors.
     * It takes {@code ratio} RGB values from back, and adds {@code 1-ratio} from fore.
     * @param back input
     * @param fore input
     * @param ratio ratio between colors
     * @return new color
     */
    public static Color merge(Color back, Color fore, double ratio) {
        double p = MathUtils.clip(ratio, 0, 1.0);
        double f = 1.0 - p;
        int r = MathUtils.round( back.getRed()*f + fore.getRed() * p, 0, 255);
        int g = MathUtils.round( back.getGreen()*f + fore.getGreen() * p, 0, 255);
        int b = MathUtils.round( back.getBlue()*f + fore.getBlue() * p, 0, 255);
        int a = MathUtils.round( back.getAlpha()*f + fore.getAlpha() * p, 0, 255);
        return new Color(r,g,b,a);
    }

    /**
     * Creates new color with identical values
     * @param color input
     * @return new color
     */
    public static Color copy(Color color) {
        return new Color( color.getRGB(), true);
    }

    /**
     * Converts color into CSS2 hex string (#rrggbb). Ignores alpha.
     * @param color input
     * @return string
     */
    public static String asCSS2(Color color) {
        return String.format("#%06x", color.getRGB() & 0xFFFFFF );
    }

    /**
     * Converts color into CSS3 string (rgba(r,g,b,a)). Preserves
     * @param color color
     * @return string
     */
    public static String asCSS3(Color color) {
        if(color.getAlpha()!=255) {
            return String.format(Locale.ROOT, "rgba(%d,%d,%d,%.2f)", color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()/255.0 );
        } else {
            return String.format(Locale.ROOT, "rgb(%d,%d,%d)", color.getRed(), color.getGreen(), color.getBlue() );
        }
    }

    /**
     * Calculates distance between colors using standard RGB space.
     *
     * @param a color
     * @param b color
     * @return distance
     */
    public static double distanceRGB(Color a, Color b) {
        return distance(a.getRGBColorComponents(null), b.getRGBColorComponents(null));
    }

    /**
     * Calculates distance between colors using CIE LAB space.
     *
     * @param a color
     * @param b color
     * @return distance
     */
    public static double distanceLAB(Color a, Color b) {
        float[] fa = a.getColorComponents(CIELab.CS_LAB, null);
        float[] fb = b.getColorComponents(CIELab.CS_LAB, null);
        return distance(fa, fb);
    }

    private static double distance(float[] a, float[] b) {
        double sum = 0;
        for(int i=0, n= a.length; i<n; i++) {
            sum += (a[i]-b[i])*(a[i]-b[i]);
        }
        return Math.sqrt(sum);
    }

    private static class CIELab extends ColorSpace {

        private static final long serialVersionUID = 5027741380892134289L;

        private static final ColorSpace CS_LAB = new CIELab();

        private static final ColorSpace CS_XYZ = ColorSpace.getInstance(ColorSpace.CS_CIEXYZ);

        private static final double N = 4.0 / 29.0;

        @Override
        public float[] fromCIEXYZ(float[] colorvalue) {
            double l = f(colorvalue[1]);
            double L = 116.0 * l - 16.0;
            double a = 500.0 * (f(colorvalue[0]) - l);
            double b = 200.0 * (l - f(colorvalue[2]));
            return new float[] {(float) L, (float) a, (float) b};
        }

        @Override
        public float[] fromRGB(float[] rgbvalue) {
            return fromCIEXYZ(CS_XYZ.fromRGB(rgbvalue));
        }

        @Override
        public float getMaxValue(int component) {
            return 128f;
        }

        @Override
        public float getMinValue(int component) {
            return (component == 0)? 0f: -128f;
        }

        @Override
        public String getName(int idx) {
            return String.valueOf("Lab".charAt(idx));
        }

        @Override
        public float[] toCIEXYZ(float[] colorvalue) {
            double i = (colorvalue[0] + 16.0) * (1.0 / 116.0);
            double X = fInv(i + colorvalue[1] * (1.0 / 500.0));
            double Y = fInv(i);
            double Z = fInv(i - colorvalue[2] * (1.0 / 200.0));
            return new float[] {(float) X, (float) Y, (float) Z};
        }

        @Override
        public float[] toRGB(float[] colorvalue) {
            return CS_XYZ.toRGB(toCIEXYZ(colorvalue));
        }

        CIELab() {
            super(ColorSpace.TYPE_Lab, 3);
        }

        private static double f(double x) {
            if (x > 216.0 / 24389.0) {
                return Math.cbrt(x);
            } else {
                return (841.0 / 108.0) * x + N;
            }
        }

        private static double fInv(double x) {
            if (x > 6.0 / 29.0) {
                return x*x*x;
            } else {
                return (108.0 / 841.0) * (x - N);
            }
        }

        private Object readResolve() {
            return CS_LAB;
        }

    }

}
