package net.returnvoid.analytics;

import net.returnvoid.color.ColorDifferenceMeasure;
import net.returnvoid.color.ColorHelper;
import net.returnvoid.color.ColorPalette;
import net.returnvoid.color.ColorSpace;
import net.returnvoid.color.RColor;
import net.returnvoid.io.ImageUpdateObserver;
import net.returnvoid.tools.ProcessingTools;
import net.returnvoid.tools.RMath;
import net.returnvoid.tools.StringTools;
import processing.core.PApplet;
import processing.core.PGraphics;
import processing.core.PImage;

/*
 * 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 class to build multiple color clusterings with the same trainings data but
 * with different number of clusters. Finds a optimal k (number of clusters)
 * within the given range of minK and maxK.
 * 
 * @author Diana Lange
 *
 */
public class ColorClusteringBuilder implements ImageUpdateObserver {

	/**
	 * The PApplet object.
	 */
	private final PApplet parent;

	/**
	 * The source for the clusterings (colors will be sampled from this image).
	 * The clusterings can also be built from a given set of colors. In that
	 * case no inputImage is provided and this image will be null.
	 */
	private PImage inputImg;

	/**
	 * The source for the clusterings (colors will be sampled from this
	 * inputColors). The clusterings can also be built from a given image. In
	 * that case no inputColors are provided and this array will be null.
	 */
	private RColor[] inputColors;

	/**
	 * The colors that are sampled from inputImage or inputColors.
	 * trainingColors.length <= trainingSize.
	 */
	private RColor[] trainingColors;

	/**
	 * The clusterings from k=minK to k=maxK.
	 */
	private ColorClustering[] clusterings;

	/**
	 * The quantized images for each clustering.
	 */
	private PImage[] quantizes;

	/**
	 * The number of clusters for the smallest clustering.
	 */
	private int minK = 2;

	/**
	 * The number of clusters for the biggest clustering.
	 */
	private int maxK = 30;

	/**
	 * The optimal number of clusters for the given data.
	 */
	private int optimalK = -1;

	/**
	 * The number of colors which will be used to learn the clusterings. When
	 * inputColors are used and there are less inputColors than trainingSize,
	 * then all inputColors are used for training.
	 */
	private int trainingSize = 420;

	/**
	 * The ColorDifferenceMeasure which defines the distance measure for the
	 * clustering process.
	 */
	private ColorDifferenceMeasure m;

	/**
	 * Builds a new ColorClusteringBuilder with minK=2, maxK=30 and RGBEuclidean
	 * as ColorDifferenceMeasure. The clustering will be done after either
	 * setImage() or setColors() is called.
	 * 
	 * @param parent
	 *            The PApplet object.
	 */
	public ColorClusteringBuilder(PApplet parent) {
		this(parent, ColorDifferenceMeasure.RGBEuclidean);
	}

	/**
	 * Builds a new ColorClusteringBuilder with minK=2, maxK=30 and <b>m</b> as
	 * ColorDifferenceMeasure. The clustering will be done after either
	 * setImage() or setColors() is called.
	 * 
	 * @param parent
	 *            The PApplet object.
	 */
	public ColorClusteringBuilder(PApplet parent, ColorDifferenceMeasure m) {
		this.m = m;
		this.parent = parent;
		this.inputImg = null;
		this.inputColors = null;

		this.clusterings = new ColorClustering[0];
		this.trainingColors = new RColor[0];
		this.quantizes = new PImage[0];
	}

	/**
	 * Sets the number of colors which will be used to learn the clusterings.
	 * When <b>inputColors</b> are used and there are less <b>inputColors</b>
	 * than <b>trainingSize</b>, then all inputColors are used for training.
	 * 
	 * @param trainingSize
	 *            The number of colors used for learning the clusterings.
	 * @return The current builder object.
	 */
	public ColorClusteringBuilder setTrainingSize(int trainingSize) {

		if (trainingSize > maxK) {
			this.trainingSize = trainingSize;
		}

		return this;
	}

	/**
	 * Sets the maximal number of clusters for any clustering of this builder.
	 * 
	 * @param k
	 *            The maximal number of clusters.
	 * @return The current builder object.
	 */
	public ColorClusteringBuilder setMaxK(int k) {

		if (k > minK) {
			maxK = k;

			if (maxK >= trainingSize) {
				trainingSize = maxK + 1;
			}

		}

		return this;
	}

	/**
	 * Returns the number of clusterings for this builder. Use this to iterate
	 * over all clusterings. Example:<br>
	 * 
	 * <pre>
	 * for (int i = 0; i < myBuilder.size(); i++) {
	 * 	ColorClustering clustering = myBuilder.get(i);
	 * }
	 * </pre>
	 * 
	 * @return The number of clusterings.
	 */
	public int size() {
		return this.clusterings.length;
	}

