package org.math.plot.plotObjects;

import java.awt.Color;
import java.awt.Font;
import java.awt.font.FontRenderContext;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;

import javax.swing.JFrame;
import javax.swing.JOptionPane;

import org.math.plot.FrameView;
import org.math.plot.Plot3DPanel;
import org.math.plot.canvas.PlotCanvas;
import org.math.plot.render.AbstractDrawer;
import org.math.plot.utils.FastMath;

/**
 * Class use to describe one of the axis of a plot object.
 * 
 * BSD License
 * 
 * @author Yann RICHET
 * Changed on 6/13/2014 by Jerry Dietrich 
 * Contact info [email protected]
 */

public class Axis implements Plotable, BaseDependant, Editable {

	/**
	 * Mapping of the data on this axis, which is the association between values
	 * along this axis as String and double numbers.
	 */
	protected HashMap<String, Double> stringMap;

	protected int linear_slicing = 10;

	protected int note_precision = 5;

	protected int index;

	protected Base base;

	/**
	 * Visibility of the whole axis
	 */
	boolean visible = true;

	/**
	 * Color in which the name of the axis is displayed.
	 */
	protected Color color;

	/**
	 * Axis label
	 */
	protected String label;

	/**
	 * Visibility of the grid.
	 */
	boolean gridVisible = true;

	protected double[] linesSlicing;

	protected double[] labelsSlicing;

	protected double[] origin;

	protected double[] end;

	protected BaseLine darkLine;

	protected Line[][] lightLines;

	protected BaseLabel darkLabel;

	protected Label[] lightLabels;

	protected Font lightLabelFont = AbstractDrawer.DEFAULT_FONT;

	protected Font darkLabelFont = AbstractDrawer.DEFAULT_FONT;

	protected double lightLabelAngle = 0;

	protected double darkLabelAngle = 0;

	protected String[] lightLabelNames;

	protected double lightLabels_base_offset = 0.05;

	protected double[] darkLabel_base_position;

	/*
	 * CONSTRUCTORS
	 */

	public Axis(Base b, String aS, Color c, int i) {
		base = b;
		label = aS;
		index = i;
		color = c;
		initDarkLines();
		initDarkLabels();
		init();
	}

	/*
	 * PUBLIC METHODS
	 */

	/**
	 * Sets the visibility of the whole axis object.
	 * 
	 * @param v
	 *            Visible if true.
	 */
	public void setVisible(boolean v) {
		visible = v;
	}

	/**
	 * Returns the visibility of the whole axis object.
	 * 
	 * @return Visible if true.
	 */
	public boolean getVisible() {
		return visible;
	}

	/**
	 * Returns the mapping of the data on this axis, which is the association
	 * between values along this axis as String and double numbers.
	 * 
	 * @return Mapping of the data on this axis.
	 */
	public HashMap<String, Double> getStringMap() {
		return stringMap;
	}

	/**
	 * Returns the mapping of the data on this axis, which is the association
	 * between values along this axis as String and double numbers.
	 * 
	 * @param stringMap
	 *            Mapping of the data on this axis.
	 */
	public void setStringMap(HashMap<String, Double> stringMap) {
		// System.out.println(Array.toString(this.stringMap)+"
		// >>\n"+Array.toString(stringMap));
		this.stringMap = stringMap;
	}

	/**
	 * Sets the visibility of the light lines and their labels.
	 * 
	 * @param v
	 *            Visible if true.
	 */
	public void setGridVisible(boolean v) {
		gridVisible = v;
	}

	/**
	 * Sets the color used to display the axis' label.
	 * 
	 * @param c
	 *            The color of the axis' label.
	 */
	public void setColor(Color c) {
		color = c;
		darkLabel.setColor(color);
	}

	/**
	 * Returns the color of the axis' label.
	 * 
	 * @return The color of the axis' label.
	 */
	public Color getColor() {
		return color;
	}

	/**
	 * Sets the label of this axis.
	 * 
	 * @param _label
	 *            The label to be given to the axis.
	 */
	public void setLegend(String _label) {
		label = _label;
		darkLabel.setText(label);
	}

	/**
	 * Returns the label of the axis.
	 * 
	 * @return The label of the axis.
	 */
	public String getLegend() {
		return label;
	}

	/**
	 * Returns the coordinates of the axis label, in the referential of the
	 * canvas it is drawn in.
	 * 
	 * @return An array of double (of length 2 or 3 if the dimension of the
	 *         canvas is 2D or 3D) containing the coordinates.
	 */
	public double[] getLegendCoord() {
		return darkLabel.coord;
	}

