/*
 * GRAL: GRAphing Library for Java(R)
 *
 * (C) Copyright 2009-2015 Erich Seifert <dev[at]erichseifert.de>,
 * Michael Seifert <mseifert[at]error-reports.org>
 *
 * This file is part of GRAL.
 *
 * GRAL is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * GRAL is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with GRAL.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.erichseifert.gral.plots;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;

import de.erichseifert.gral.data.DataSource;
import de.erichseifert.gral.data.DummyData;
import de.erichseifert.gral.data.Row;
import de.erichseifert.gral.graphics.AbstractDrawable;
import de.erichseifert.gral.graphics.Drawable;
import de.erichseifert.gral.graphics.DrawingContext;
import de.erichseifert.gral.plots.areas.AreaRenderer;
import de.erichseifert.gral.plots.axes.Axis;
import de.erichseifert.gral.plots.axes.AxisRenderer;
import de.erichseifert.gral.plots.axes.LinearRenderer2D;
import de.erichseifert.gral.plots.colors.ColorMapper;
import de.erichseifert.gral.plots.legends.ValueLegend;
import de.erichseifert.gral.plots.lines.LineRenderer;
import de.erichseifert.gral.plots.points.DefaultPointRenderer2D;
import de.erichseifert.gral.plots.points.PointData;
import de.erichseifert.gral.plots.points.PointRenderer;
import de.erichseifert.gral.util.GraphicsUtils;
import de.erichseifert.gral.graphics.Location;
import de.erichseifert.gral.util.MathUtils;
import de.erichseifert.gral.util.PointND;
import de.erichseifert.gral.util.SerializationUtils;


/**
 * <p>Class that displays data in a bar plot.</p>
 * <p>To create a new {@code BarPlot} simply create a new instance
 * using one or more data sources. Example:</p>
 * <pre>
 * DataTable data = new DataTable(Integer.class, Double.class);
 * data.add(2010, -5.00);
 * data.add(2011,  3.25);
 * data.add(2012, -0.50);
 * data.add(2012,  4.00);
 *
 * BarPlot plot = new BarPlot(data);
 * </pre>
 */
public class BarPlot extends XYPlot {
	/** Version id for serialization. */
	private static final long serialVersionUID = 3177733647455649147L;

	/** Relative width of the bars. 1.0 means the bars touch each other
	 * without gap. */
	private double barWidth;
	/** Minimal height of the bars in pixels. */
	private double barHeightMin;
	/** Decides whether the bars should be filled as a whole, or each bar on
	 * its own. This can e.g. be important for gradients. */
	private boolean paintAllBars;

	/**
	 * Class that renders a bar in a bar plot.
	 */
	public static class BarRenderer extends DefaultPointRenderer2D {
		/** Version id for serialization. */
		private static final long serialVersionUID = 2183638342305398522L;

		/** Plot that contains settings and renderers. */
		private final BarPlot plot;

		/** Stroke to draw the border of the bar. */
		// Custom serialization will be done with a wrapper object
		private transient Stroke borderStroke;
		/** Color to fill the border of the bar. */
		private Paint borderColor;

		/**
		 * Constructor that creates a new instance and initializes it with a
		 * plot as data provider.
		 * @param plot The associated plot.
		 */
		public BarRenderer(BarPlot plot) {
			this.plot = plot;
			setValueLocation(Location.NORTH);
			borderStroke = null;
			borderColor = Color.BLACK;
		}

		/**
		 * Custom deserialization method.
		 * @param in Input stream.
		 * @throws ClassNotFoundException if a serialized class doesn't exist anymore.
		 * @throws IOException if there is an error while reading data from the
		 *         input stream.
		 */
		private void readObject(ObjectInputStream in)
				throws ClassNotFoundException, IOException {
			// Default deserialization
			in.defaultReadObject();
			// Custom deserialization
			borderStroke = (Stroke) SerializationUtils.unwrap(
					(Serializable) in.readObject());
		}

		/**
		 * Custom serialization method.
		 * @param out Output stream.
		 * @throws ClassNotFoundException if a serialized class doesn't exist.
		 * @throws IOException if there is an error while writing data to the
		 *         output stream.
		 */
		private void writeObject(ObjectOutputStream out)
				throws ClassNotFoundException, IOException {
			// Default serialization
			out.defaultWriteObject();
			// Custom serialization
			out.writeObject(SerializationUtils.wrap(borderStroke));
		}