	/**
	 * Returns the clustering with the given index (the index doesn't equal to k
	 * - the number of clusters). Use this to iterate over all clusterings of
	 * this builder. Example:<br>
	 * 
	 * <pre>
	 * for (int i = 0; i < myBuilder.size(); i++) {
	 * 	ColorClustering clustering = myBuilder.get(i);
	 * }
	 * </pre>
	 * 
	 * @param index
	 *            The index of the clustering [0, size()).
	 * @return The clusering.
	 */
	public ColorClustering get(int index) {
		return this.clusterings[index];
	}

	/**
	 * Returns the clustering with optimal number of clusters for the given
	 * data.
	 * 
	 * @return The clustering.
	 */
	public ColorClustering getClustering() {

		return getClustering(this.getOptimalK());
	}

	/**
	 * Returns the clustering with <b>k</b> number of clusters. Make sure
	 * <b>k</b> is in range of [minK, maxK]. Example usage: <br>
	 * 
	 * <pre>
	 * for (int k = myBuilder.getMinK(); k <= myBuilder.getMaxK(); k++) {
	 * 	myBuilder.getClustering(k);
	 * }
	 * </pre>
	 * 
	 * @param k
	 *            The number of clusters which maps to the desired clustering.
	 * 
	 * @return The clustering with k clusters.
	 */
	public ColorClustering getClustering(int k) {

		if (clusterings.length == 0 || k - minK >= clusterings.length) {
			return null;
		}

		return this.clusterings[k - minK];
	}

	/**
	 * Returns the ColorDifferenceMeasure which defines the distance measure for
	 * the clustering process.
	 * 
	 * @return The ColorDifferenceMeasure.
	 */
	public ColorDifferenceMeasure getColorDifferenceMeasure() {
		return m;
	}

	/**
	 * Returns the source for the clusterings (colors will be sampled from this
	 * image) or null if no image is set.
	 * 
	 * @return The input image or null.
	 */
	public PImage getInputImg() {

		return inputImg;
	}

	/**
	 * Returns the source for the clusterings - the training colors.
	 * 
	 * @return The set of colors which is used for the clusterings.
	 */
	public RColor[] getColors() {
		if (this.inputColors != null) {
			return this.inputColors;
		} else {
			return this.getClustering(minK).getMembers();
		}
	}

	/**
	 * Returns the maximal number of clusters for any clustering of this
	 * builder.
	 * 
	 * @return The maximal number of clusters.
	 */
	public int getMaxK() {
		return maxK;
	}

	/**
	 * Returns the minimal number of clusters for any clustering of this
	 * builder.
	 * 
	 * @return The minimal number of clusters.
	 */
	public int getMinK() {
		return minK;
	}

	/**
	 * Calculates the optimal k for the given input data (optimal number of
	 * clusters that represent the input).
	 * 
	 * @return The optimal k.
	 */
	public int getOptimalK() {
		if (clusterings.length == 0) {
			return -1;
		}

		if (optimalK == -1) {
			float[] losses = ClusteringHelper.getLosses(clusterings);

			// minimal loss should be at k=2 and maximal loss should be at maxK
			// but not all loss graphs are descent constantly therefor the
			// true range must be calculated by finding the true min and max
			float[] lossRang = RMath.range(losses);
			float maxLoss = lossRang[1] - lossRang[0];
			for (int i = 0; i < losses.length; i++) {

				// loss improvement to k-1 clustering
				float diff = i == 0 ? maxLoss : losses[i] - losses[i - 1];

				// loss improvement percentage
				float pDiff = 100 * diff / maxLoss;

				// if improvement is small check the improvements for the next
				// two k. If there is no great improvement in these two steps,
				// the optimal k is found.
				if (RMath.abs(pDiff) <= 1 && (pDiff < 0 || pDiff < 0.001)) {

					float nextP = i == losses.length - 1 ? 0 : 100 * (losses[i + 1] - losses[i]) / maxLoss;
					float nextnextP = i >= losses.length - 2 ? 0 : 100 * (losses[i + 2] - losses[i + 1]) / maxLoss;

					// println("\n" + (i + minClusters) + ", " +
					// RMath.abs(pDiff));
					// println("next: " + nextP + ", " + RMath.abs(nextP));
					// println("nextnext: " + nextnextP + ", " +
					// RMath.abs(nextnextP));
					if (RMath.abs(nextP) < 2 && RMath.abs(nextnextP) < 2
							&& RMath.abs(nextP) + RMath.abs(nextnextP) <= 3.0 && (nextP < 0 || nextP <= 1)
							&& (nextnextP < 0 || nextnextP < 1.0)) {
						// println("optimal");
						optimalK = i + minK;
						break;
					}
				}
			}

			// no good value is found for k. Then maxK is optimal k.
			if (optimalK == -1) {
				optimalK = maxK;
			}
		}

		return optimalK;
	}

	/**
	 * Sets the source for the clusterings (colors will be sampled from this
	 * image). The clusterings will start after this method has been called.
	 * 
	 * @param inputImg
	 *            The source for the clusterings.
	 */
	@Override
	public void setImage(PImage inputImg) {
		// System.out.println("got image");
		this.inputImg = inputImg;
		this.inputColors = null;
		this.update();
	}