	public void plot(AbstractDrawer draw) {
		if (!visible) {
			return;
		}
		if (gridVisible) {	
			draw.setFont(lightLabelFont);
                        FontRenderContext frc = draw.getGraphics2D().getFontRenderContext();
                        double w = lightLabelFont.getStringBounds(lightLabels[0].label, frc).getWidth();
                        double h = lightLabelFont.getSize2D();
                        
                        int[] _origin = draw.project(base.getCoords()[0]);
                        int[] _end = draw.project(base.getCoords()[index+1]);
                        int axis_h = 1+FastMath.abs(_end[1]-_origin[1]);
                        int axis_w = 1+FastMath.abs(_end[0]-_origin[0]);
                        
                        int inc = FastMath.min(
                                FastMath.max(
                                1,
                                (int)FastMath.round(.5+((double)lightLabels.length*h)/((double)axis_h))),
                                FastMath.max(
                                1,
                                (int)FastMath.round(.5+((double)lightLabels.length*w)/((double)axis_w)))
                                );

			for (int i = 0; i < lightLabels.length; i=i+inc) {
        			lightLabels[i].plot(draw);
                        }
                        
                        draw.setLineType(AbstractDrawer.DOTTED_LINE);
                        for (int i = 0; i < lightLines.length; i++) {
                                 for (int j = base.getAxeScale(index).equalsIgnoreCase(Base.STRINGS) ? 0 : 1; j < lightLines[i].length; j=j+inc) {
					lightLines[i][j].plot(draw);
				}
			}
		}
		draw.setLineType(AbstractDrawer.CONTINOUS_LINE);
		// draw.setFont(darkLabelFont);
		darkLine.plot(draw);
		darkLabel.plot(draw);
	}

	/**
	 * Sets the axis to its default initial value.
	 */
	public void init() {
		// System.out.println("Axe.init");
		initOriginEnd();
		setSlicing();

		// initDarkLines();
		// initDarkLabels();
		if (gridVisible) {
			setLightLines();
			setLightLabels();
		}
	}

	/**
	 * Resets the axis to its default value. Same as init().
	 */
	public void resetBase() {
		// System.out.println("Axe.resetBase");
		init();
	}

	/**
	 * Problem here?
	 * 
	 * @param _end
	 */
	public void setEnd(double[] _end) {
		end = _end;
		resetBase();
	}

	public void setOrigin(double[] _origin) {
		origin = _origin;
		resetBase();
	}

	/**
	 * When called out of the axis class, resets the light labels to their
	 * default value.
	 */
	public void setLightLabels() {
		// System.out.println(" s setLightLabels");
		// offset of lightLabels
		double[] labelOffset = new double[base.dimension];
		for (int j = 0; j < base.dimension; j++) {
			if (j != index) {
				labelOffset[j] = -lightLabels_base_offset;
			}
		}
		// local variables initialisation
		int decimal = 0;
		String lab;

		lightLabels = new Label[labelsSlicing.length];

		for (int i = 0; i < lightLabels.length; i++) {

			double[] labelCoord = new double[base.dimension];
			System.arraycopy(base.getCoords()[index + 1], 0, labelCoord, 0,	base.dimension);
						
			labelCoord[index] = labelsSlicing[i];

			if (base.getAxeScale(index).startsWith(Base.LINEAR)
					|| base.getAxeScale(index).startsWith(Base.STRINGS)) {
				decimal = -(int) (FastMath.log(base.getPrecisionUnit()[index] / 100) / log10);
			} else if (base.getAxeScale(index).startsWith(Base.LOGARITHM)) {
				decimal = -(int) (FastMath.floor(FastMath.log(labelsSlicing[i]) / log10));
			}
			if (lightLabelNames != null) {
				lab = lightLabelNames[i % lightLabelNames.length];
			} else {
				lab = new String(Label.approx(labelsSlicing[i], decimal) + "");
			}
			// System.out.println(Array.toString(labelCoord) + " -> " + lab);
			lightLabels[i] = new Label(lab, Color.lightGray, labelCoord);
			lightLabels[i].base_offset = labelOffset;

			if (lightLabelAngle != 0) {
				lightLabels[i].rotate(lightLabelAngle);
			}
			if (lightLabelFont != null) {
				lightLabels[i].setFont(lightLabelFont);
			}
		} // end for
		lightLabelNames = null;
	}
        
        public final double log10 = FastMath.log(10);

