package net.returnvoid.analytics;

import java.util.ArrayList;

import net.returnvoid.color.ColorDifference;
import net.returnvoid.color.ColorDifferenceMeasure;
import net.returnvoid.color.ColorHelper;
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.
 */

/**
 * @author Diana
 *
 */
public class ColorCluster implements Cluster {

	/**
	 * The mean color of this cluster.
	 */
	private RColor mean;

	/**
	 * All colors that are member of this cluster.
	 */
	private ArrayList<RColor> clusterMembers;

	/**
	 * A ColorDifferenceMeasure (this will define the distance measure for
	 * finding the representative of the cluster and the color space for
	 * calculating the mean).
	 */
	private ColorDifferenceMeasure m;

	/**
	 * The loss of the cluster. If the the value of the cluster has changed
	 * (e.g. new cluster member have been added) the value of loss will be null.
	 * Use getter to access the value (if it is null, the loss will be
	 * calculated).
	 */
	private Float loss;

	/**
	 * Builds a new cluster without any member but with a set mean. The
	 * ColorDifferenceMeasure will be RGBEuclidean. The ColorDifferenceMeasure
	 * will define the distance measure for finding the representative of the
	 * cluster and the color space for calculating the mean.
	 * 
	 * @param mean
	 *            The mean value for this cluster.
	 */
	public ColorCluster(RColor mean) {
		this(mean, ColorDifferenceMeasure.RGBEuclidean);
	}

	/**
	 * Builds a new cluster without any member but with a set mean. The
	 * ColorDifferenceMeasure will define the distance measure for finding the
	 * representative of the cluster and the color space for calculating the
	 * mean.
	 * 
	 * @param mean
	 *            The mean value for this cluster.
	 * @param m
	 *            The ColorDifferenceMeasure.
	 */
	public ColorCluster(RColor mean, ColorDifferenceMeasure m) {
		this.loss = -1f;
		this.m = m;
		this.mean = ColorHelper.convert(mean, m);
		clusterMembers = new ArrayList<RColor>();
	}

	/**
	 * Returns the current ColorDifferenceMeasure.
	 * 
	 * @return The current ColorDifferenceMeasure.
	 */
	public ColorDifferenceMeasure getColorDifferenceMeasure() {
		return m;
	}

	/**
	 * Sets a new ColorDifferenceMeasure. All members and the mean will be
	 * converted to match the new ColorDifferenceMeasure.
	 * 
	 * @param m
	 *            The new ColorDifferenceMeasure.
	 * 
	 * @return The current cluster.
	 */
	public ColorCluster setColorDifferenceMeasure(ColorDifferenceMeasure m) {
		this.m = m;
		ColorHelper.convert(this.clusterMembers, m.toColorSpace());
		this.mean = ColorHelper.convert(this.mean, m);

		return this;
	}

