package net.returnvoid.analytics;

import java.util.ArrayList;

import net.returnvoid.color.ColorDifference;
import net.returnvoid.color.ColorDifferenceMeasure;
import net.returnvoid.color.ColorPalette;
import net.returnvoid.color.RGBColor;
import net.returnvoid.color.RColor;
import processing.core.PApplet;
import processing.core.PGraphics;

/*
 * 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 store color clusterings. Instances of this class are meant to
 * final. This means no changes of the clusters or to the content of any
 * clusters should be made.
 * 
 * @author Diana Lange
 *
 */
public class ColorClustering implements Clustering {

	/**
	 * The number of clusters of this clustering. k might differ from the
	 * clusters.length, when empty clusters (clusters with no members) are
	 * automatically removed.
	 */
	private int k;

	/**
	 * The loss of this clustering (mean loss of all clusters).
	 */
	private Float loss;

	/**
	 * The clusters of this clusterings with clusters.length <= k.
	 */
	private ColorCluster[] clusters;

	/**
	 * All mean colors of the clusters of this clustering (for faster get access
	 * to these colors).
	 */
	private RColor[] means;

	/**
	 * The used ColorDifferenceMeasure of the clusters. Also used to calculate
	 * the most similar cluster to a given input color in getCluster(RColor).
	 */
	private ColorDifferenceMeasure m;