		/**
		 * Returns the stroke used to paint the outline of the point shape.
		 * @return Stroke used to paint the outline of the point shape.
		 */
		public Stroke getBorderStroke() {
			return borderStroke;
		}

		/**
		 * Sets the stroke used to paint the outline of the point shape.
		 * @param stroke Stroke used to paint the outline of the point shape.
		 */
		public void setBorderStroke(Stroke stroke) {
			this.borderStroke = stroke;
		}

		/**
		 * Returns the paint which is used to fill the point shape.
		 * @return Paint which is used to fill the point shape.
		 */
		public Paint getBorderColor() {
			return borderColor;
		}

		/**
		 * Sets the paint which will be used to fill the point shape.
		 * @param color Paint which will be used to fill the point shape.
		 */
		public void setBorderColor(Paint color) {
			this.borderColor = color;
		}

		/**
		 * Returns the graphical representation to be drawn for the specified data
		 * value.
		 * @param data Information on axes, renderers, and values.
		 * @param shape Outline that describes the point's shape.
		 * @return Component that can be used to draw the point
		 */
		@Override
		public Drawable getPoint(final PointData data, final Shape shape) {
			return new AbstractDrawable() {
				/** Version id for serialization. */
				private static final long serialVersionUID = -3145112034673683520L;

				public void draw(DrawingContext context) {
					BarRenderer renderer = BarRenderer.this;

					Row row = data.row;

					Rectangle2D paintBoundaries = null;
					Graphics2D graphics = context.getGraphics();

					ColorMapper colors = renderer.getColor();
					Paint paint = colors.get(row.getIndex());

					if (plot.isPaintAllBars()) {
						AffineTransform txOld = graphics.getTransform();
						Rectangle2D shapeBounds = shape.getBounds2D();
						paintBoundaries = new Rectangle2D.Double();//plot.getPlotArea().getBounds();
						paintBoundaries = new Rectangle2D.Double(
							shapeBounds.getX(), paintBoundaries.getY() - txOld.getTranslateY(),
							shapeBounds.getWidth(), paintBoundaries.getHeight()
						);
					}

					GraphicsUtils.fillPaintedShape(
						graphics, shape, paint, paintBoundaries);

					Stroke stroke = renderer.getBorderStroke();
					Paint strokePaint = renderer.getBorderColor();
					if (stroke != null && strokePaint != null) {
						GraphicsUtils.drawPaintedShape(
							graphics, shape, strokePaint, null, stroke);
					}
				}
			};
		}

		/**
		 * Returns a {@code Shape} instance that can be used for further
		 * calculations.
		 * @param data Information on axes, renderers, and values.
		 * @return Outline that describes the point's shape.
		 */
		@Override
		public Shape getPointShape(PointData data) {
			int colX = 0;
			int colY = 1;

			Axis axisX = data.axes.get(0);
			Axis axisY = data.axes.get(1);
			AxisRenderer axisXRenderer = data.axisRenderers.get(0);
			AxisRenderer axisYRenderer = data.axisRenderers.get(1);
			Row row = data.row;

			if (!row.isColumnNumeric(colX) || !row.isColumnNumeric(colY)) {
				return null;
			}

			double valueX = ((Number) row.get(colX)).doubleValue();
			double valueY = ((Number) row.get(colY)).doubleValue();
			double axisYOrigin = 0.0;

			double barWidthRel = plot.getBarWidth();
			barWidthRel = Math.max(barWidthRel, 0.0);
			double barAlign = 0.5;

			double barXMin = axisXRenderer
				.getPosition(axisX, valueX - barWidthRel*barAlign, true, false)
				.get(PointND.X);
			double barXMax = axisXRenderer
				.getPosition(axisX, valueX + barWidthRel*barAlign, true, false)
				.get(PointND.X);

			double barYVal = axisYRenderer.getPosition(
					axisY, valueY, true, false).get(PointND.Y);
			double barYOrigin = axisYRenderer.getPosition(
					axisY, axisYOrigin, true, false).get(PointND.Y);
			double barYMin = Math.min(barYVal, barYOrigin);
			double barYMax = Math.max(barYVal, barYOrigin);

			double barWidth = Math.abs(barXMax - barXMin);
			double barHeight = Math.abs(barYMax - barYMin);

			// position of the bar's left edge in screen coordinates
			double barX = axisXRenderer.getPosition(
				axisX, valueX, true, false).get(PointND.X);
			// position of the bar's upper edge in screen coordinates
			// (the origin of the screen y axis is at the top)
			boolean barAboveAxis = barYMax == barYOrigin;
			double barY = barAboveAxis ? 0.0 : -barHeight;

			double barHeightMin = plot.getBarHeightMin();
			if (MathUtils.isCalculatable(barHeightMin) && barHeightMin > 0.0 &&
					barHeight < barHeightMin) {
				if (barAboveAxis) {
					barY += -barHeightMin + barHeight;
				}
				barHeight = barHeightMin;
			}

			Shape shape = getBarShape(
				barXMin - barX, barY, barWidth, barHeight);
			return shape;
		}