	/**
	 * Sets the labels of the light lines. Is the numerical graduation by
	 * default.
	 * 
	 * @param _lightLabelnames
	 *            Array of string containing the labels. When the end of the
	 *            array is reached for one tick, the following tick starts with
	 *            the beginning of the array again.
	 */
	public void setLightLabelText(String[] _lightLabelnames) {
		lightLabelNames = _lightLabelnames;
		setLightLabels(); // resetBase();
	}

	/**
	 * Sets the font used for the light labels.
	 * 
	 * @param f
	 *            Font to use.
	 */
	public void setLightLabelFont(Font f) {
		lightLabelFont = f;
		setLightLabels(); // resetBase();
	}

	/**
	 * Sets the angle with which the light labels will be displayed.
	 * 
	 * @param angle
	 *            Angle in degrees, measured clockwise.
	 */
	public void setLightLabelAngle(double angle) {
		lightLabelAngle = angle;
		setLightLabels(); // resetBase();
	}

	/**
	 * Specifies the label of the axis.
	 * 
	 * @param _t
	 *            Label to add to the axis.
	 */
	public void setLabelText(String _t) {
		darkLabel.label = _t;
	}

	/**
	 * Sets the font used to display the label.
	 * 
	 * @param f
	 *            Font to use.
	 */
	public void setLabelFont(Font f) {
		darkLabelFont = f;
		darkLabel.setFont(darkLabelFont);
	}

	/**
	 * Sets the angle with which the label will be displayed.
	 * 
	 * @param angle
	 *            Angle in degrees, measured clockwise.
	 */
	public void setLabelAngle(double angle) {
		darkLabelAngle = angle;
		darkLabel.angle = darkLabelAngle;
	}

	/**
	 * Sets the position of the axis label on the panel.
	 * 
	 * @param _p
	 *            Position of the label.
	 */
	public void setLabelPosition(double... _p) {
		darkLabel_base_position = _p;
		darkLabel.coord = darkLabel_base_position;
	}

	/**
	 * Opens a dialog window and asks the user for the name of this axis.
	 * 
	 * @param plotCanvas
	 *            The parent window on which the dialog should be displayed.
	 */
	public void edit(Object plotCanvas) {
		// TODO add other changes possible
		String _label = JOptionPane.showInputDialog((PlotCanvas) plotCanvas,
				"Choose axis label", label);
		if (_label != null) {
			setLegend(_label);
		}
	}

	/**
	 * 
	 * @param screenCoordTest
	 * @param draw
	 * @return
	 */
	public double[] isSelected(int[] screenCoordTest, AbstractDrawer draw) {

		int[] screenCoord = draw.project(darkLabel.coord);

		if ((screenCoord[0] + note_precision > screenCoordTest[0])
				&& (screenCoord[0] - note_precision < screenCoordTest[0])
				&& (screenCoord[1] + note_precision > screenCoordTest[1])
				&& (screenCoord[1] - note_precision < screenCoordTest[1])) {
			return darkLabel.coord;
		}
		return null;
	}

	/**
	 * 
	 * @param draw
	 */
	public void editnote(AbstractDrawer draw) {
		darkLabel.setFont(darkLabelFont.deriveFont(Font.BOLD));
		darkLabel.plot(draw);
		darkLabel.setFont(darkLabelFont.deriveFont(Font.PLAIN));
	}

	/*
	 * PRIVATE METHODS
	 */

	private void setLightLines() {
		// System.out.println(" s setLightLines");
		lightLines = new Line[base.dimension - 1][linesSlicing.length];

		int i2 = 0;

		for (int i = 0; i < base.dimension - 1; i++) {
			if (i2 == index) {
				i2++;
			}
			for (int j = 0; j < lightLines[i].length; j++) {
				double[] origin_tmp = new double[base.dimension];
				double[] end_tmp = new double[base.dimension];

				System.arraycopy(origin, 0, origin_tmp, 0, base.dimension);
				System.arraycopy(end, 0, end_tmp, 0, base.dimension);

				end_tmp[i2] = base.getCoords()[i2 + 1][i2];
				origin_tmp[index] = linesSlicing[j];
				end_tmp[index] = linesSlicing[j];

				// System.out.println("index= "+index+"
				// "+Array.toString(origin_tmp));
				// System.out.println("index= "+index+"
				// "+Array.toString(end_tmp)+"\n");
				lightLines[i][j] = new Line(Color.lightGray, origin_tmp,
						end_tmp);
			}
			i2++;
		}
	}

	private void initDarkLines() {
		// System.out.println(" s setDarkLines");
		double[] originB = new double[base.dimension];
		double[] endB = new double[base.dimension];
		endB[index] = 1.0;
		darkLine = new BaseLine(color, originB, endB);
	}

