package net.returnvoid.color;

import java.awt.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 HSB color space. A HSBColor can be converted to any
 * other color model. To use this in Processing use the getColor() Method:<br>
 * 
 * <pre>
 * fill(myHsbObject.getColor());
 * </pre>
 * 
 * The difference between multiple color objects can be calculated with the
 * methods of ColorDifference class. <br>
 * <br>
 * H = hue [0, 360]<br>
 * S = saturation [0, 100]<br>
 * B = brightness [0, 100] <br>
 * A = alpha = transparency [0, 100] <br>
 * <br>
 * 
 * @author Diana Lange
 *
 */
public class HSBColor implements RColor {

	/**
	 * The values of H - S - B - A
	 */
	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.HSBColor;

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

	/**
	 * Creates a new opaque HSBColor object. The input values will be
	 * constrained to the valid range of HSB.
	 * 
	 * @param h
	 *            The hue value [0, 360]
	 * @param s
	 *            The saturation value [0, 100]
	 * @param b
	 *            The brightness value [0, 100]
	 */
	public HSBColor(Float h, Float s, Float b) {
		this();
		this.setHue(h).setSaturation(s).setBrightness(b).setAlpha(100f);
	}

	/**
	 * Creates a new HSBColor object. The input values will be constrained to
	 * the valid range of HSB.
	 * 
	 * @param h
	 *            The hue value [0, 360]
	 * @param s
	 *            The saturation value [0, 100]
	 * @param b
	 *            The brightness value [0, 100]
	 * @param alpha
	 *            THe alpha value [0, 100]
	 */
	public HSBColor(Float h, Float s, Float b, Float alpha) {
		this();
		this.setHue(h).setSaturation(s).setBrightness(b).setAlpha(alpha);
	}