		/**
		 * Returns the shape for a bar. The default shape is defined in the
		 * settings, but more complex shapes may be implemented by overriding
		 * this method.
		 * @param x Distance from the left in view units (e.g. pixels).
		 * @param y Distance from the top in view units (e.g. pixels).
		 * @param width Width of the shape in view units (e.g. pixels).
		 * @param height Height of the shape in view units (e.g. pixels).
		 * @return A geometric shape for displaying a bar in bar plot.
		 */
		protected Shape getBarShape(double x, double y, double width, double height) {
			Shape shape = getShape();
			Rectangle2D shapeBounds = shape.getBounds2D();

			AffineTransform tx = new AffineTransform();
			tx.translate(x, y);
			tx.scale(width/shapeBounds.getWidth(), height/shapeBounds.getHeight());
			tx.translate(-shapeBounds.getMinX(), -shapeBounds.getMinY());

			Shape shapeTransformed = tx.createTransformedShape(shape);
			return shapeTransformed;
		}

		/**
		 * Returns a graphical representation of the value label to be drawn for
		 * the specified data value.
		 * @param data Information on axes, renderers, and values.
		 * @param shape Outline that describes the bounds for the value label.
		 * @return Component that can be used to draw the value label.
		 */
		@Override
		public Drawable getValue(final PointData data, final Shape shape) {
			Drawable drawable = new AbstractDrawable() {
				/** Version id for serialization. */
				private static final long serialVersionUID = -1133369168849171793L;

				public void draw(DrawingContext context) {
					PointRenderer renderer = BarRenderer.this;
					Row row = data.row;

					if (renderer.isValueVisible()) {
						int colValue = renderer.getValueColumn();
						drawValueLabel(context, shape, row, colValue);
					}
				}
			};
			return drawable;
		}
	}

	/**
	 * A legend implementation for bar plots that displays all values of the
	 * data source as items.
	 */
	public static class BarPlotLegend extends ValueLegend {
		/** Version id for serialization. */
		private static final long serialVersionUID = 4752278896167602641L;
		/** Source for dummy data. */
		private static final DataSource DUMMY_DATA = new DummyData(2, 1, 0.5);

		/** Plot that contains settings and renderers. */
		private final BarPlot plot;

		/**
		 * Constructor that initializes the instance with a plot acting as a
		 * provider for settings and renderers.
		 * @param plot Plot.
		 */
		public BarPlotLegend(BarPlot plot) {
			this.plot = plot;
		}

		/**
		 * Returns a symbol for rendering a legend item.
		 * @param row Data row.
		 * @return A drawable object that can be used to display the symbol.
		 */
		public Drawable getSymbol(final Row row) {
			return new AbstractSymbol(this) {
				/** Version id for serialization. */
				private static final long serialVersionUID = 5744026898590787285L;

				public void draw(DrawingContext context) {
					DataSource data = row.getSource();

					Row symbolRow = new Row(DUMMY_DATA, row.getIndex());
					Rectangle2D bounds = getBounds();

					double barWidthRel = plot.getBarWidth();

					Axis axisX = new Axis(0.5 - barWidthRel/2.0, 0.5 + barWidthRel/2.0);
					AxisRenderer axisRendererX = new LinearRenderer2D();
					axisRendererX.setShape(new Line2D.Double(
							bounds.getMinX(), bounds.getMaxY(),
							bounds.getMaxX(), bounds.getMaxY()));
					Axis axisY = new Axis(0.0, 0.5);
					AxisRenderer axisRendererY = new LinearRenderer2D();
					axisRendererY.setShape(new Line2D.Double(
							bounds.getMinX(), bounds.getMaxY(),
							bounds.getMinX(), bounds.getMinY()));

					PointData pointData = new PointData(
						Arrays.asList(axisX, axisY),
						Arrays.asList(axisRendererX, axisRendererY),
						symbolRow, 0);

					// TODO: Provide a means to set the PointRenderer used for the Legend
					PointRenderer pointRenderer = null;
					List<PointRenderer> pointRenderers = plot.getPointRenderers(data);
					if (!pointRenderers.isEmpty()) {
						pointRenderer = pointRenderers.get(0);
					}
					Drawable drawable = null;
					if (pointRenderer != null) {
						Shape shape = pointRenderer.getPointShape(pointData);
						drawable = pointRenderer.getPoint(pointData, shape);
					}

					DataPoint point = new DataPoint(pointData,
						new PointND<Double>(bounds.getCenterX(),
							bounds.getMinY()));

					if (drawable != null) {
						Graphics2D graphics = context.getGraphics();
						Point2D pos = point.position.getPoint2D();
						AffineTransform txOrig = graphics.getTransform();
						graphics.translate(pos.getX(), pos.getY());
						drawable.draw(context);
						graphics.setTransform(txOrig);
					}
				}
			};
		}
	}

