package net.returnvoid.color;

import java.util.ArrayList;

import net.returnvoid.tools.RMath;
import net.returnvoid.tools.StringTools;

/*
 * This code is copyright (c) Diana Lange 2017
 *
 * The library is published under the Creative Commons license NonCommercial 4.0.
 * Please check https://creativecommons.org/licenses/by-nc/4.0/ for more information.
 * 
 * This program is distributed in the hope that it will be useful, but without any warranty.
 */

/**
 * A model for color in XYZ color space. A XYZColor can be converted to any
 * other color model. To use this in Processing use the getColor() Method:<br>
 * 
 * <pre>
 * fill(myXYZObject.getColor());
 * </pre>
 * 
 * The difference between multiple color objects can be calculated with the
 * methods of ColorDifference class. <br>
 * <br>
 * X [0, XYZColor.REFERENCE_X] = [0, 95.047]<br>
 * Y [0, XYZColor.REFERENCE_Y] = [0, 100]<br>
 * Z [0, XYZColor.REFERENCE_Z] = [0, 108.883]<br>
 * A = alpha = transparency [0, 100] <br>
 * <br>
 * 
 * @author Diana Lange
 *
 */
public class XYZColor implements RColor {

	/**
	 * The values of X - Y - Z - Alpha
	 */
	private Float[] channels;

	/**
	 * True if any channel has been updated (e.g. with one of the setters). The
	 * hex value will be recalculated if wasUpdated is true and the method for
	 * getting the hex value is called.
	 */
	private boolean wasUpdated;

	/**
	 * The hexadecimal value of the color object. This value can be used within
	 * the the color methods of processing to set the fill or stroke color. The
	 * range of hex is 0x00000000 to 0xFFFFFFFF. hex only will be calculated
	 * when wasUpdated is true and a method for getting the hex value is called.
	 */
	private int hex;

	/**
	 * The ColorSpace of RGB colors.
	 */
	public final static ColorSpace NAME = ColorSpace.XYZColor;

	/**
	 * X Value for the specific illuminants and observers (2° [CIE 1931] - D65 -
	 * Daylight, sRGB, Adobe-RGB conversion)
	 */
	public final static float REFERENCE_X = 95.047f;

	/**
	 * Y Value for the specific illuminants and observers (2° [CIE 1931] - D65 -
	 * Daylight, sRGB, Adobe-RGB conversion)
	 */
	public final static float REFERENCE_Y = 100.0f;

	/**
	 * Z Value for the specific illuminants and observers (2° [CIE 1931] - D65 -
	 * Daylight, sRGB, Adobe-RGB conversion)
	 */
	public final static float REFERENCE_Z = 108.883f;

	/**
	 * Builds an new XYZColor (all values initialized, all color channels are
	 * zero).
	 */
	private XYZColor() {
		wasUpdated = true;
		hex = 0;
		channels = new Float[4];
	}

	/**
	 * Creates a new opaque XYZColor object. The input values will be
	 * constrained to the valid range of XYZ.
	 * 
	 * @param x
	 *            The x value [0, REFERENCE_X] = [0, 95.047]
	 * @param y
	 *            The y value [0, REFERENCE_Y] = [0, 100]
	 * @param z
	 *            The z value [0, REFERENCE_Z] = [0, 108.883]
	 */
	public XYZColor(Float x, Float y, Float z) {
		this();
		this.setX(x).setY(y).setZ(z).setAlpha(100f);
	}

	/**
	 * Creates a new XYZColor object. The input values will be constrained to
	 * the valid range of XYZ.
	 * 
	 * @param x
	 *            The x value [0, REFERENCE_X] = [0, 95.047]
	 * @param y
	 *            The y value [0, REFERENCE_Y] = [0, 100]
	 * @param z
	 *            The z value [0, REFERENCE_Z] = [0, 108.883]
	 * @param alpha
	 *            THe alpha value [0, 100]
	 */
	public XYZColor(Float x, Float y, Float z, Float alpha) {
		wasUpdated = true;
		hex = 0;
		channels = new Float[4];

		this.setX(x).setY(y).setZ(z).setAlpha(alpha);
	}