	/**
	 * Sets the source for the clusterings (colors will be sampled from this
	 * colors). The clusterings will start after this method has been called.
	 * 
	 * @param inputColors
	 *            The source for the clusterings.
	 */
	public void setColors(RColor[] inputColors) {
		this.inputImg = null;
		this.inputColors = inputColors;
		this.update();
	}

	/**
	 * Creates a quantized image to the given <b>inputImage</b> image. The
	 * quantized image will only consists of colors which are found as a cluster
	 * mean in this clustering with optimal k cluster. If the quantized image
	 * with full resolution was already calculated this function will return the
	 * image with the full resolution. Otherwise it will return a version with
	 * reduced resolution (with long side = 600px).
	 * 
	 * @param k
	 *            The number of clusters / colors which will be used to build
	 *            the quantized image. Make sure <b> is within range of [minK,
	 *            maxK].
	 * @return The quantized image.
	 */
	public PImage getQuantizedImage() {
		return getQuantizedImage(getOptimalK());
	}

	/**
	 * Creates a quantized image to the given <b>inputImage</b> image. The
	 * quantized image will only consists of colors which are found as a cluster
	 * mean in this clustering with the given k. If the quantized image with
	 * full resolution was already calculated this function will return the
	 * image with the full resolution. Otherwise it will return a version with
	 * reduced resolution (with long side = 600px).
	 * 
	 * @param k
	 *            The number of clusters / colors which will be used to build
	 *            the quantized image. Make sure <b> is within range of [minK,
	 *            maxK].
	 * @return The quantized image.
	 */
	public PImage getQuantizedImage(int k) {
		if (this.quantizes[k - minK] == null) {
			return getQuantizedImagePreview(k);
		} else {
			return this.quantizes[k - minK];
		}
	}

	/**
	 * Creates a quantized image to the given <b>inputImg</b> image (source of
	 * clustering). The quantized image will only consists of colors which are
	 * found as cluster mean in the clustering with optimal k clusters. Returns
	 * null if no <b>inputImg</b> is provided. In that case use
	 * getQuantizedImageFull(PImage) instead.
	 * 
	 * @return The quantized image (same resolution as <b>inputImg</b>) or null.
	 */
	public PImage getQuantizedImageFull() {
		return getQuantizedImageFull(getOptimalK());
	}

	/**
	 * Creates a quantized image to the given <b>inputImg</b> image (source of
	 * clustering). The quantized image will only consists of colors which are
	 * found as cluster mean in the clustering with <b>k</b> clusters. Returns
	 * null if no <b>inputImg</b> is provided. In that case use
	 * getQuantizedImageFull(int, PImage) instead.
	 * 
	 * @param k
	 *            The number of clusters / colors which will be used to build
	 *            the quantized image. Make sure <b> is within range of [minK,
	 *            maxK].
	 * 
	 * @return The quantized image (same resolution as <b>inputImg</b>) or null.
	 */
	public PImage getQuantizedImageFull(int k) {
		if (this.inputImg == null || this.clusterings.length == 0 || k - minK >= this.clusterings.length) {
			return null;
		}

		if (this.quantizes[k - minK] == null || this.quantizes[k - minK].width < this.inputImg.width) {
			PImage copy = this.inputImg.copy();

			this.quantizes[k - minK] = copy;
			ColorPalette palette = clusterings[k - minK].toColorPalette(false);
			palette.get(copy);
		}

		return this.quantizes[k - minK];
	}

	/**
	 * Creates a quantized image to the given <b>inputImg</b> image (source of
	 * clustering). The quantized image will only consists of colors which are
	 * found as cluster mean in the clustering with optimal k clusters. Returns
	 * null if no <b>inputImg</b> is provided. In that case use
	 * getQuantizedImagePreview(PImage) instead.
	 * 
	 * @return The quantized image (with max. 600x600px resolution) or null.
	 */
	public PImage getQuantizedImagePreview() {
		return getQuantizedImagePreview(getOptimalK());
	}

	/**
	 * Creates a quantized image to the given <b>inputImg</b> image (source of
	 * clustering). The quantized image will only consists of colors which are
	 * found as cluster mean in the clustering with <b>k</b> clusters. Returns
	 * null if no <b>inputImg</b> is provided. In that case use
	 * getQuantizedImagePreview(int, PImage) instead.
	 * 
	 * @param k
	 *            The number of clusters / colors which will be used to build
	 *            the quantized image. Make sure <b> is within range of [minK,
	 *            maxK].
	 * 
	 * @return The quantized image (with max. 600x600px resolution) or null.
	 */
	public PImage getQuantizedImagePreview(int k) {
		if (this.inputImg == null || this.clusterings.length == 0 || k - minK >= this.clusterings.length) {
			return null;
		}

		if (this.quantizes[k - minK] == null) {
			PImage copy = this.inputImg.copy();

			if (copy.width > copy.height) {
				copy.resize(600, 0);
			} else {
				copy.resize(0, 600);
			}

			this.quantizes[k - minK] = copy;
			ColorPalette palette = clusterings[k - minK].toColorPalette(false);
			palette.get(copy);
		}

		return this.quantizes[k - minK];
	}

