package net.returnvoid.color;

import java.util.ArrayList;

import net.returnvoid.tools.RMath;

/*
 * 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 collection of static functions in relationship to RColor and
 * ColorDifference.
 * 
 * @author Diana Lange
 *
 */
public class ColorHelper {

	private ColorHelper() {
	}

	/**
	 * Creates a copy array of the given input.
	 * 
	 * @param colors
	 *            A set of colors.
	 * @return The (true) copy of the given set.
	 */
	static public RColor[] copy(RColor[] colors) {
		RColor[] copies = new RColor[colors.length];

		for (int i = 0; i < copies.length; i++) {
			copies[i] = colors[i].copy();
		}

		return copies;
	}

	/**
	 * Creates a copy array of the given input.
	 * 
	 * @param colors
	 *            A set of colors.
	 * @return The (true) copy of the given set.
	 */
	static public ArrayList<RColor> copy(ArrayList<RColor> colors) {
		ArrayList<RColor> copies = new ArrayList<RColor>(colors.size());

		for (int i = 0; i < colors.size(); i++) {
			copies.add(colors.get(i).copy());
		}

		return copies;
	}

	/**
	 * 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.
	 * @return The mean loss value.
	 */
	static public Float getLoss(RColor rvMean, RColor[] colors, ColorDifferenceMeasure m) {
		return ColorHelper.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.
	 * @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 ColorHelper.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 colors
	 *            Colors.
	 * @param m
	 *            The measure which defines how the loss is calculated and which
	 *            color model will be used.
	 * @return The mean loss value.
	 */
	static public Float getLoss(RColor rvMean, ArrayList<RColor> colors, ColorDifferenceMeasure m) {
		return ColorHelper.getLoss(rvMean, null, colors, m, 0, colors.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 colors
	 *            Colors.
	 * @param m
	 *            The measure which defines how the loss is calculated and which
	 *            color model will be used.
	 * @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> colors, ColorDifferenceMeasure m, int start, int end) {
		return ColorHelper.getLoss(rvMean, null, colors, 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.
	 * @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) {

		ColorSpace space = m.toColorSpace();

		float loss = 0;
		switch (space) {
		case HSBColor:
			if (aColors != null) {
				loss = HSBColor.getLoss(rvMean, aColors, m, start, end);
			} else {
				loss = HSBColor.getLoss(rvMean, lColors, m, start, end);
			}
			break;
		case LabColor:
			if (aColors != null) {
				loss = LabColor.getLoss(rvMean, aColors, m, start, end);
			} else {
				loss = LabColor.getLoss(rvMean, lColors, m, start, end);
			}
			break;
		case LCHColor:
			if (aColors != null) {
				loss = LCHColor.getLoss(rvMean, aColors, m, start, end);
			} else {
				loss = LCHColor.getLoss(rvMean, lColors, m, start, end);
			}
			break;

		case XYZColor:
			if (aColors != null) {
				loss = XYZColor.getLoss(rvMean, aColors, m, start, end);
			} else {
				loss = XYZColor.getLoss(rvMean, lColors, m, start, end);
			}
			break;

		default:
			if (aColors != null) {
				loss = RGBColor.getLoss(rvMean, aColors, m, start, end);
			} else {
				loss = RGBColor.getLoss(rvMean, lColors, m, start, end);
			}
			break;
		}

		return loss;
	}

	/**
	 * Returns the mean color of the given set <b>colors</b> using the color
	 * space defined by the ColorDifferenceMeasure <b>m</b>, e.g RGBEuclidean is
	 * mapped to RGBColor.
	 * 
	 * @param m
	 *            A ColorDifferenceMeasure.
	 * @param colors
	 *            A set of colors (not null, one or more colors).
	 * @return A mean color (member of <b>space</b>).
	 */
	static public RColor getMeanColor(ColorDifferenceMeasure m, RColor... colors) {
		return ColorHelper.getMeanColor(colors, null, m.toColorSpace());
	}

	/**
	 * Returns the mean color of the given set <b>colors</b> using the color
	 * space defined by the ColorDifferenceMeasure <b>m</b>, e.g RGBEuclidean is
	 * mapped to RGBColor.
	 * 
	 * @param colors
	 *            A set of colors (not null, one or more colors).
	 * @param m
	 *            A ColorDifferenceMeasure.
	 * @return A mean color (member of <b>space</b>).
	 */
	static public RColor getMeanColor(RColor[] colors, ColorDifferenceMeasure m) {
		return ColorHelper.getMeanColor(colors, null, m.toColorSpace());
	}

	/**
	 * Returns the mean color of the given set <b>colors</b> using the color
	 * space defined by the ColorDifferenceMeasure <b>m</b>, e.g RGBEuclidean is
	 * mapped to RGBColor.
	 * 
	 * @param colors
	 *            A set of colors (not null, one or more colors).
	 * @param m
	 *            A ColorDifferenceMeasure.
	 * @return A mean color (member of <b>space</b>).
	 */
	static public RColor getMeanColor(ArrayList<RColor> colors, ColorDifferenceMeasure m) {
		return ColorHelper.getMeanColor(null, colors, m.toColorSpace());
	}

	/**
	 * Returns the mean color of the given set <b>colors</b> using the color
	 * space defined by <b>space</b>.
	 * 
	 * @param colors
	 *            A set of colors (not null, one or more colors).
	 * @param space
	 *            A color space.
	 * @return A mean color (member of <b>space</b>).
	 */
	static public RColor getMeanColor(RColor[] colors, ColorSpace space) {

		return ColorHelper.getMeanColor(colors, null, space);
	}

	/**
	 * Returns the mean color of the given set <b>colors</b> using the color
	 * space defined by <b>space</b>.
	 * 
	 * @param colors
	 *            A set of colors (not null, one or more colors).
	 * @param space
	 *            A color space.
	 * @return A mean color (member of <b>space</b>).
	 */
	static public RColor getMeanColor(ArrayList<RColor> colors, ColorSpace space) {

		return ColorHelper.getMeanColor(null, colors, space);
	}

	/**
	 * Returns the mean color of the given set <b>colors</b> using the color
	 * space defined by <b>space</b>. Uses just aColors OR lColors. Not both.
	 * One of aColors / lColors should be null, the other one should be not
	 * null.
	 * 
	 * @param aColors
	 *            A set of colors or null. * @param lColors A set of colors or
	 *            null.
	 * @param space
	 *            A color space.
	 * @return A mean color (member of <b>space</b>).
	 */
	static private RColor getMeanColor(RColor[] aColors, ArrayList<RColor> lColors, ColorSpace space) {

		RColor mean = null;
		switch (space) {
		case HSBColor:
			if (aColors != null) {
				mean = HSBColor.getMeanColor(aColors);
			} else {
				mean = HSBColor.getMeanColor(lColors);
			}
			break;
		case LabColor:
			if (aColors != null) {
				mean = LabColor.getMeanColor(aColors);
			} else {
				mean = LabColor.getMeanColor(lColors);
			}

			break;
		case LCHColor:
			if (aColors != null) {
				mean = LCHColor.getMeanColor(aColors);
			} else {
				mean = LCHColor.getMeanColor(lColors);
			}
			break;
		case XYZColor:
			if (aColors != null) {
				mean = XYZColor.getMeanColor(aColors);
			} else {
				mean = XYZColor.getMeanColor(lColors);
			}
			break;
		default:
			if (aColors != null) {
				mean = RGBColor.getMeanColor(aColors);
			} else {
				mean = RGBColor.getMeanColor(lColors);
			}
			break;
		}
		return mean;
	}

	/**
	 * 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. Uses the lerp function of the class of c1 (e.g.
	 * of c1 is a RGBColor, RGBColor.lerpColors() will be executed).
	 * 
	 * @param c1
	 *            The first color.
	 * @param c2
	 *            The second color.
	 * @param amt
	 *            The lerping value [0, 1].
	 * @return A new 'mixed' color.
	 */
	public static RColor lerpColors(RColor c1, RColor c2, float amt) {

		ColorSpace target = c1.getColorSpace();
		RColor newC;

		switch (target) {
		case HSBColor:
			newC = HSBColor.lerpColors(c1, c2, amt);
			break;
		case LabColor:
			newC = LabColor.lerpColors(c1, c2, amt);
			break;
		case LCHColor:
			newC = LCHColor.lerpColors(c1, c2, amt);
			break;
		case XYZColor:
			newC = XYZColor.lerpColors(c1, c2, amt);
			break;

		default:
			newC = RGBColor.lerpColors(c1, c2, amt);
			break;
		}

		return newC;
	}

	/**
	 * Converts the input color c into the given target color space.
	 * 
	 * @param c
	 *            The color which should be converted.
	 * @param target
	 *            The color space to convert to.
	 * @return The converted color.
	 */
	public static RColor convert(RColor c, ColorSpace target) {

		RColor newC;

		switch (target) {
		case HSBColor:
			newC = c.toHSB();
			break;
		case LabColor:
			newC = c.toLab();
			break;
		case LCHColor:
			newC = c.toLCH();
			break;
		case XYZColor:
			newC = c.toXYZ();
			break;

		default:
			newC = c.toRGB();
			break;
		}

		return newC;
	}

	/**
	 * Converts the input color c into a appropriate color model defined by the
	 * ColorDifferenceMeasure. E.G. if the ColorDifferenceMeasure is
	 * RGBEuclidean distance the resulting type of the output color will be
	 * RGBColor. If the color is already member of the right color space, the
	 * function will return the input color.
	 * 
	 * @param c
	 *            The color which should be converted.
	 * @param m
	 *            The color difference measure.
	 * @return The converted color.
	 */
	public static RColor convert(RColor c, ColorDifferenceMeasure m) {
		return ColorHelper.convert(c, m.toColorSpace());
	}

	/**
	 * Converts the input colors c into the given color space. Uses just cA OR
	 * cL. Not both. One of aColors / lColors should be null, the other one
	 * should
	 * 
	 * @param colors
	 *            The colors which should be converted.
	 * @param target
	 *            The target color space.
	 * @return The converted colors.
	 */
	public static void convert(RColor[] colors, ColorSpace target) {
		ColorHelper.convert(colors, null, target);
	}

	/**
	 * Converts the input colors c into the given color space. Uses just cA OR
	 * cL. Not both. One of aColors / lColors should be null, the other one
	 * should
	 * 
	 * @param colors
	 *            The colors which should be converted.
	 * @param target
	 *            The target color space.
	 * @return The converted colors.
	 */
	public static void convert(ArrayList<RColor> colors, ColorSpace target) {
		ColorHelper.convert(null, colors, target);
	}

	/**
	 * Converts the input colors c into the given color space. Uses just cA OR
	 * cL. Not both. One of aColors / lColors should be null, the other one
	 * should
	 * 
	 * @param cA
	 *            The colors which should be converted.
	 * @param cL
	 *            The colors which should be converted.
	 * @param m
	 *            The target color space.
	 * @return The converted colors.
	 */
	private static void convert(RColor[] cA, ArrayList<RColor> cL, ColorSpace target) {

		int max = cA != null ? cA.length : cL.size();

		for (int i = 0; i < max; i++) {
			RColor converted = ColorHelper.convert(cA != null ? cA[i] : cL.get(i), target);
			if (cA != null) {
				cA[i] = converted;
			} else {
				cL.set(i, converted);
			}
		}
	}

	/**
	 * Checks if ref is member of colors.
	 * 
	 * @param ref
	 *            A color or null.
	 * @param colors
	 *            Colors (Not null, members might be null)
	 * @return True if ref is member of the given set. Otherwise false.
	 */
	static public boolean contains(RColor ref, RColor[] colors) {

		return ColorHelper.contains(ref, colors, null);
	}

	/**
	 * Checks if ref is member of colors.
	 * 
	 * @param ref
	 *            A color or null.
	 * @param colors
	 *            Colors (Not null, members might be null)
	 * @return True if ref is member of the given set. Otherwise false.
	 */
	static public boolean contains(RColor ref, ArrayList<RColor> colors) {

		return ColorHelper.contains(ref, null, colors);
	}

	/**
	 * Checks if ref is member of aColors / lColors. Uses just aColors OR
	 * lColors. Not both. One of aColors / lColors should be null, the other one
	 * should be not null.
	 * 
	 * @param ref
	 *            A color or null.
	 * @param aColors
	 *            Colors (Members might be null)
	 * @param lColors
	 *            Colors (Members might be null)
	 * @return True if ref is member of the given set. Otherwise false.
	 */
	static private boolean contains(RColor ref, RColor[] aColors, ArrayList<RColor> lColors) {

		if (ref == null) {
			return false;
		}

		int max = aColors != null ? aColors.length : lColors.size();
		for (int i = 0; i < max; i++) {
			RColor tmp = aColors != null ? aColors[i] : lColors.get(i);
			if (tmp == null) {
				continue;
			}

			if (ref.equals(tmp)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Creates a random opaque color.
	 * 
	 * @return The int color representation of the random color.
	 */
	static public int getRandomColor() {
		return 255 << 24 | RMath.random(0, 256) << 16 | RMath.random(0, 256) << 8 | RMath.random(0, 256);
	}

	/**
	 * Converts a String, containing a hexadecimal number, to a color. The
	 * String should be in form of "FF0000" or "#FF0000" or "AAFF0000" or
	 * "#AAFF0000" (first two characters for transparency).
	 * 
	 * @param hex
	 *            A string containing a hexadecimal number.
	 * @return The int color representation of the color of the given String.
	 */
	static public int getColor(String hex) {
		hex = hex.replaceAll("[^0-9a-fA-F]", "");

		String rgb;
		String alpha;

		if (hex.length() == 8) {
			rgb = hex.substring(hex.length() - 6, hex.length());
			alpha = hex.substring(hex.length() - 8, hex.length() - 6);
		} else if (hex.length() == 6) {
			rgb = hex;
			alpha = "FF";
		} else {
			System.err.println(hex + " is no valid hexadecimal color");
			return 0xFF000000;
		}

		return Integer.parseInt(alpha, 16) << 24 | Integer.parseInt(rgb, 16);
	}

	/**
	 * Converts input values to a color.
	 * 
	 * @param paramters
	 *            Either [gray], [gray, alpha] or [r, g, b] or [r, g, b, alpha].
	 * @return The int color representation of the input.
	 */
	static public int getColor(float... paramters) {
		float r;
		float g;
		float b;
		float a;

		if (paramters.length == 0) {
			r = g = b = 0f;
			a = 255f;
		} else if (paramters.length == 1) {
			r = g = b = paramters[0];
			a = 255f;
		} else if (paramters.length == 2) {
			r = g = b = paramters[0];
			a = paramters[1];
		} else if (paramters.length == 3) {
			r = paramters[0];
			g = paramters[1];
			b = paramters[2];
			a = 255f;
		} else {
			r = paramters[0];
			g = paramters[1];
			b = paramters[2];
			a = paramters[3];
		}

		int intA = RMath.constrain((int) a, 0, 255);
		int intR = RMath.constrain((int) r, 0, 255);
		int intG = RMath.constrain((int) g, 0, 255);
		int intB = RMath.constrain((int) b, 0, 255);

		return intA << 24 | intR << 16 | intG << 8 | intB;
	}
}