	/**
	 * Creates a new XYZColor object. The input values will be constrained to
	 * the valid range of XYZColor.
	 * 
	 * @param channels
	 *            Either [x, y, z] or [x, y, z, alpha].
	 */
	public XYZColor(float... channels) {
		this();
		this.setX(channels.length >= 1 ? channels[0] : 0f).setY(channels.length >= 2 ? channels[1] : 0f)
				.setZ(channels.length >= 3 ? channels[2] : 0f).setAlpha(channels.length >= 4 ? channels[3] : 100f);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#getColor()
	 */
	@Override
	public int getColor() {

		if (wasUpdated) {

			hex = this.toRGB().getColor();

			wasUpdated = false;
		}

		return hex;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.color.RColor#setAlpha(java.lang.Float)
	 */
	@Override
	public XYZColor setAlpha(Float alpha) {
		wasUpdated = true;
		channels[3] = alpha >= 0 && alpha <= 100 ? alpha : (alpha < 0 ? 0 : 100);
		return this;
	}
	
	/**
	 * Sets the value of alpha / transparency. The range of alpha is [0, 100].
	 * Zero always means that the color is completely transparent. The high
	 * value means that the color is completely opaque.
	 * 
	 * @param alpha
	 *            The value of the colors transparency.
	 * 
	 * @return The current color object.
	 */
	public XYZColor setAlpha(int alpha) {
		return this.setAlpha((float) alpha);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.color.RColor#getAlpha()
	 */
	@Override
	public Float getAlpha() {
		return channels[3];
	}

	/**
	 * Sets the x value. The input value will be constrained to the valid range
	 * of XYZ.
	 * 
	 * @param x
	 *            The new x value [0, REFERENCE_X] = [0, 95.047].
	 * @return The current XYZColor object.
	 */
	public XYZColor setX(Float x) {
		wasUpdated = true;
		channels[0] = x >= 0 && x <= XYZColor.REFERENCE_X ? x : (x < 0 ? 0 : XYZColor.REFERENCE_X);
		return this;
	}
	
	/**
	 * Sets the x value. The input value will be constrained to the valid range
	 * of XYZ.
	 * 
	 * @param x
	 *            The new x value [0, REFERENCE_X] = [0, 95.047].
	 * @return The current XYZColor object.
	 */
	public XYZColor setX(int x) {
		return setX((float) x);
	}

	/**
	 * Sets the y value. The input value will be constrained to the valid range
	 * of XYZ.
	 * 
	 * @param y
	 *            The new y value [0, REFERENCE_Y] = [0, 100].
	 * @return The current XYZColor object.
	 */
	public XYZColor setY(Float y) {
		wasUpdated = true;
		channels[1] = y >= 0 && y <= XYZColor.REFERENCE_Y ? y : (y < 0 ? 0 : XYZColor.REFERENCE_Y);
		return this;
	}
	
	/**
	 * Sets the y value. The input value will be constrained to the valid range
	 * of XYZ.
	 * 
	 * @param y
	 *            The new y value [0, REFERENCE_Y] = [0, 100].
	 * @return The current XYZColor object.
	 */
	public XYZColor setY(int y) {
		return setY((float) y);
	}

	/**
	 * Sets the z value. The input value will be constrained to the valid range
	 * of XYZ.
	 * 
	 * @param z
	 *            The new z value [0, REFERENCE_Z] = [0, 108.883].
	 * @return The current XYZColor object.
	 */
	public XYZColor setZ(Float z) {
		wasUpdated = true;
		channels[2] = z >= 0 && z <= XYZColor.REFERENCE_Z ? z : (z < 0 ? 0 : XYZColor.REFERENCE_Z);
		return this;
	}
	
	/**
	 * Sets the z value. The input value will be constrained to the valid range
	 * of XYZ.
	 * 
	 * @param z
	 *            The new z value [0, REFERENCE_Z] = [0, 108.883].
	 * @return The current XYZColor object.
	 */
	public XYZColor setZ(int z) {
		return setZ((float) z);
	}

	/**
	 * Returns the current value of x of this color.
	 * 
	 * @return The x of this color.
	 */
	public Float getX() {
		return channels[0];
	}

	/**
	 * Returns the current value of y of this color.
	 * 
	 * @return The y of this color.
	 */
	public Float getY() {
		return channels[1];
	}

	/**
	 * Returns the current value of z of this color.
	 * 
	 * @return The z of this color.
	 */
	public Float getZ() {
		return channels[2];
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#getAlpha()
	 */
	@Override
	public String getHexString() {
		return Integer.toHexString(this.getColor()).toUpperCase();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#getColorIngnoringAlpha()
	 */
	@Override
	public int getOpColor() {

		int hex = this.getColor();
		return 255 << 24 | hex;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#getHexStringIngnoringAlpha()
	 */
	@Override
	public String getOpHexString() {
		return Integer.toHexString(this.getOpColor()).toUpperCase().substring(2);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#getValues()
	 */
	@Override
	public float[] getValues() {
		float[] copy = new float[4];
		copy[0] = channels[0];
		copy[1] = channels[1];
		copy[2] = channels[2];
		copy[3] = channels[3];
		return copy;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#getValuesNormalized()
	 */
	@Override
	public float[] getValuesNormalized() {
		float[] vals = getValues();
		vals[0] /= XYZColor.REFERENCE_X;
		vals[1] /= XYZColor.REFERENCE_Y;
		vals[2] /= XYZColor.REFERENCE_Z;
		vals[3] /= 100;

		return vals;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#getName()
	 */
	@Override
	public String getName() {
		return XYZColor.NAME.name();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#getColorSpace()
	 */
	@Override
	public ColorSpace getColorSpace() {
		return XYZColor.NAME;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#copy()
	 */
	@Override
	public XYZColor copy() {
		return new XYZColor(new Float(getX()), new Float(getY()), new Float(getZ()), new Float(getAlpha()));
	}

	/**
	 * Checks if the input color is equal to this object. Two colors are equal
	 * if they are member of the same color space and all channel values are
	 * equal.
	 * 
	 * @param other
	 *            The other color
	 * @return True if both colors are considered the same;
	 */
	public boolean equals(RColor other) {

		if (this.getColorSpace() != other.getColorSpace()) {
			return false;
		}

		XYZColor xyzOther = (XYZColor) other;

		for (int i = 0; i < channels.length; i++) {
			if (channels[i] != xyzOther.channels[i]) {
				return false;
			}
		}

		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return getName() + ": [" + StringTools.joinAndFormat(getValues(), ", ", 3, 2) + "] [#" + getOpHexString() + "]";
	}

	/*
	 * ------------------------------------------------------------------------
	 * | Static Functions |
	 * ------------------------------------------------------------------------
	 */

	/**
	 * Calculates the mean color of the input colors
	 * 
	 * @param colors
	 *            Colors (1 or more).
	 * @return The new XYZColor object that represents the mean color.
	 */
	static public XYZColor getMeanColor(RColor... colors) {
		return XYZColor.getMeanColor(0, colors.length, colors);
	}

	/**
	 * Calculates the mean color of the input colors
	 * 
	 * @param colors
	 *            Colors.
	 * @return The new XYZColor object that represents the mean color.
	 */
	static public XYZColor getMeanColor(ArrayList<RColor> colors) {
		return XYZColor.getMeanColor(0, colors.size(), colors);
	}

	/**
	 * Calculates the mean color of the input colors. With start and end a
	 * subset is defined. Just this subset will be used to calculate the mean.
	 * 
	 * @param start
	 *            Start index of the subset of the input colors (start < end).
	 *            start will be member of the subset.
	 * @param end
	 *            End index of the subset of the input colors (start < end). end
	 *            will not be member of the subset.
	 * @param colors
	 *            Colors (1 or more).
	 * @return The new XYZColor object that represents the mean color.
	 */
	static public XYZColor getMeanColor(int start, int end, RColor... colors) {

		return XYZColor.getMeanColor(start, end, colors, null);
	}

	/**
	 * Calculates the mean color of the input colors. With start and end a
	 * subset is defined. Just this subset will be used to calculate the mean.
	 * 
	 * @param start
	 *            Start index of the subset of the input colors (start < end).
	 *            start will be member of the subset.
	 * @param end
	 *            End index of the subset of the input colors (start < end). end
	 *            will not be member of the subset.
	 * @param colors
	 *            Colors.
	 * @return The new XYZColor object that represents the mean color.
	 */
	static public XYZColor getMeanColor(int start, int end, ArrayList<RColor> colors) {

		return XYZColor.getMeanColor(start, end, null, colors);
	}

	/**
	 * Calculates the mean color of the input colors of aColors / lColors. With
	 * start and end a subset is defined. Just this subset will be used to
	 * calculate the mean. start and end should be in the array range; otherwise
	 * this will produce an ArrayIndexOutOfBounds Exception. Uses just aColors
	 * OR lColors. Not both. One of aColors / lColors should be null, the other
	 * one should be not null.
	 * 
	 * @param start
	 *            Start index of the subset of the input colors (start < end).
	 *            start will be member of the subset.
	 * @param end
	 *            End index of the subset of the input colors (start < end). end
	 *            will not be member of the subset.
	 * @param aColors
	 *            Colors.
	 * @param lColors
	 *            Colors.
	 * @return The new XYZColor object that represents the mean color.
	 */
	static private XYZColor getMeanColor(int start, int end, RColor[] aColors, ArrayList<RColor> lColors) {

		float[] means = { 0, 0, 0, 0 };
		for (int i = start; i < end; i++) {
			XYZColor xyz = aColors != null ? aColors[i].toXYZ() : lColors.get(i).toXYZ();
			means[0] += xyz.getX();
			means[1] += xyz.getY();
			means[2] += xyz.getZ();
			means[3] += xyz.getAlpha();
		}

		means[0] /= (end - start);
		means[1] /= (end - start);
		means[2] /= (end - start);
		means[3] /= (end - start);

		return new XYZColor(means[0], means[1], means[2], means[3]);
	}

	/**
	 * Calculates the loss - the mean distance of all members of colors to
	 * rvMean.
	 * 
	 * @param rvMean
	 *            A base color which will be used to calculate the distance to.
	 * @param colors
	 *            Colors.
	 * @param m
	 *            The measure which defines how the loss is calculated and which
	 *            color model will be used. This should be an RGB color
	 *            difference measure.
	 * @return The mean loss value.
	 */
	static public Float getLoss(RColor rvMean, RColor[] colors, ColorDifferenceMeasure m) {
		return XYZColor.getLoss(rvMean, colors, null, m, 0, colors.length);
	}

	/**
	 * Calculates the loss - the mean distance of all members of colors to
	 * rvMean. With start and end a subset is defined. Just this subset will be
	 * used to calculate the loss. start and end should be in the array range;
	 * otherwise this will produce an ArrayIndexOutOfBounds Exception.
	 * 
	 * @param rvMean
	 *            A base color which will be used to calculate the distance to.
	 * @param colors
	 *            Colors.
	 * @param m
	 *            The measure which defines how the loss is calculated and which
	 *            color model will be used. This should be an RGB color
	 *            difference measure.
	 * @param start
	 *            Start index of the subset of the input colors (start < end).
	 *            start will be member of the subset.
	 * @param end
	 *            End index of the subset of the input colors (start < end). end
	 *            will not be member of the subset.
	 * @return The mean loss value.
	 */
	static public Float getLoss(RColor rvMean, RColor[] colors, ColorDifferenceMeasure m, int start, int end) {
		return XYZColor.getLoss(rvMean, colors, null, m, start, end);
	}

	/**
	 * Calculates the loss - the mean distance of all members of colors to
	 * rvMean.
	 * 
	 * @param rvMean
	 *            A base color which will be used to calculate the distance to.
	 * @param lColors
	 *            Colors.
	 * @param m
	 *            The measure which defines how the loss is calculated and which
	 *            color model will be used. This should be an RGB color
	 *            difference measure.
	 * @return The mean loss value.
	 */
	static public Float getLoss(RColor rvMean, ArrayList<RColor> lColors, ColorDifferenceMeasure m) {
		return XYZColor.getLoss(rvMean, null, lColors, m, 0, lColors.size());
	}

	/**
	 * Calculates the loss - the mean distance of all members of colors to
	 * rvMean. With start and end a subset is defined. Just this subset will be
	 * used to calculate the loss. start and end should be in the array range;
	 * otherwise this will produce an ArrayIndexOutOfBounds Exception.
	 * 
	 * @param rvMean
	 *            A base color which will be used to calculate the distance to.
	 * @param lColors
	 *            Colors.
	 * @param m
	 *            The measure which defines how the loss is calculated and which
	 *            color model will be used. This should be an RGB color
	 *            difference measure.
	 * @param start
	 *            Start index of the subset of the input colors (start < end).
	 *            start will be member of the subset.
	 * @param end
	 *            End index of the subset of the input colors (start < end). end
	 *            will not be member of the subset.
	 * @return The mean loss value.
	 */
	static public Float getLoss(RColor rvMean, ArrayList<RColor> lColors, ColorDifferenceMeasure m, int start,
			int end) {
		return XYZColor.getLoss(rvMean, null, lColors, m, start, end);
	}

	/**
	 * Calculates the loss - the mean distance of all members of aColors /
	 * lColors to rvMean. With start and end a subset is defined. Just this
	 * subset will be used to calculate the loss. start and end should be in the
	 * array range; otherwise this will produce an ArrayIndexOutOfBounds
	 * Exception. Uses just aColors OR lColors. Not both. One of aColors /
	 * lColors should be null, the other one should be not null.
	 * 
	 * @param rvMean
	 *            A base color which will be used to calculate the distance to.
	 * @param aColors
	 *            Colors.
	 * @param lColors
	 *            Colors.
	 * @param m
	 *            The measure which defines how the loss is calculated and which
	 *            color model will be used. This should be an RGB color
	 *            difference measure.
	 * @param start
	 *            Start index of the subset of the input colors (start < end).
	 *            start will be member of the subset.
	 * @param end
	 *            End index of the subset of the input colors (start < end). end
	 *            will not be member of the subset.
	 * @return The mean loss value.
	 */
	static private Float getLoss(RColor rvMean, RColor[] aColors, ArrayList<RColor> lColors, ColorDifferenceMeasure m,
			int start, int end) {
		float loss = 0;

		for (int i = start; i < end; i++) {
			RColor c = aColors != null ? aColors[i] : lColors.get(i);

			loss += ColorDifference.difference(rvMean, c, m);
		}

		return loss / (end - start);
	}

	/**
	 * Lerps the two input colors and creates a new color. The values of the
	 * colors will be interpolated linear. The interpolation is defined by the
	 * amt parameter. For amt=0 the outcome will be input1, for amt=1 the
	 * outcome will be input2.
	 * 
	 * @param input1
	 *            The first color.
	 * @param input2
	 *            The second color.
	 * @param amt
	 *            The lerping value [0, 1].
	 * @return A new 'mixed' color.
	 */
	static public XYZColor lerpColors(RColor input1, RColor input2, float amt) {
		amt = RMath.constrain(amt, 0, 1);

		XYZColor c1 = input1.toXYZ();
		XYZColor c2 = input2.toXYZ();

		return new XYZColor(RMath.map(amt, 0, 1, c1.getX(), c2.getX()), RMath.map(amt, 0, 1, c1.getY(), c2.getY()),
				RMath.map(amt, 0, 1, c1.getZ(), c2.getZ()), RMath.map(amt, 0, 1, c1.getAlpha(), c2.getAlpha()));
	}

	/**
	 * Creates a new opaque XYZColor with random values.
	 * 
	 * @return The new color.
	 */
	static public XYZColor getRandomXYZ() {

		XYZColor xyz = new XYZColor(RMath.random(XYZColor.REFERENCE_X), RMath.random(XYZColor.REFERENCE_Y),
				RMath.random(XYZColor.REFERENCE_Z));

		return xyz.toRGB().toXYZ();
	}

	/*
	 * ------------------------------------------------------------------------
	 * | Color Conversion |
	 * ------------------------------------------------------------------------
	 */

	// RGB -----------------------

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#toRGB()
	 */
	@Override
	public RGBColor toRGB() {
		float r = channels[0] / 100 * 3.2406f + channels[1] / 100 * -1.5372f + channels[2] / 100 * -0.4986f;
		float g = channels[0] / 100 * -0.9689f + channels[1] / 100 * 1.8758f + channels[2] / 100 * 0.0415f;
		float b = channels[0] / 100 * 0.0557f + channels[1] / 100 * -0.2040f + channels[2] / 100 * 1.0570f;

		if (r > 0.0031308) {
			r = (float) (1.055 * Math.pow(r, 1 / 2.4) - 0.055);
		} else {
			r *= 12.92;
		}

		if (g > 0.0031308) {
			g = (float) (1.055 * Math.pow(g, 1 / 2.4) - 0.055);
		} else {
			g *= 12.92;
		}

		if (b > 0.0031308) {
			b = (float) (1.055 * Math.pow(b, 1 / 2.4) - 0.055);
		} else {
			b *= 12.92;
		}

		return new RGBColor(r * 255, g * 255, b * 255, 255 * getAlpha() / 100);
	}

	// HSB -----------------------

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#toHSB()
	 */
	@Override
	public HSBColor toHSB() {
		return this.toRGB().toHSB();
	}

	// XYZ -----------------------

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#toXYZ()
	 */
	@Override
	public XYZColor toXYZ() {
		return this;
	}

	// Lab -----------------------

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#toLab()
	 */
	@Override
	public LabColor toLab() {
		float x = this.getX() / XYZColor.REFERENCE_X;
		float y = this.getY() / XYZColor.REFERENCE_Y;
		float z = this.getZ() / XYZColor.REFERENCE_Z;
		
		Float l = 903.3f * y;

	    if (y > 0.008856) {
	      l = (float) (116 * Math.pow(y, 1 / 3.0) - 16);
	    } 

		if (x > 0.008856) {
			x = (float) Math.pow(x, 1 / 3.0);
		} else {
			x = 7.787f * x + 16.0f / 116;
		}

		if (y > 0.008856) {
			y = (float) Math.pow(y, 1 / 3.0);
		} else {
			y = 7.787f * y + 16.0f / 116;
		}

		if (z > 0.008856) {
			z = (float) Math.pow(z, 1 / 3.0);
		} else {
			z = 7.787f * z + 16.0f / 116;
		}

		return new LabColor(l, 500 * (x - y), 200 * (y - z), this.getAlpha());
	}

	// LCH -----------------------

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#toLCH()
	 */
	@Override
	public LCHColor toLCH() {
		return this.toLab().toLCH();
	}
}