	/**
	 * Creates new clusterings for the given range of k. Samples new training
	 * data as well.
	 */
	public void update() {
		int oldRange = clusterings.length;
		optimalK = -1;
		// img = loader.nextRandom();
		this.trainingColors = getTrainingColors();
		// System.out.println("got training colors");
		this.clusterings = ClusteringHelper.kmeansClusterings(trainingColors, minK, maxK, m);

		if (oldRange != clusterings.length) {
			this.quantizes = new PImage[clusterings.length];
		} else {
			for (int i = 0; i < quantizes.length; i++) {
				this.quantizes[i] = null;
			}
		}
	}

	/**
	 * Sets a new ColorDifferenceMeasure. The training colors will be converted
	 * to the color space defined by that measure. All clusterings will be
	 * updated.
	 * 
	 * @param m
	 *            The new ColorDifferenceMeasure.
	 */
	public void setColorDifferenceMeasure(ColorDifferenceMeasure m) {
		// println(m);
		if (!this.m.equals(m)) {
			ColorSpace oldSpace = this.m.toColorSpace();
			ColorSpace newSpace = m.toColorSpace();
			optimalK = -1;
			this.m = m;

			if (!oldSpace.equals(newSpace)) {
				for (RColor c : trainingColors) {
					c = ColorHelper.convert(c, newSpace);
				}
			}

			for (int i = 0; i < quantizes.length; i++) {
				this.quantizes[i] = null;
			}
			// println("new clustering");
			this.clusterings = ClusteringHelper.kmeansClusterings(trainingColors, minK, maxK, m);
		}
	}

	/**
	 * Calculates and returns the sample colors either from <b>inputImg</b> or
	 * <b>inputColors</b> (Depends on which of the two is not null).
	 * 
	 * @return The training colors (number of colors <= trainingSize).
	 */
	private RColor[] getTrainingColors() {
		if (inputImg == null && inputColors == null) {
			System.err.println(
					"You can't create any clusters without data. Please call setImage(PImage) or setColors(RColor[]) before trying to cluster.");
			return null;
		}

		if (inputImg != null) {
			return ClusteringHelper.getTrainingColors(inputImg, trainingSize, m, 1);
		} else {
			if (inputColors[0].getColorSpace() == this.m.toColorSpace() && inputColors.length <= trainingSize) {
				return this.inputColors;
			} else {
				return ClusteringHelper.getTrainingColors(inputColors, trainingSize, m);
			}
		}
	}

	/**
	 * Exports the current clustering with <b>k</b> clusters with 6 sample
	 * representations. These representations are: <br>
	 * (1) Quantized image. <br>
	 * (2) Loss graph. <br>
	 * (3) Clustering with k clusters.<br>
	 * (4) Input image with clustered colors as overlay.<br>
	 * (5) A graph with all clusterings from minK to maxK.<br>
	 * (6) The training data (sampled colors).<br>
	 * <br>
	 * The images will be exported in a folder named "export" in the current
	 * sketch folder.
	 * 
	 * @param k
	 *            The number of clusters / colors which will be used to build
	 *            the quantized image. Make sure <b> is within range of [minK,
	 *            maxK].
	 * @param fileName
	 *            The (base) name of the images which will be exported.
	 */
	public void export(int k, String fileName) {
		if (clusterings.length == 0) {
			System.err.println("no clustering done yet");
			return;
		}

		String path = ProcessingTools.getSketchPath(parent) + "/export/";
		String time = StringTools.timestamp();
		String fileAndMeasure = fileName + "_" + m.name();

		System.out.println("exporting 1 / 6");
		if (inputImg != null) {

			PImage img;
			if (this.quantizes[k - minK] != null && this.quantizes[k - minK].width == this.inputImg.width) {
				img = this.quantizes[k - minK];
			} else {
				ColorPalette palette = this.clusterings[k - minK].toColorPalette(false);
				img = this.inputImg.copy();
				this.quantizes[k - minK] = img;
				String stamp = palette.get(img);

				// block code until image is finished
				while (!palette.finishedImage(stamp)) {
				}

			}

			img.save(path + fileAndMeasure + "_" + time + "_quantization_k" + k + ".png");
		}
		System.out.println("exporting 2 / 6");
		parent.g.beginDraw();
		parent.g.background(247);
		drawLoss(parent.g, 0, 0, parent.width, parent.height, -1);
		parent.g.endDraw();
		parent.g.save(path + fileAndMeasure + "_" + time + "_loss.png");

		System.out.println("exporting 3 / 6");
		parent.g.beginDraw();
		parent.g.background(247);
		drawClustering(parent.g, 0, 0, parent.width, parent.height, k);
		parent.g.endDraw();
		parent.g.save(path + fileAndMeasure + "_" + time + "_clustering_k" + k + ".png");

		System.out.println("exporting 4 / 6");
		if (inputImg != null) {
			parent.g.beginDraw();
			parent.g.background(247);
			drawImageWithClustering(parent.g, 0, 0, parent.width, parent.height, k);
			parent.g.endDraw();
			parent.g.save(path + fileAndMeasure + "_" + time + "_imgAndClustering_k" + k + ".png");
		}

		System.out.println("exporting 5 / 6");
		parent.g.beginDraw();
		parent.g.background(247);
		drawAllClusteringsMeans(parent.g, 0, 0, parent.width, parent.height);
		parent.g.endDraw();
		parent.g.save(path + fileAndMeasure + "_" + time + "_clustering_all.png");

		System.out.println("exporting 6 / 6");
		parent.g.beginDraw();
		parent.g.background(247);
		drawInputColors(parent.g, 0, 0, parent.width, parent.height);
		parent.g.endDraw();
		parent.g.save(path + fileAndMeasure + "_" + time + "_inputcolors.png");

		System.out.println("done exporting");
	}