	/**
	 * Builds a new clustering from a given set of clusters. The clusters should
	 * not be changed after the clustering has been build. All calculations /
	 * optimizations of the clusters should before building a clustering. The
	 * clusters will be sorted by luminance (first cluster = lowest luminance,
	 * last cluster = highest luminance).
	 * 
	 * @param clusters
	 *            The clusters with set means and cluster members. All clusters
	 *            should uses the same ColorDifferenceMeasure.
	 * @param k
	 *            The number of clusters of this clustering. k might differ from
	 *            the clusters.length, when empty clusters (clusters with no
	 *            members) are automatically removed.
	 */
	public ColorClustering(ColorCluster[] clusters, int k) {
		this.k = k;
		this.m = clusters[0].getColorDifferenceMeasure();
		this.clusters = clusters;
		this.loss = ClusteringHelper.computeLoss(this.clusters);
		this.sort();
		this.means = computeMeanColors();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.analytics.Clustering#getLoss()
	 */
	public Float getLoss() {
		return loss;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.analytics.Clustering#getK()
	 */
	public int getK() {
		return k;
	}

	/**
	 * Returns the minimal luminance of this clustering.
	 * 
	 * @return The minimal luminance of this clustering [0, 100].
	 */
	public Float getMinLuminance() {
		return clusters[0].getMean().toLCH().getLuminance();
	}

	/**
	 * Returns the maximal luminance of this clustering.
	 * 
	 * @return The maximal luminance of this clustering [0, 100].
	 */
	public Float getMaxLuminance() {
		return clusters[clusters.length - 1].getMean().toLCH().getLuminance();
	}

	/**
	 * Builds a color palette from this clustering. The palette will consist of
	 * the the means of the clusters.
	 * 
	 * @return A new (weighted) color palette.
	 */
	public ColorPalette toColorPalette() {

		return toColorPalette(false);
	}

	/**
	 * Builds a color palette from this clustering. If <b>useMeans</b> is false:
	 * The palette will only contain colors of the cluster members (will choose
	 * a cluster member for each cluster that is the closest to the mean).
	 * Otherwise the means of the clusters will build the palette.
	 * 
	 * @param useMeans
	 *            A parameter which defines if the palette should consist of
	 *            cluster means or a representative.
	 * @return A new (weighted) color palette.
	 */
	public ColorPalette toColorPalette(boolean useMeans) {

		RColor[] colors;
		ColorPalette cp;

		// means can be accessed directly, but the representatives have to be
		// calculated first
		if (useMeans) {
			colors = means;
		} else {
			colors = new RColor[this.clusters.length];
			for (int i = 0; i < colors.length; i++) {
				colors[i] = clusters[i].getRepresentative().copy();
			}

		}

		// build palette
		cp = new ColorPalette(colors);
		cp.setColorDifferenceMeasure(this.m);

		// apply importance for each color of the palette (importance = number
		// of cluster members). If the input was an image and the trainings data
		// is sampled well than more often found colors (= clusters with high
		// number of size) represent the visual importance in the image.
		for (int i = 0; i < clusters.length; i++) {
			cp.applyImportance(colors[i], clusters[i].size());
		}

		return cp;
	}

	/**
	 * Returns all cluster means.
	 * 
	 * @return The cluster means.
	 */
	public RColor[] getMeans() {
		return means;
	}

	/**
	 * Returns a set of colors which contains all members of all clusters of
	 * this clustering.
	 * 
	 * @return The set of colors.
	 */
	public RColor[] getMembers() {
		ArrayList<RColor> colors = new ArrayList<RColor>();

		for (ColorCluster cluster : this.clusters) {
			colors.addAll(cluster.getElements());
		}

		return colors.toArray(new RColor[colors.size()]);
	}

	/**
	 * Builds and returns the cluster means.
	 * 
	 * @return The cluster means.
	 */
	private RColor[] computeMeanColors() {

		RColor[] means = new RColor[clusters.length];
		for (int i = 0; i < means.length; i++) {
			means[i] = clusters[i].getMean();
		}

		return means;

	}

	/**
	 * The number of clusters for this clustering. This should be used to
	 * iterate over all clusters. Example:<br>
	 * 
	 * <pre>
	 * for (int i = 0; i < myClustering.size(); i++) {
	 * 	myClustering.get(i); // The cluster with index i
	 * }
	 * </pre>
	 * 
	 * @return The number of clusters for this clustering.
	 */
	public int size() {
		return clusters.length;
	}

	/**
	 * Returns the cluster with the given <b>index</b>. Make sure the index is
	 * within bounds, e.g. use the <b>size()</b> method to iterate over all
	 * clusters. Example: <br>
	 * 
	 * <pre>
	 * for (int i = 0; i < myClustering.size(); i++) {
	 * 	myClustering.get(i); // The cluster with index i
	 * }
	 * </pre>
	 * 
	 * @param index
	 *            The index of the cluster.
	 * @return The cluster with the given index.
	 */
	public ColorCluster get(int index) {
		return clusters[index];
	}

	/**
	 * Returns the index of the cluster which is the most similar to the input
	 * color.
	 * 
	 * @param c
	 *            A color.
	 * @return The index of the cluster which is the most similar to the input
	 *         or -1 if the input wasn't a color.
	 */
	public int getClusterIndex(Object c) {
		return -1;
	}

	/**
	 * Returns the index of the cluster which is the most similar to the input
	 * color.
	 * 
	 * @param c
	 *            A color (e.g 0xFFFF0000).
	 * @return The index of the cluster which is the most similar to the input
	 *         or -1 if the input wasn't a color.
	 */
	public int getClusterIndex(int c) {

		return getClusterIndex(RGBColor.toRGB(c));
	}

	/**
	 * Returns the index of the cluster which is the most similar to the input
	 * color.
	 * 
	 * @param c
	 *            A color object.
	 * @return The index of the cluster which is the most similar to the input
	 *         or -1 if the input wasn't a color.
	 */
	public int getClusterIndex(RColor c) {

		return ColorDifference.getMostSimilarColor(c, means, m);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.analytics.Clustering#getCluster(java.lang.Object)
	 */
	public ColorCluster getCluster(Object c) {
		return getCluster(Integer.parseInt(c + ""));
	}

	/**
	 * Returns the cluster which is the most similar to the given color.
	 * 
	 * @param c
	 *            A color (e.g 0xFFFF0000).
	 * @return The most similar cluster to the input.
	 */
	public ColorCluster getCluster(int c) {

		return getCluster(RGBColor.toRGB(c));
	}

	/**
	 * Returns the cluster which is the most similar to the given color.
	 * 
	 * @param c
	 *            A color.
	 * @return The most similar cluster to the input.
	 */
	public ColorCluster getCluster(RColor c) {

		Integer id = ColorDifference.getMostSimilarColor(c, means, m);

		if (id >= 0) {
			return clusters[id];
		} else {
			System.err.println("No cluster found for " + c);
			return null;
		}
	}

	/**
	 * Sorts the clusters by luminance of the means beginning with the lowest
	 * luminance.
	 */
	private void sort() {
		boolean sorted = false;
		while (!sorted) {
			sorted = true;

			for (int i = 0; i < clusters.length - 1; i++) {
				if (clusters[i].getMean().toLab().getLuminance() < clusters[i + 1].getMean().toLab().getLuminance()) {
					ColorCluster tmp = clusters[i];
					clusters[i] = clusters[i + 1];
					clusters[i + 1] = tmp;
					sorted = false;
				}
			}
		}

	}

	/**
	 * Returns the ColorDifferenceMeasure which is used to calculate the most
	 * similar cluster to a given color within the getCluster() and
	 * getClusterIndex() methods.
	 * 
	 * @return The ColorDifferenceMeasure of this clustering.
	 */
	public ColorDifferenceMeasure getColorDifferenceMeasure() {
		return m;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.analytics.Clustering#getClusters()
	 */
	@Override
	public ColorCluster[] getClusters() {
		return clusters;
	}

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

	/**
	 * Draws the cluster members with horizontal striped 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.
	 */
	public void drawMembers(PGraphics g, float x, float y, float w, float h) {
		drawMembers(g, null, x, y, w, h);
	}

	/**
	 * Draws the cluster members with horizontal striped within the given
	 * rectangular area.
	 * 
	 * @param parent
	 *            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 drawMembers(PApplet parent, float x, float y, float w, float h) {
		drawMembers(null, parent, x, y, w, h);
	}

	/**
	 * Draws the cluster members with horizontal striped within the given
	 * rectangular area. Uses just one recorder (one of g or parent should be
	 * null).
	 * 
	 * @param g
	 *            A recorder or null.
	 * @param parent
	 *            A recorder or null.
	 * @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.
	 */
	private void drawMembers(PGraphics g, PApplet parent, float x, float y, float w, float h) {

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

			if (g == null) {
				clusters[i].drawMembers(parent, cx, cy, cw, ch);
			} else {
				clusters[i].drawMembers(g, cx, cy, cw, ch);
			}

		}
	}

	/**
	 * Draws the cluster means with horizontal striped within the given
	 * rectangular area.
	 * 
	 * @param g
	 *            A recorder or null.
	 * @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 drawMeans(PGraphics g, float x, float y, float w, float h) {
		drawMeans(g, null, x, y, w, h);
	}

	/**
	 * Draws the cluster means with horizontal striped within the given
	 * rectangular area.
	 * 
	 * @param parent
	 *            A recorder or null.
	 * @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 drawMeans(PApplet parent, float x, float y, float w, float h) {
		drawMeans(null, parent, x, y, w, h);
	}

	/**
	 * Draws the cluster means with horizontal striped within the given
	 * rectangular area. Uses just one recorder (one of g or parent should be
	 * null).
	 * 
	 * @param g
	 *            A recorder or null.
	 * @param parent
	 *            A recorder or null.
	 * @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.
	 */
	private void drawMeans(PGraphics g, PApplet parent, float x, float y, float w, float h) {

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

			if (g == null) {
				clusters[i].drawMean(parent, cx, cy, cw, ch);
			} else {
				clusters[i].drawMean(g, cx, cy, cw, ch);
			}

		}
	}

}