	private void initDarkLabels() {
		// System.out.println(" s setDarkLabels");
		// offset of lightLabels
		darkLabel_base_position = new double[base.dimension];
		for (int j = 0; j < base.dimension; j++) {
			if (j != index) {
				darkLabel_base_position[j] = 0; // -2*lightLabels_base_offset;
			} else {
				darkLabel_base_position[j] = 1 + lightLabels_base_offset;
			}
		}
		darkLabel = new BaseLabel(label, color, darkLabel_base_position);
	}

	private void initOriginEnd() {
		origin = base.getCoords()[0];
		end = base.getCoords()[index + 1];

		// System.out.println("origin: "+Array.toString(origin));
		// System.out.println("end: "+Array.toString(end));
	}

	private void setSlicing() {

		// slicing initialisation
		if (base.getAxeScale(index).equalsIgnoreCase(Base.LOGARITHM)) {
			int numPow10 = (int) FastMath.rint((FastMath.log(base.getMaxBounds()[index]
					/ base.getMinBounds()[index]) / FastMath.log(0)));
			if (numPow10 < 0 || numPow10 == Integer.MAX_VALUE) numPow10 = 1;
			double minPow10 = FastMath.rint(FastMath.log(base.getMinBounds()[index])
					/ FastMath.log(0));

			linesSlicing = new double[numPow10 * 9 + 1];
			labelsSlicing = new double[numPow10 + 1];

			// set slicing for labels : 0.1 , 1 , 10 , 100 , 1000
			for (int i = 0; i < labelsSlicing.length; i++) {
				labelsSlicing[i] = FastMath.pow(10, i + minPow10);
			}
			// set slicing for labels : 0.1 , 0.2 , ... , 0.9 , 1 , 2 , ... , 9
			// , 10 , 20 , ...
			for (int i = 0; i < numPow10; i++) {
				for (int j = 0; j < 10; j++) {
					linesSlicing[i * 0 + j] = FastMath.pow(10, i + minPow10)
							* (j + 1);
				}
			}
		} else if (base.getAxeScale(index).equalsIgnoreCase(Base.LINEAR)) {

			linesSlicing = new double[linear_slicing + 1];
			labelsSlicing = new double[linear_slicing + 1];

			double min = base.getMinBounds()[index];

			double pitch = (base.baseCoords[index + 1][index] - base.baseCoords[0][index])
					/ (linear_slicing);

			for (int i = 0; i < linear_slicing + 1; i++) {
				// lines and labels slicing are the same
				linesSlicing[i] = min + i * pitch;
				labelsSlicing[i] = min + i * pitch;
			}
		} else if (base.getAxeScale(index).equalsIgnoreCase(Base.STRINGS)) {

			if (stringMap == null) {
				stringMap = new HashMap<String, Double>();
				stringMap.put("?", 1.0);
			}

			linesSlicing = new double[stringMap.size()];
			labelsSlicing = new double[stringMap.size()];
			lightLabelNames = new String[stringMap.size()];

			int i = 0;
			for (String string : stringMap.keySet()) {
				// System.out.println(string+" : "+stringMap.get(string));
				linesSlicing[i] = stringMap.get(string);
				labelsSlicing[i] = stringMap.get(string);
				lightLabelNames[i] = string;
				i++;
			}
		}

		// System.out.println("linesSlicing: "+Array.toString(linesSlicing));
		// System.out.println("labelsSlicing: "+Array.toString(labelsSlicing));
	}

	/*
	 * MAIN METHOD(for testing)
	 */

	public static void main(String[] args) {
		Plot3DPanel p = new Plot3DPanel();
		Object[][] XYZ = new Object[8][3];
		Object[][] XYZ2 = new Object[10][3];

		for (int j = 0; j < XYZ.length; j++) {
			XYZ[j][0] = Math.random();
			XYZ[j][1] = Math.random();
			XYZ[j][2] = "" + ((char) ('a' + j));
		}

		for (int j = 0; j < XYZ2.length; j++) {
			XYZ2[j][0] = Math.random();
			XYZ2[j][1] = Math.random();
			XYZ2[j][2] = "" + ((char) ('a' + j));
		}

		p.addScatterPlot("toto", p.mapData(XYZ));
		p.addScatterPlot("toti", p.mapData(XYZ2));
		p.setAxisScale(1, "log");

		new FrameView(p).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		HashMap<String, Double> arg = p.getAxis(2).getStringMap();
		Collection<Double> ouch = arg.values();
		Iterator<Double> it = ouch.iterator();
		while (it.hasNext()) {
			System.out.println(it.next());
		}
	}
}