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 LCH color space. A LCHColor can be converted to any
 * other color model. To use this in Processing use the getColor() Method:<br>
 * 
 * <pre>
 * fill(myLchObject.getColor());
 * </pre>
 * 
 * The difference between multiple color objects can be calculated with the
 * methods of ColorDifference class. <br>
 * 
 * The ranges of chroma value is not bounded. The following value just give an
 * approximation of the range.<br>
 * <br>
 * 
 * L = Luminance [0, 100]<br>
 * C = Chroma ~ [0, 133.45]<br>
 * H = Hue [0, 360] <br>
 * a = Alpha = transparency [0, 100] <br>
 * <br>
 * 
 * @author Diana Lange
 *
 */
public class LCHColor implements RColor {

	/**
	 * The values of L - C - H - 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.LCHColor;

	/**
	 * The maximal chroma value any LCH color can have (approximation).
	 */
	public final static float MAX_C = 133.45f;

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

	/**
	 * Creates a new opaque LCHColor object. The input values will be
	 * constrained to the valid range of LCH.
	 * 
	 * @param l
	 *            The luminance value [0, 100]
	 * @param c
	 *            The chroma value [0, 133.45]
	 * @param h
	 *            The hue value [0, 360]
	 */
	public LCHColor(Float l, Float c, Float h) {
		this();
		this.setLuminance(l).setHue(h).setChroma(c).setAlpha(100f);
	}

	/**
	 * Creates a new LCHColor object. The input values will be constrained to
	 * the valid range of LCH.
	 * 
	 * @param l
	 *            The luminance value [0, 100]
	 * @param c
	 *            The chroma value [0, 133.45]
	 * @param h
	 *            The hue value [0, 360]
	 * @param alpha
	 *            THe alpha value [0, 100]
	 */
	public LCHColor(Float l, Float c, Float h, Float alpha) {
		this();
		this.setLuminance(l).setHue(h).setChroma(c).setAlpha(alpha);
	}