	/**
	 * Returns the number of cluster members.
	 * 
	 * @return The number of cluster members.
	 */
	public int size() {
		return clusterMembers.size();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.analytics.Cluster#addElement(java.lang.Object)
	 */
	@Override
	public Cluster addElement(Object newMember) {
		return this;
	}

	/**
	 * Adds an element to the cluster.
	 * 
	 * @param newMember
	 *            The new cluster member.
	 * @return The current cluster object.
	 */
	public ColorCluster addElement(RColor newMember) {
		// System.out.println("Element added: " + newMember.getName());
		loss = null;
		clusterMembers.add(newMember);
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.analytics.Cluster#removeElement(java.lang.Object)
	 */
	@Override
	public Cluster removeElement(Object member) {
		return this;
	}

	/**
	 * Removes an element from the cluster (if the given element is a member).
	 * 
	 * @param member
	 *            The element which should be removed.
	 * @return The current cluster object.
	 */
	public ColorCluster removeElement(RColor member) {
		loss = null;
		clusterMembers.remove(member);
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.analytics.Cluster#switchElement(java.lang.Object,
	 * net.returnvoid.analytics.Cluster)
	 */
	@Override
	public boolean switchElement(Object member, Cluster newCluster) {
		return false;
	}

	/**
	 * The given element <b>member</b> switches to another cluster under certain
	 * circumstances. Returns if the given member actually changed its cluster.
	 * 
	 * @param member
	 *            The element which might switch the cluster.
	 * @param newCluster
	 *            The cluster where the element might switch to.
	 * @return True, if element has changed its cluster. Otherwise false.
	 */
	public boolean switchElement(RColor member, ColorCluster newCluster) {
		if (newCluster == this) {
			return false;
		} else {
			this.removeElement(member);
			newCluster.addElement(member);

			return true;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.analytics.Cluster#updateMean()
	 */
	@Override
	public ColorCluster updateMean() {

		loss = null;

		if (clusterMembers.size() > 0) {

			// if there is just one member, than this one is the mean
			if (clusterMembers.size() == 1) {
				mean = clusterMembers.get(0).copy();
			} else {
				// calculate mean color

				// HSB and LCH really suck in mean calculation when there
				// is a huge color range, so may it would be better to use
				// other color spaces for that
				/*
				 * switch (m.toColorSpace()) { case RGBColor: case HSBColor:
				 * mean = RGBColor.getMeanColor(clusterMembers); break; case
				 * XYZColor: mean = XYZColor.getMeanColor(clusterMembers);
				 * break; case LabColor: mean =
				 * LabColor.getMeanColor(clusterMembers); break;
				 * 
				 * case LCHColor: mean =
				 * RGBColor.lerpColors(LabColor.getMeanColor(clusterMembers),
				 * LCHColor.getMeanColor(clusterMembers), 0.5f); break; default:
				 * mean = RGBColor.getMeanColor(clusterMembers); break;
				 * 
				 * }
				 */
				mean = ColorHelper.getMeanColor(this.clusterMembers, m);

				// find a member color that is closest to the calculated mean -
				// that one is the true mean
				// mean = getRepresentative();

				// to target color space
				// mean = ColorHelper.convert(mean, m);
			}
		}

		return this;
	}

	/**
	 * Finds and returns the representative of this cluster. The representative
	 * is the cluster member which is the most similar to the cluster mean.
	 * 
	 * @return The representative (element of cluster members).
	 */
	public RColor getRepresentative() {
		// find a member color that is closest to the calculated mean -
		int bestMeanID = ColorDifference.getMostSimilarColor(mean, clusterMembers, m);

		return clusterMembers.get(bestMeanID);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.analytics.Cluster#getMean()
	 */
	@Override
	public RColor getMean() {
		return mean;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.analytics.Cluster#getElements()
	 */
	@Override
	public ArrayList<RColor> getElements() {
		return clusterMembers;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.analytics.Cluster#getLoss()
	 */
	public Float getLoss() {
		if (clusterMembers.size() == 0) {
			return 0f;
		} else {

			if (loss == null) {
				loss = ColorHelper.getLoss(mean, clusterMembers, m);
			}

			return loss;
		}
	}

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

	/**
	 * Draws the mean color (colored rectangle with given parameters).
	 * 
	 * @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 drawMean(PGraphics g, float x, float y, float w, float h) {
		drawMean(g, null, x, y, w, h);
	}

	/**
	 * Draws the mean color (colored rectangle with given parameters).
	 * 
	 * @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 drawMean(PApplet parent, float x, float y, float w, float h) {
		drawMean(null, parent, x, y, w, h);
	}

	/**
	 * Draws the mean color (colored rectangle with given parameters). Uses just
	 * one recorder (one of g or parent should be null).
	 * 
	 * @param parent
	 *            A recorder.
	 * @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.
	 */
	private void drawMean(PGraphics g, PApplet parent, float x, float y, float w, float h) {

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

	/**
	 * Draws the members of the cluster (colored horizontal stripes within a
	 * rectangular area defined by the given parameters).
	 * 
	 * @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 members of the cluster (colored horizontal stripes within a
	 * rectangular area defined by the given parameters).
	 * 
	 * @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 members of the cluster (colored horizontal stripes within a
	 * rectangular area defined by the given parameters). Uses just one recorder
	 * (one of g or parent should be null).
	 * 
	 * @param parent
	 *            A recorder or null.
	 * @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.
	 */
	private void drawMembers(PGraphics g, PApplet parent, float x, float y, float w, float h) {
		if (g == null) {
			parent.noStroke();
		} else {
			g.noStroke();
		}

		for (int i = 0; i < clusterMembers.size(); i++) {
			float rh = h / clusterMembers.size();
			float ry = y + i * rh;

			if (g == null) {
				parent.fill(clusterMembers.get(i).getColor());
				parent.rect(x, ry, w, rh);
			} else {
				g.fill(clusterMembers.get(i).getColor());
				g.rect(x, ry, w, rh);
			}
		}

		if (clusterMembers.size() == 0) {
			if (g == null) {
				parent.strokeWeight(1);
				parent.stroke(255, 0, 0);
				parent.noFill();
				parent.rect(x, y, w, h);
				parent.noStroke();
			} else {
				g.strokeWeight(1);
				g.stroke(255, 0, 0);
				g.noFill();
				g.rect(x, y, w, h);
				g.noStroke();
			}
		}
	}

}