	/*
	 * ------------------------------------------------------------------------
	 * | Drawing Functions |
	 * ------------------------------------------------------------------------
	 */

	/**
	 * Draws the quantized image for the given <b>k</b> centered in the given
	 * rectangular area. If no inputImage is provided no image will be drawn.
	 * 
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 * @param k
	 *            The number of clusters / colors which will be used to build
	 *            the quantized image. Make sure <b> is within range of [minK,
	 *            maxK].
	 */
	public void drawQuantizedImage(float x, float y, float w, float h, int k) {
		drawQuantizedImage(null, x, y, w, h, k);
	}

	/**
	 * Draws the quantized image for the given <b>k</b> centered in the given
	 * rectangular area. If no inputImage is provided no image will be drawn.
	 * 
	 * @param g
	 *            A recorder.
	 * 
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 * @param k
	 *            The number of clusters / colors which will be used to build
	 *            the quantized image. Make sure <b> is within range of [minK,
	 *            maxK].
	 */
	public void drawQuantizedImage(PGraphics g, float x, float y, float w, float h, int k) {
		if (inputImg == null || clusterings.length == 0) {
			return;
		}

		float amtW = (float) w / inputImg.width;
		float amtH = (float) h / inputImg.height;
		float amt = RMath.max(amtW, amtH);

		float iW = inputImg.width * amt;
		float iH = inputImg.height * amt;
		float iX = x - RMath.abs(iW - w) * 0.5f;
		float iY = y - RMath.abs(iH - h) * 0.5f;

		if (g == null) {
			parent.image(getQuantizedImage(k), iX, iY, iW, iH);

		} else {

			g.image(getQuantizedImage(k), iX, iY, iW, iH);

		}

	}

	/**
	 * Draws the input image centered in the given rectangular area. If no
	 * inputImage is provided no image will be drawn.
	 * 
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 */
	public void drawImage(float x, float y, float w, float h) {
		drawImage(null, x, y, w, h);
	}

	/**
	 * Draws the input image centered in the given rectangular area. If no
	 * inputImage is provided no image will be drawn.
	 * 
	 * @param g
	 *            A recorder.
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 */
	public void drawImage(PGraphics g, float x, float y, float w, float h) {
		if (inputImg == null) {
			return;
		}

		float amtW = (float) w / inputImg.width;
		float amtH = (float) h / inputImg.height;
		float amt = RMath.max(amtW, amtH);

		float iW = inputImg.width * amt;
		float iH = inputImg.height * amt;
		float iX = x - RMath.abs(iW - w) * 0.5f;
		float iY = y - RMath.abs(iH - h) * 0.5f;

		if (g == null) {
			parent.image(inputImg, iX, iY, iW, iH);
		} else {
			g.image(inputImg, iX, iY, iW, iH);
		}
	}

	/**
	 * Draws the the loss graph for minK to maxK within a rectangular area.
	 * 
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 */
	public void drawLoss(float x, float y, float w, float h) {
		drawLoss(null, x, y, w, h, -1);
	}

	/**
	 * Draws the the loss graph for minK to maxK within a rectangular area.
	 * 
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 * @param highlight
	 *            The k which defines which loss will be highlighted.
	 *            <b>highlight</b> should be in range of [minK, maxK] or out of
	 *            range if no loss should be highlighted.
	 */
	public void drawLoss(float x, float y, float w, float h, int highlight) {
		drawLoss(null, x, y, w, h, highlight);
	}