	/**
	 * Creates a new LCHColor object. The input values will be constrained to
	 * the valid range of LCHColor.
	 * 
	 * @param channels
	 *            Either [l,c, h] or [l, c, h, alpha].
	 */
	public LCHColor(float... channels) {
		this();
		this.setLuminance(channels.length >= 1 ? channels[0] : 0f).setChroma(channels.length >= 2 ? channels[1] : 0f)
				.setHue(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 de.returnvoid.color.RVColor#setAlpha(float)
	 */
	@Override
	public LCHColor 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 LCHColor setAlpha(int alpha) {
		return this.setAlpha((float) alpha);
	}

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

	/**
	 * Sets the luminance value. The input value will be constrained to the
	 * valid range of LCH.
	 * 
	 * @param l
	 *            The new luminance value [0, 100].
	 * @return The current LCHColor object.
	 */
	public LCHColor setLuminance(Float l) {
		wasUpdated = true;
		channels[0] = l >= LabColor.MIN_L && l <= LabColor.MAX_L ? l
				: (l < LabColor.MIN_L ? LabColor.MIN_L : LabColor.MAX_L);
		return this;
	}
	
	/**
	 * Sets the luminance value. The input value will be constrained to the
	 * valid range of LCH.
	 * 
	 * @param l
	 *            The new luminance value [0, 100].
	 * @return The current LCHColor object.
	 */
	public LCHColor setLuminance(int l) {
		return setLuminance((float) l);
	}

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

	/**
	 * Sets the chroma value. The input value will be constrained to the valid
	 * range of LCH.
	 * 
	 * @param c
	 *            The new chroma value [0, 133.45].
	 * @return The current LCHColor object.
	 */
	public LCHColor setChroma(Float c) {
		wasUpdated = true;
		channels[1] = c >= 0 && c <= LCHColor.MAX_C ? c : (c < 0 ? 0 : LCHColor.MAX_C);
		return this;
	}
	
	/**
	 * Sets the chroma value. The input value will be constrained to the valid
	 * range of LCH.
	 * 
	 * @param c
	 *            The new chroma value [0, 133.45].
	 * @return The current LCHColor object.
	 */
	public LCHColor setChroma(int c) {
		return setChroma((float) c);
	}

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

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

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

	/*
	 * (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] = RMath.map(vals[0], LabColor.MIN_L, LabColor.MAX_L, 0, 1);
		vals[1] /= LCHColor.MAX_C;
		vals[2] /= 360;
		vals[3] /= 100;

		return vals;
	}

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

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

	/*
	 * (non-Javadoc)
	 * 
	 * @see de.returnvoid.color.RVColor#copy()
	 */
	@Override
	public LCHColor copy() {
		return new LCHColor(new Float(getLuminance()), new Float(getChroma()), new Float(getHue()), 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;
		}

		LCHColor lchOther = (LCHColor) other;

		for (int i = 0; i < channels.length; i++) {
			if (channels[i] != lchOther.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.
	 * @return The new RCHColor object that represents the mean color.
	 */
	static public LCHColor getMeanColor(RColor... colors) {
		return LCHColor.getMeanColor(0, colors.length, colors);
	}

	/**
	 * Calculates the mean color of the input colors
	 * 
	 * @param colors
	 *            Colors.
	 * @return The new LCHColor object that represents the mean color.
	 */
	static public LCHColor getMeanColor(ArrayList<RColor> colors) {
		return LCHColor.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 LCHColor object that represents the mean color.
	 */
	static public LCHColor getMeanColor(int start, int end, RColor... colors) {

		return LCHColor.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 LCHColor object that represents the mean color.
	 */
	static public LCHColor getMeanColor(int start, int end, ArrayList<RColor> colors) {

		return LCHColor.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 LCHColor object that represents the mean color.
	 */
	static private LCHColor getMeanColor(int start, int end, RColor[] aColors, ArrayList<RColor> lColors) {

		float[] means = { 0, 0, 0, 0, 0 };
		for (int i = start; i < end; i++) {
			LCHColor lch = aColors != null ? aColors[i].toLCH() : lColors.get(i).toLCH();
			means[0] += lch.getLuminance();
			means[1] += lch.getChroma();

			// vector.x from hue angle
			means[2] += Math.cos(2 * Math.PI * lch.getHue() / 360);

			// vector.y from hue angle
			means[3] += Math.sin(2 * Math.PI * lch.getHue() / 360);

			means[4] += lch.getAlpha();
		}

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

		// angle from mean vector
		float h = (float) (360 * Math.atan2(means[3], means[2]) / (Math.PI * 2));

		if (h < 0) {
			h += 360;
		} else if (h > 360) {
			h -= 360;
		}

		return new LCHColor(means[0], means[1], h, means[4]);
	}

	/**
	 * 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 LCHColor.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 LCHColor.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 LCHColor.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 LCHColor.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,
			Integer start, Integer end) {
		float loss = 0f;

		// euclidean uses LCH, CMC uses Lab and calculates chroma / hue by
		// itself
		RColor mean = m == ColorDifferenceMeasure.LCHCMC ? rvMean.toLab() : rvMean.toLCH();

		for (int i = start; i < end; i++) {
			float diff = 0f;
			RColor current = aColors != null ? aColors[i] : lColors.get(i);
			RColor c = m == ColorDifferenceMeasure.LCHCMC ? current.toLab() : current.toLCH();

			if (m == ColorDifferenceMeasure.LCHCMC) {
				diff = ColorDifference.lchCMC(mean, c);
			} else {
				diff = ColorDifference.lchEuclidean(mean, c);
			}

			loss += diff;
		}

		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 LCHColor lerpColors(RColor input1, RColor input2, float amt) {
		amt = RMath.constrain(amt, 0, 1);

		LCHColor c1 = input1.toLCH();
		LCHColor c2 = input2.toLCH();

		float minH = RMath.min(c1.getHue(), c2.getHue());
		float maxH = RMath.min(c1.getHue(), c2.getHue());

		float h1 = c1.getHue();
		float h2 = c2.getHue();
		float h = 0;

		// since the hue value is an angle on a color circle the highest
		// distance of two hue values should be 180 degrees. The following will
		// re-calculate the range if the distance is more then 180 degrees (e.g.
		// if h1=350 and h2=10 [distance=340°] h1 will become -10 [distance=20°]
		if (maxH - minH > 180) {
			if (h1 == minH) {
				h1 += 360;
			} else {
				h1 -= 360;
			}
		}

		// h2 / h1 are the same
		if (h2 - h1 == 0) {
			h = h2;
		} else {

			// interpolate the h values if h1 / h2 are not the same
			h = RMath.map(amt, 0, 1, h1, h2);
		}

		// since the values of h are re-arranged, negative results may be
		// possible which needs to be re-mapped to 0-360
		if (h < 0) {
			h += 360;
		} else if (h > 360) {
			h -= 360;
		}
		return new LCHColor(RMath.map(amt, 0, 1, c1.getLuminance(), c2.getLuminance()),
				RMath.map(amt, 0, 1, c1.getChroma(), c2.getChroma()), h,
				RMath.map(amt, 0, 1, c1.getAlpha(), c2.getAlpha()));
	}

	/**
	 * Creates a new opaque LabColor with random values.
	 * 
	 * @return The new color.
	 */
	static public LCHColor getRandomLCH() {

		// since the valid ranges for c varies (non uniform space) the random
		// color is converted to RGB and back to get true valid values
		LCHColor lch = new LCHColor(RMath.random(LabColor.MIN_L, LabColor.MAX_L), RMath.random(LCHColor.MAX_C),
				RMath.random(360.0f));

		return lch.toRGB().toLCH();
	}

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

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

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

	// 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.toLab().toXYZ();
	}

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

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

		return new LabColor(channels[0], (float) Math.cos(2 * Math.PI * channels[2] / 360) * channels[1],
				(float) Math.sin(2 * Math.PI * channels[2] / 360) * channels[1], this.getAlpha());
	}

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

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