	/**
	 * Creates a new instance and initializes it with the specified
	 * data sources.
	 * @param data Data to be displayed.
	 */
	public BarPlot(DataSource... data) {
		super(data);

		((XYPlotArea2D) getPlotArea()).setMajorGridX(false);
		barWidth = 1.0;
		barHeightMin = 0.0;
		paintAllBars = false;

		setLegend(new BarPlotLegend(this));

		autoscaleAxes();
	}

	@Override
	public void autoscaleAxis(String axisName) {
		if (!AXIS_X.equals(axisName) && !AXIS_Y.equals(axisName)) {
			super.autoscaleAxis(axisName);
		}
		Axis axis = getAxis(axisName);
		if (axis == null || !axis.isAutoscaled()) {
			return;
		}

		List<DataSource> sources = getData();
		if (sources.isEmpty()) {
			return;
		}

		int rowCount = 0;
		for (DataSource data : sources) {
			rowCount = Math.max(rowCount, data.getRowCount());
		}
		if (rowCount == 0) {
			return;
		}

		double min = getAxisMin(axisName);
		double max = getAxisMax(axisName);
		double spacing = 0.0;
		if (AXIS_X.equals(axisName)) {
			// Add margin
			double barWidth = getBarWidth();
			double margin = barWidth*(max - min)/rowCount;
			spacing = margin/2.0;
		} else {
			// Make sure 0 is always visible for y axis
			min = Math.min(min, 0.0);
			max = Math.max(max, 0.0);
		}
		axis.setRange(min - spacing, max + spacing);
	}

	@Override
	public void add(int index, DataSource source, boolean visible) {
		super.add(index, source, visible);

		// Assign default renderers
		PointRenderer pointRendererDefault = new BarRenderer(this);
		LineRenderer lineRendererDefault = null;
		AreaRenderer areaRendererDefault = null;
		// FIXME: Overwrites possible present point and line renderers
		setPointRenderers(source, pointRendererDefault);
		setLineRenderers(source, lineRendererDefault);
		setAreaRenderers(source, areaRendererDefault);
	}

	/**
	 * Returns the width of the bars in axis coordinates.
	 * @return Width of the bars in axis coordinates.
	 */
	public double getBarWidth() {
		return barWidth;
	}

	/**
	 * Sets the width of the bars in axis coordinates.
	 * @param barWidth Width of the bars in axis coordinates.
	 */
	public void setBarWidth(double barWidth) {
		this.barWidth = barWidth;
	}

	/**
	 * Returns the minimum height of the bars in view units
	 * (e.g. pixels on screen).
	 * @return Minimum height of the bars in view units.
	 */
	public double getBarHeightMin() {
		return barHeightMin;
	}

	/**
	 * Sets the minimum height of the bars in view units
	 * (e.g. pixels on screen).
	 * @param barHeightMin Minimum height of the bars in view units.
	 */
	public void setBarHeightMin(double barHeightMin) {
		this.barHeightMin = barHeightMin;
	}

	/**
	 * Returns whether all bars are filled as a whole, or if each bar is filled
	 * independently.
	 * @return {@code true} if all bars are filled as a whole, or
	 * 		   {@code false} if each bar is filled independently.
	 */
	public boolean isPaintAllBars() {
		return paintAllBars;
	}

	/**
	 * Sets whether all bars will be filled as a whole, or if each bar will be
	 * filled independently.
	 * @param paintAllBars {@code true} to fill all bars as a whole, or
	 * 		   {@code false} to fill each bar independently.
	 */
	public void setPaintAllBars(boolean paintAllBars) {
		this.paintAllBars = paintAllBars;
	}
}