	/**
	 * Draws the the loss graph for minK to maxK within a rectangular area.
	 * 
	 * @param g
	 *            A recorder.
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 * @param highlight
	 *            The k which defines which loss will be highlighted.
	 *            <b>highlight</b> should be in range of [minK, maxK] or out of
	 *            range if no loss should be highlighted.
	 */
	public void drawLoss(PGraphics g, float x, float y, float w, float h, int highlight) {
		if (clusterings.length < 1 + maxK - minK) {
			return;
		}

		if (g == null) {
			parent.fill(227);
			parent.noStroke();
			parent.rect(x, y, w, h);
		} else {
			g.fill(227);
			g.noStroke();
			g.rect(x, y, w, h);
		}

		float cw = w / clusterings.length;
		float fontSize = cw * 0.3f;
		float textPadd = cw * 0.5f;

		if (g == null) {
			parent.textSize(fontSize);
			parent.textAlign(PApplet.CENTER, PApplet.CENTER);
		} else {
			g.textSize(fontSize);
			g.textAlign(PApplet.CENTER, PApplet.CENTER);
		}

		for (int i = 0; i < clusterings.length; i++) {
			float cx = x + cw * i;

			if (i + minK == highlight) {

				if (g == null) {
					parent.fill(20, 80, 100, 80);
					parent.rect(cx - 0.5f, y, cw + 1, h - textPadd);
				} else {
					g.fill(200);
					g.rect(cx - 0.5f, y, cw + 1, h - textPadd);
				}

			} else if (i % 2 == 0) {
				if (g == null) {
					parent.fill(200);
					parent.rect(cx - 0.5f, y, cw + 1, h - textPadd);
				} else {
					g.fill(200);
					g.rect(cx - 0.5f, y, cw + 1, h - textPadd);
				}
			}

			if (g == null) {
				parent.fill(20);
				parent.text("k=" + clusterings[i].getK(), cx - 0.5f + cw * 0.5f, y + h - textPadd * 0.5f);
			} else {
				g.fill(20);
				g.text("k=" + clusterings[i].getK(), cx - 0.5f + cw * 0.5f, y + h - textPadd * 0.5f);
			}
		}

		float[] losses = ClusteringHelper.getLosses(clusterings);
		float minLoss = RMath.min(losses);
		float maxLoss = RMath.max(losses) - minLoss;
		float lossScale = 1.1f;

		if (g == null) {
			parent.textAlign(PApplet.CENTER, PApplet.BOTTOM);
		} else {
			g.textAlign(PApplet.CENTER, PApplet.BOTTOM);
		}

		// bars with total loss values
		for (int i = 0; i < losses.length; i++) {
			float cx = x + cw * i;
			float ch = PApplet.map(losses[i], 0, (minLoss + maxLoss) * lossScale, 0, h - textPadd);
			float cy = y + h - textPadd - ch;

			if (getOptimalK() == i + minK) {
				if (g == null) {
					parent.fill(20, 80, 100);
				} else {
					g.fill(20, 80, 100);
				}
			} else {
				if (g == null) {
					parent.fill(50);
				} else {
					g.fill(50);
				}
			}

			if (g == null) {
				parent.noStroke();
				parent.rect(cx + 1.5f, cy, cw - 3, ch);
				parent.text(StringTools.nfs(losses[i], 2, 2), cx + cw * 0.5f, cy - textPadd * 0.1f);
			} else {
				g.noStroke();
				g.rect(cx + 1.5f, cy, cw - 3, ch);
				g.text(StringTools.nfs(losses[i], 2, 2), cx + cw * 0.5f, cy - textPadd * 0.1f);
			}
		}

		// loss percentages text
		for (int i = 0; i < losses.length; i++) {
			float cx = x + cw * i;
			float ch = PApplet.map(losses[i], 0, (minLoss + maxLoss) * lossScale, 0, h - textPadd);

			float cy = y + h - textPadd - ch;
			float diff = i == 0 ? 0 : losses[i] - losses[i - 1];

			if (g == null) {
				parent.fill(247);
				parent.text(StringTools.nfs(diff, 2, 2), cx + cw * 0.5f, cy + textPadd);
			} else {
				g.fill(247);
				g.text(StringTools.nfs(diff, 2, 2), cx + cw * 0.5f, cy + textPadd);
			}

			if (getOptimalK() == i + minK) {
				if (g == null) {
					parent.fill(180);
				} else {
					g.fill(180);
				}
			} else {
				if (g == null) {
					parent.fill(230, 15, 120);
				} else {
					g.fill(230, 15, 120);
				}
			}

			if (g == null) {
				parent.text(StringTools.nfs(100 * diff / maxLoss, 2, 2) + "%", cx + cw * 0.5f, cy + 2 * textPadd);
			} else {
				g.text(StringTools.nfs(100 * diff / maxLoss, 2, 2) + "%", cx + cw * 0.5f, cy + 2 * textPadd);
			}
		}
	}

	/**
	 * Draws the clustering (all cluster mean colors) with vertical stripes for
	 * the clustering with <b>k</b> clusters within a rectangular area.
	 * 
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 * @param k
	 *            The number of clusters / colors which defines which clustering
	 *            will be drawn. Make sure <b> is within range of [minK, maxK].
	 */
	public void drawClustering(float x, float y, float w, float h, int k) {
		drawClustering(null, x, y, w, h, k);
	}