	/**
	 * Creates a new HSBColor object. The input values will be constrained to
	 * the valid range of HSB.
	 * 
	 * @param channels
	 *            Either [h, s, b] or [h, s, b, alpha].
	 */
	public HSBColor(float... channels) {
		this();
		this.setHue(channels.length >= 1 ? channels[0] : 0f).setSaturation(channels.length >= 2 ? channels[1] : 0f)
				.setBrightness(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 HSBColor 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 HSBColor 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 hue value. The input value will be constrained to the valid
	 * range of HSB.
	 * 
	 * @param h
	 *            The new hue value [0, 360].
	 * @return The current HSBColor object.
	 */
	public HSBColor setHue(int h) {
		return setHue((float) h);
	}

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

		return this;
	}

	/**
	 * Sets the saturation value. The input value will be constrained to the
	 * valid range of HSB.
	 * 
	 * @param s
	 *            The new saturation value [0, 100].
	 * @return The current HSBColor object.
	 */
	public HSBColor setSaturation(int s) {
		return setSaturation((float) s);
	}

	/**
	 * Sets the saturation value. The input value will be constrained to the
	 * valid range of HSB.
	 * 
	 * @param s
	 *            The new saturation value [0, 100].
	 * @return The current HSBColor object.
	 */
	public HSBColor setSaturation(Float s) {
		wasUpdated = true;
		channels[1] = s >= 0 && s <= 100 ? s : (s < 0 ? 0 : 100);

		return this;
	}

	/**
	 * Sets the hue value. The input value will be constrained to the valid
	 * range of HSB.
	 * 
	 * @param b
	 *            The new brightness value [0, 100].
	 * @return The current HSBColor object.
	 */
	public HSBColor setBrightness(int b) {
		return setBrightness((float) b);
	}

	/**
	 * Sets the hue value. The input value will be constrained to the valid
	 * range of HSB.
	 * 
	 * @param b
	 *            The new brightness value [0, 100].
	 * @return The current HSBColor object.
	 */
	public HSBColor setBrightness(Float b) {
		wasUpdated = true;
		channels[2] = b >= 0 && b <= 100 ? b : (b < 0 ? 0 : 100);

		return this;
	}

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

	/**
	 * Returns the current value of saturation of this color.
	 * 
	 * @return The saturation of this color [0, 100].
	 */
	public Float getSaturation() {
		return channels[1];
	}

	/**
	 * Returns the current value of brightness of this color.
	 * 
	 * @return The brightness of this color [0, 360].
	 */
	public Float getBrightness() {
		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#getName()
	 */
	@Override
	public String getName() {
		return HSBColor.NAME.name();
	}

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

	/*
	 * (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] /= 360;
		vals[1] /= 100;
		vals[2] /= 100;
		vals[3] /= 100;

		return vals;
	}

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

		HSBColor hsbOther = (HSBColor) other;

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

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

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

		return HSBColor.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 HSBColor object that represents the mean color.
	 */
	static private HSBColor getMeanColor(int start, int end, RColor[] aColors, ArrayList<RColor> lColors) {
		float[] means = { 0, 0, 0, 0, 0 };
		for (int i = start; i < end; i++) {
			HSBColor hsb = aColors != null ? aColors[i].toHSB() : lColors.get(i).toHSB();

			// hue + sat mean is calculated by interpreting the values
			// geometric: both values are translated to a position on a circle
			// with the angle defined by hue and the radius defined by
			// saturation

			// x on circle
			means[0] += Math.cos(2 * Math.PI * hsb.getHue() / 360);

			// y on circle
			means[1] += Math.sin(2 * Math.PI * hsb.getHue() / 360);
			means[2] += hsb.getSaturation();
			means[3] += hsb.getBrightness();
			means[4] += hsb.getAlpha();
		}

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

		float h = (float) (360 * Math.atan2(means[1], means[0]) / (Math.PI * 2));

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

		return new HSBColor(h, means[2], means[3], 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 HSB color
	 *            difference measure.
	 * @return The mean loss value.
	 */
	static public Float getLoss(RColor rvMean, RColor[] colors, ColorDifferenceMeasure m) {
		return HSBColor.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 HSB 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 HSBColor.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 HSB color
	 *            difference measure.
	 * @return The mean loss value.
	 */
	static public Float getLoss(RColor rvMean, ArrayList<RColor> lColors, ColorDifferenceMeasure m) {
		return HSBColor.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 HSB 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 HSBColor.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 HSB 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 = 0f;

		HSBColor mean = rvMean.toHSB();

		for (int i = start; i < end; i++) {
			float diff = 0f;
			HSBColor c = aColors != null ? aColors[i].toHSB() : lColors.get(i).toHSB();

			if (m == ColorDifferenceMeasure.HSBCone) {
				diff = ColorDifference.hsbCone(mean, c);
			} else if (m == ColorDifferenceMeasure.HSBCylinder) {
				diff = ColorDifference.hsbCylinder(mean, c);
			} else {
				diff = ColorDifference.hsbEuclidean(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 HSBColor lerpColors(RColor input1, RColor input2, float amt) {
		amt = RMath.constrain(amt, 0, 1);

		HSBColor c1 = input1.toHSB();
		HSBColor c2 = input2.toHSB();

		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 HSBColor(h, RMath.map(amt, 0, 1, c1.getSaturation(), c2.getSaturation()),
				RMath.map(amt, 0, 1, c1.getBrightness(), c2.getBrightness()),
				RMath.map(amt, 0, 1, c1.getAlpha(), c2.getAlpha()));
	}

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

		return new HSBColor(RMath.random(360.0f), RMath.random(100.0f), RMath.random(100.0f));
	}

	/**
	 * Creates an opaque HSBColor by the given input value. Usage:
	 * 
	 * <pre>
	 * color c1 = color(255, 0, 0);
	 * color c2 = #FF0000;
	 * HSBColor hsb1 = HSBColor.toHSB(c1);
	 * HSBColor hsb2 = HSBColor.toHSB(c2);
	 * </pre>
	 * 
	 * @param hex
	 *            The color value.
	 * @return The color object.
	 */
	public static HSBColor toHSB(int hex) {
		return RGBColor.toHSB(new Float((hex >> 16) & 255), new Float((hex >> 8) & 255), new Float(hex & 255), 255f);
	}

	/**
	 * Creates HSBColor with transparency by the given input value. Usage:
	 * 
	 * <pre>
	 * color c1 = color(255, 0, 0, 120);
	 * color c2 = #FF0000;
	 * HSBColor hsb1 = HSBColor.toHSB(c1, alpha(c1) / 2.55);
	 * HSBColor hsb2 = HSBColor.toHSB(c2, 120);
	 * </pre>
	 * 
	 * @param hex
	 *            The color value.
	 * @param alpha
	 *            The transparency value.
	 * @return The color object.
	 */
	public static HSBColor toHSB(int hex, float alpha) {
		return RGBColor.toHSB(new Float((hex >> 16) & 255), new Float((hex >> 8) & 255), new Float(hex & 255), alpha);
	}

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

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

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

		int hex = Color.HSBtoRGB(channels[0] / 360, channels[1] / 100, channels[2] / 100);

		return RGBColor.toRGB(hex, 255 * this.getAlpha() / 100);
	}

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

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

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

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

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

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

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

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