	/**
	 * Draws the clustering (all cluster mean colors) with vertical stripes for
	 * the clustering with <b>k</b> clusters within a rectangular area.
	 * 
	 * @param g
	 *            A recorder.
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 * @param k
	 *            The number of clusters / colors which defines which clustering
	 *            will be drawn. Make sure <b> is within range of [minK, maxK].
	 */
	public void drawClustering(PGraphics g, float x, float y, float w, float h, int k) {
		if (clusterings.length < 1 + maxK - minK) {
			return;
		}

		ColorCluster[] clusters = clusterings[k - minK].getClusters();
		k = clusters.length;
		float cH = h / k;

		if (g == null) {
			parent.noStroke();
		} else {
			g.noStroke();
		}
		for (int i = 0; i < clusters.length; i++) {
			float cY = y + i * cH;
			if (g == null) {
				parent.fill(clusters[i].getMean().getColor());
				parent.rect(x, cY - 0.5f, w, cH + 1);
			} else {
				g.fill(clusters[i].getMean().getColor());
				g.rect(x, cY - 0.5f, w, cH + 1);
			}
		}
	}

	/**
	 * Draws the input image with an overlay of the clustering with <b>k</b>
	 * colors / clusters. If no inputImage is provided no image will be drawn.
	 * The image will be centered within the given rectangular area.
	 * 
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 * @param k
	 *            The number of clusters / colors which defines which clustering
	 *            will be drawn. Make sure <b> is within range of [minK, maxK].
	 */
	public void drawImageWithClustering(float x, float y, float w, float h, int k) {
		drawImageWithClustering(null, x, y, w, h, k);
	}

	/**
	 * Draws the input image with an overlay of the clustering with <b>k</b>
	 * colors / clusters. If no inputImage is provided no image will be drawn.
	 * The image will be centered within the given rectangular area.
	 * 
	 * @param g
	 *            A recorder.
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 * @param k
	 *            The number of clusters / colors which defines which clustering
	 *            will be drawn. Make sure <b> is within range of [minK, maxK].
	 */
	public void drawImageWithClustering(PGraphics g, float x, float y, float w, float h, int k) {
		if (inputImg == null) {
			return;
		}

		float amtW = (float) w / inputImg.width;
		float amtH = (float) h / inputImg.height;
		float amt = RMath.max(amtW, amtH);

		float iW = inputImg.width * amt;
		float iH = inputImg.height * amt;
		float iX = x - RMath.abs(iW - w) * 0.5f;
		float iY = y - RMath.abs(iH - h) * 0.5f;

		if (g == null) {
			parent.image(inputImg, iX, iY, iW, iH);
		} else {
			g.image(inputImg, iX, iY, iW, iH);
		}

		ColorCluster[] clusters = clusterings[k - minK].getClusters();
		k = clusters.length;
		float rPaddY = h * 0.1f + 0.2f * h / k;
		float rH = (h - 2 * rPaddY) / (2 * k - 1);
		float rW = w / 2;
		float rX = x + 0.5f * (w - rW);
		if (g == null) {
			parent.noStroke();
		} else {
			g.noStroke();
		}
		for (int i = 0; i < clusters.length; i++) {
			float rY = y + rPaddY + 2 * i * rH;

			if (g == null) {
				parent.fill(clusters[i].getMean().getColor());
				parent.rect(rX, rY, rW, rH);
			} else {
				g.fill(clusters[i].getMean().getColor());
				g.rect(rX, rY, rW, rH);
			}
		}
	}

	/**
	 * Draws all clusterings from minK to maxK with all cluster mean colors for
	 * each clustering within a rectangular area.
	 * 
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 */
	public void drawAllClusteringsMeans(float x, float y, float w, float h) {
		drawAllClusteringsMeans(null, x, y, w, h);
	}

	/**
	 * Draws all clusterings from minK to maxK with all cluster mean colors for
	 * each clustering within a rectangular area.
	 * 
	 * @param g
	 *            A recorder.
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 */
	public void drawAllClusteringsMeans(PGraphics g, float x, float y, float w, float h) {
		if (clusterings.length < 1 + maxK - minK) {
			return;
		}

		float cw = w / clusterings.length;
		float fontSize = cw * 0.3f;
		float textPadd = cw * 0.5f;
		if (g == null) {
			parent.textSize(fontSize);
			parent.textAlign(PApplet.CENTER, PApplet.CENTER);
		} else {
			g.textSize(fontSize);
			g.textAlign(PApplet.CENTER, PApplet.CENTER);
		}
		for (int i = 0; i < clusterings.length; i++) {
			float cx = x + cw * i;
			float cy = y;
			float ch = h - textPadd;

			if (g == null) {
				clusterings[i].drawMeans(parent, cx - 0.5f, cy - 0.5f, cw + 1, ch + 1);
			} else {
				clusterings[i].drawMeans(g, cx - 0.5f, cy - 0.5f, cw + 1, ch + 1);
			}

			if (g == null) {
				parent.fill(20);
				parent.text("k=" + clusterings[i].getK(), cx - 0.5f + cw * 0.5f, y + h - textPadd * 0.5f);
			} else {
				g.fill(20);
				g.text("k=" + clusterings[i].getK(), cx - 0.5f + cw * 0.5f, y + h - textPadd * 0.5f);
			}
		}
	}

	/**
	 * Draws all clusterings from minK to maxK with all cluster mean colors and
	 * all cluster members for each cluster within a rectangular area.
	 * 
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 */
	public void drawAllClusterings(float x, float y, float w, float h) {
		drawAllClusterings(null, x, y, w, h);
	}

	/**
	 * Draws all clusterings from minK to maxK with all cluster mean colors and
	 * all cluster members for each cluster within a rectangular area.
	 * 
	 * @param g
	 *            A recorder.
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 */
	public void drawAllClusterings(PGraphics g, float x, float y, float w, float h) {
		if (clusterings.length < 1 + maxK - minK) {
			return;
		}

		float cw = w / clusterings.length;
		float fontSize = cw * 0.3f;
		float textPadd = cw * 0.5f;
		if (g == null) {
			parent.textSize(fontSize);
			parent.textAlign(PApplet.CENTER, PApplet.CENTER);
		} else {
			g.textSize(fontSize);
			g.textAlign(PApplet.CENTER, PApplet.CENTER);
		}
		for (int i = 0; i < clusterings.length; i++) {
			float cx = x + cw * i;
			float cy = y;
			float ch = h - textPadd;

			if (g == null) {
				clusterings[i].drawMeans(parent, cx - 0.5f, cy - 0.5f, cw + 1, ch + 1);
				clusterings[i].drawMembers(parent, cx - 0.5f + cw * 0.5f, cy - 0.5f, (cw + 1) * 0.5f, ch + 1);
			} else {
				clusterings[i].drawMeans(g, cx - 0.5f, cy - 0.5f, cw + 1, ch + 1);
				clusterings[i].drawMembers(g, cx - 0.5f + cw * 0.5f, cy - 0.5f, (cw + 1) * 0.5f, ch + 1);
			}

			if (g == null) {
				parent.fill(20);
				parent.text("k=" + clusterings[i].getK(), cx - 0.5f + cw * 0.5f, y + h - textPadd * 0.5f);
			} else {
				g.fill(20);
				g.text("k=" + clusterings[i].getK(), cx - 0.5f + cw * 0.5f, y + h - textPadd * 0.5f);
			}
		}
	}

	/**
	 * Draws all members of all clusters of all clusterings from minK to maxK
	 * (sorted by clustering and cluster) within a rectangular area.
	 * 
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 */
	public void drawAllClusteringsMembers(float x, float y, float w, float h) {
		drawAllClusteringsMembers(null, x, y, w, h);
	}

	/**
	 * Draws all members of all clusters of all clusterings from minK to maxK
	 * (sorted by clustering and cluster) within a rectangular area.
	 * 
	 * @param g
	 *            A recorder.
	 * @param x
	 *            The x-position (top left corner) of the rectangle.
	 * @param y
	 *            The y-position (top left corner) of the rectangle.
	 * @param w
	 *            The width of the rectangle.
	 * @param h
	 *            The height of the rectangle.
	 */
	public void drawAllClusteringsMembers(PGraphics g, float x, float y, float w, float h) {
		if (clusterings.length < 1 + maxK - minK) {
			return;
		}

		float cw = w / clusterings.length;
		float fontSize = cw * 0.3f;
		float textPadd = cw * 0.5f;

		if (g == null) {
			parent.textSize(fontSize);
			parent.textAlign(PApplet.CENTER, PApplet.CENTER);
		} else {
			g.textSize(fontSize);
			g.textAlign(PApplet.CENTER, PApplet.CENTER);
		}
		for (int i = 0; i < clusterings.length; i++) {
			float cx = x + cw * i;
			float cy = y;
			float ch = h - textPadd;

			if (g == null) {
				clusterings[i].drawMembers(parent, cx - 0.5f, cy - 0.5f, cw + 1, ch + 1);
			} else {
				clusterings[i].drawMembers(g, cx - 0.5f, cy - 0.5f, cw + 1, ch + 1);
			}

			if (g == null) {
				parent.fill(20);
				parent.text("k=" + clusterings[i].getK(), cx - 0.5f + cw * 0.5f, y + h - textPadd * 0.5f);
			} else {
				g.fill(20);
				g.text("k=" + clusterings[i].getK(), cx - 0.5f + cw * 0.5f, y + h - textPadd * 0.5f);
			}
		}
	}

	/**
	 * @param x
	 * @param y
	 * @param w
	 * @param h
	 */
	public void drawInputColors(float x, float y, float w, float h) {
		drawInputColors(null, x, y, w, h);
	}

	/**
	 * @param g
	 * @param x
	 * @param y
	 * @param w
	 * @param h
	 */
	public void drawInputColors(PGraphics g, float x, float y, float w, float h) {
		if (clusterings.length < 1 + maxK - minK) {
			return;
		}

		float cw = w / this.trainingColors.length;
		for (int i = 0; i < trainingColors.length; i++) {
			float cx = x + cw * i;
			float cy = y;
			float ch = h;

			if (g == null) {
				parent.noStroke();
				parent.fill(trainingColors[i].getColor());
				parent.rect(cx - 0.5f, cy, cw + 1, ch);
			} else {
				g.noStroke();
				g.fill(trainingColors[i].getColor());
				g.rect(cx - 0.5f, cy, cw + 1, ch);
			}
		}
	}

}
