/*******************************************************************************
 * Copyright (c) 2008, 2020 SWTChart project.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 * 
 * Contributors:
 * yoshitaka - initial API and implementation
 * Christoph Läubrich - add support for datamodel
 * Frank Buloup = Internationalization
 *******************************************************************************/
package org.eclipse.swtchart.internal.series;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swtchart.Chart;
import org.eclipse.swtchart.IAxis.Direction;
import org.eclipse.swtchart.IBarSeries;
import org.eclipse.swtchart.Range;
import org.eclipse.swtchart.internal.axis.Axis;
import org.eclipse.swtchart.internal.compress.CompressBarSeries;
import org.eclipse.swtchart.internal.compress.CompressScatterSeries;
import org.eclipse.swtchart.model.CartesianSeriesModel;
import org.eclipse.swtchart.model.DoubleArraySeriesModel;

/**
 * Bar series.
 */
public class BarSeries<T> extends Series<T> implements IBarSeries<T> {

	/** the riser index in a category */
	private int riserIndex;
	/** the riser color */
	private Color barColor;
	/** the bar width */
	private int barWidth;
	/** the padding */
	private int padding;
	/** the overlay style */
	private boolean barOverlay;
	/** the bar width style */
	private BarWidthStyle barWidthStyle;
	/** the initial bar width in pixels */
	public static final int INITIAL_BAR_WIDTH = 20;
	/** the initial bar padding in percentage */
	public static final int INITIAL_PADDING = 20;
	/** the alpha value */
	private static final int ALPHA = 0xD0;
	/** the margin in pixels attached at the minimum/maximum plot */
	private static final int MARGIN_AT_MIN_MAX_PLOT = 6;
	/** the default bar color */
	private static final int DEFAULT_BAR_COLOR = SWT.COLOR_CYAN;

	/**
	 * Constructor.
	 *
	 * @param chart
	 *            the chart
	 * @param id
	 *            the series id
	 */
	protected BarSeries(Chart chart, String id) {

		super(chart, id);
		barColor = Display.getDefault().getSystemColor(DEFAULT_BAR_COLOR);
		barWidthStyle = BarWidthStyle.STRETCHED;
		barWidth = INITIAL_PADDING;
		padding = INITIAL_PADDING;
		barOverlay = false;
		type = SeriesType.BAR;
		compressor = new CompressBarSeries();
	}

	@Override
	public BarWidthStyle getBarWidthStyle(BarWidthStyle style) {

		return barWidthStyle;
	}

	@Override
	public void setBarWidthStyle(BarWidthStyle style) {

		this.barWidthStyle = style;
	}

	@Override
	public int getBarWidth() {

		return barWidth;
	}

	@Override
	public void setBarWidth(int width) {

		if(width <= 0) {
			SWT.error(SWT.ERROR_INVALID_ARGUMENT);
		}
		this.barWidth = width;
	}

	@Override
	public int getBarPadding() {

		return padding;
	}

	@Override
	public void setBarPadding(int padding) {

		if(padding < 0 || padding > 100) {
			SWT.error(SWT.ERROR_INVALID_ARGUMENT);
		}
		this.padding = padding;
	}

	@Override
	public boolean isBarOverlay() {

		return barOverlay;
	}

	@Override
	public void setBarOverlay(boolean barOverlay) {

		this.barOverlay = barOverlay;
	}

	@Override
	public Color getBarColor() {

		if(barColor.isDisposed()) {
			barColor = Display.getDefault().getSystemColor(DEFAULT_BAR_COLOR);
		}
		return barColor;
	}

	@Override
	public void setBarColor(Color color) {

		if(color != null && color.isDisposed()) {
			SWT.error(SWT.ERROR_INVALID_ARGUMENT);
		}
		if(color == null) {
			this.barColor = Display.getDefault().getSystemColor(DEFAULT_BAR_COLOR);
		} else {
			this.barColor = color;
		}
	}

	@Override
	public Rectangle[] getBounds() {

		Rectangle[] compressedBounds = getBoundsForCompressedSeries();
		if(((Axis)chart.getAxisSet().getXAxis(xAxisId)).isValidCategoryAxis()) {
			return compressedBounds;
		}
		double[] xSeries = getXSeries();
		Rectangle[] rs = new Rectangle[xSeries.length];
		double[] comporessedXSeries = compressor.getCompressedXSeries();
		int cnt = 0;
		for(int i = 0; i < xSeries.length; i++) {
			if(cnt < comporessedXSeries.length && comporessedXSeries[cnt] == xSeries[i]) {
				if(compressedBounds[cnt].width != 0 && compressedBounds[cnt].height != 0) {
					rs[i] = compressedBounds[cnt];
				}
				cnt++;
			}
		}
		return rs;
	}

	/**
	 * Gets the array of bar rectangles for compressed series.
	 *
	 * @return the array of bar rectangles for compressed series
	 */
	public Rectangle[] getBoundsForCompressedSeries() {

		Axis xAxis = (Axis)chart.getAxisSet().getXAxis(xAxisId);
		Axis yAxis = (Axis)chart.getAxisSet().getYAxis(yAxisId);
		// get x and y series
		double[] xseries = compressor.getCompressedXSeries();
		double[] yseries = compressor.getCompressedYSeries();
		int[] indexes = compressor.getCompressedIndexes();
		if(xAxis.isValidCategoryAxis()) {
			for(int i = 0; i < xseries.length; i++) {
				xseries[i] = indexes[i];
			}
		}
		Rectangle[] rectangles = new Rectangle[xseries.length];
		Range xRange = xAxis.getRange();
		Range yRange = yAxis.getRange();
		for(int i = 0; i < xseries.length; i++) {
			int x = xAxis.getPixelCoordinate(xseries[i]);
			int y = yAxis.getPixelCoordinate(isValidStackSeries() ? stackSeries[indexes[i]] : yseries[i]);
			double baseYCoordinate = yAxis.getRange().lower > 0 ? yAxis.getRange().lower : 0;
			double riserwidth = getRiserWidth(xseries, i, xAxis, xRange.lower, xRange.upper);
			double riserHeight = Math.abs(yAxis.getPixelCoordinate(yseries[i], yRange.lower, yRange.upper) - yAxis.getPixelCoordinate(yAxis.isLogScaleEnabled() ? yRange.lower : baseYCoordinate, yRange.lower, yRange.upper));
			// adjust riser x coordinate and riser width for multiple series
			int riserCnt = xAxis.getNumRisers();
			if(riserCnt > 1 && !barOverlay) {
				if(xAxis.isHorizontalAxis()) {
					x = (int)(x - riserwidth / 2d + riserwidth / riserCnt * (riserIndex + 0.5));
				} else {
					x = (int)(x - riserwidth / 2d + riserwidth / riserCnt * (riserCnt - riserIndex - 0.5));
				}
				riserwidth /= riserCnt;
			}
			if(xAxis.isHorizontalAxis()) {
				// adjust coordinate for negative series
				if(y > yAxis.getPixelCoordinate(0)) {
					y = yAxis.getPixelCoordinate(0);
				}
				int width = (int)Math.ceil(riserwidth);
				width = (width == 0) ? 1 : width;
				rectangles[i] = getVisibleRectangle((int)Math.floor(x - riserwidth / 2d), y, width, (int)riserHeight);
			} else {
				// adjust coordinate for negative series
				if(y < yAxis.getPixelCoordinate(0)) {
					y = yAxis.getPixelCoordinate(0);
				}
				int height = (int)Math.ceil(riserwidth);
				height = (height == 0) ? 1 : height;
				rectangles[i] = getVisibleRectangle((int)(y - riserHeight), (int)Math.floor(x - riserwidth / 2d), (int)riserHeight, height);
			}
		}
		return rectangles;
	}

	/**
	 * Gets the rectangle that is visible part of given rectangle.
	 *
	 * @param x
	 *            The x coordinate
	 * @param y
	 *            The y coordinate
	 * @param width
	 *            the width
	 * @param height
	 *            The height
	 * @return The visible rectangle
	 */
	private Rectangle getVisibleRectangle(int x, int y, int width, int height) {

		final int offset = 5;
		int newX = x;
		int newY = y;
		int newWidth = width;
		int newHeight = height;
		if(x < 0) {
			newX = -offset;
			newWidth += x + offset;
		}
		if(y < 0) {
			newY = -offset;
			newHeight += y + offset;
		}
		Point size = chart.getPlotArea().getSize();
		if(x + width > size.x) {
			newWidth -= x + width - size.x + offset;
			if(newWidth < 0) {
				newWidth = 0;
			}
		}
		if(y + height > size.y) {
			newHeight -= y + height - size.y + offset;
			if(newHeight < 0) {
				newHeight = 0;
			}
		}
		return new Rectangle(newX, newY, newWidth, newHeight);
	}

	/**
	 * Sets the index of riser in a category.
	 *
	 * @param riserIndex
	 *            the index of riser in a category
	 */
	protected void setRiserIndex(int riserIndex) {

		this.riserIndex = riserIndex;
	}

	@Override
	protected void setCompressor() {

		CartesianSeriesModel<T> dataModel = getDataModel();
		if(dataModel instanceof DoubleArraySeriesModel) {
			if(((DoubleArraySeriesModel)dataModel).isXMonotoneIncreasing()) {
				compressor = new CompressBarSeries();
				return;
			}
		}
		compressor = new CompressScatterSeries();
	}

	@Override
	public Range getAdjustedRange(Axis axis, int length) {

		// calculate a range which has margin
		Range range;
		int lowerPlotMargin;
		int upperPlotMargin;
		if(axis.getDirection() == Direction.X) {
			double[] xSeries = getXSeries();
			Range xRange = getXRange();
			double lowerRiserWidth = getRiserWidth(xSeries, 0, axis, xRange.lower, xRange.upper);
			double upperRiserWidth = getRiserWidth(xSeries, xSeries.length - 1, axis, xRange.lower, xRange.upper);
			lowerPlotMargin = (int)(lowerRiserWidth / 2d + MARGIN_AT_MIN_MAX_PLOT);
			upperPlotMargin = (int)(upperRiserWidth / 2d + MARGIN_AT_MIN_MAX_PLOT);
			range = getXRange();
		} else {
			range = getYRange();
			if(range.upper < 0) {
				range.upper = 0;
			}
			if(range.lower > 0) {
				range.lower = axis.isLogScaleEnabled() ? range.lower : 0;
			}
			lowerPlotMargin = (range.lower == 0) ? 0 : MARGIN_AT_MIN_MAX_PLOT;
			upperPlotMargin = (range.upper == 0) ? 0 : MARGIN_AT_MIN_MAX_PLOT;
		}
		return getRangeWithMargin(lowerPlotMargin, upperPlotMargin, length, axis, range);
	}

	/**
	 * Gets the riser width.
	 *
	 * @param series
	 *            the X series
	 * @param index
	 *            the series index
	 * @param xAxis
	 *            the X axis
	 * @param min
	 *            the min value of range
	 * @param max
	 *            the max value of range
	 * @return the raiser width in pixels
	 */
	private int getRiserWidth(double[] series, int index, Axis xAxis, double min, double max) {

		if(series.length == 0) {
			return 1;
		}
		// get two x coordinates
		double upper;
		double lower;
		if(series.length == 1) {
			upper = series[0] + 0.5;
			lower = series[0] - 0.5;
		} else if(index != series.length - 1 && (index == 0 || series[index + 1] - series[index] < series[index] - series[index - 1])) {
			upper = series[index + 1];
			lower = series[index];
		} else {
			upper = series[index];
			lower = series[index - 1];
		}
		if(barWidthStyle == BarWidthStyle.STRETCHED) {
			// get riser width without padding
			int width = Math.abs(xAxis.getPixelCoordinate(upper, min, max) - xAxis.getPixelCoordinate(lower, min, max));
			// adjust for padding
			width *= (100 - padding) / 100d;
			// symbol size should be at least more than 1
			if(width == 0) {
				width = 1;
			}
			return width;
		} else if(barWidthStyle == BarWidthStyle.FIXED) {
			return barWidth;
		}
		throw new IllegalStateException(Messages.getString(Messages.UNKNOWN_BAR_WIDTH_STYLE));
	}

	/**
	 * Gets the color for riser frame. The color will be darker or lighter than
	 * the given color.
	 *
	 * @param color
	 *            the riser color
	 * @return the riser frame color
	 */
	private static Color getFrameColor(Color color) {

		int red = color.getRed();
		int green = color.getGreen();
		int blue = color.getBlue();
		red *= (red > 128) ? 0.8 : 1.2;
		green *= (green > 128) ? 0.8 : 1.2;
		blue *= (blue > 128) ? 0.8 : 1.2;
		return new Color(color.getDevice(), red, green, blue);
	}

	@Override
	protected void draw(GC gc, int width, int height, Axis xAxis, Axis yAxis) {

		// draw riser
		Rectangle[] rs = getBoundsForCompressedSeries();
		for(int i = 0; i < rs.length; i++) {
			drawRiser(gc, rs[i].x, rs[i].y, rs[i].width, rs[i].height);
		}
		// draw label and error bars
		if(seriesLabel.isVisible() || xErrorBar.isVisible() || yErrorBar.isVisible()) {
			double[] xSeries = getXSeries();
			double[] ySeries = getYSeries();
			double[] yseries = compressor.getCompressedYSeries();
			int[] indexes = compressor.getCompressedIndexes();
			for(int i = 0; i < rs.length; i++) {
				seriesLabel.draw(gc, rs[i].x + rs[i].width / 2, rs[i].y + rs[i].height / 2, yseries[i], indexes[i], SWT.CENTER);
				int h, v;
				if(xAxis.isHorizontalAxis()) {
					if(xAxis.isCategoryEnabled()) {
						h = rs[i].x + rs[i].width / 2;
					} else {
						h = xAxis.getPixelCoordinate(xSeries[indexes[i]]);
					}
					v = yAxis.getPixelCoordinate(ySeries[indexes[i]]);
				} else {
					if(xAxis.isCategoryEnabled()) {
						v = rs[i].y + rs[i].height / 2;
					} else {
						v = xAxis.getPixelCoordinate(xSeries[indexes[i]]);
					}
					h = yAxis.getPixelCoordinate(ySeries[indexes[i]]);
				}
				xErrorBar.draw(gc, h, v, xAxis, indexes[i]);
				yErrorBar.draw(gc, h, v, yAxis, indexes[i]);
			}
		}
	}

	/**
	 * Draws riser.
	 *
	 * @param gc
	 *            the graphics context
	 * @param h
	 *            the horizontal coordinate
	 * @param v
	 *            the vertical coordinate
	 * @param width
	 *            the riser width
	 * @param height
	 *            the riser height
	 */
	private void drawRiser(GC gc, int h, int v, int width, int height) {

		int alpha = gc.getAlpha();
		gc.setAlpha(ALPHA);
		Color oldBackground = gc.getBackground();
		gc.setBackground(getBarColor());
		gc.fillRectangle(h, v, width, height);
		gc.setLineStyle(SWT.LINE_SOLID);
		Color frameColor = getFrameColor(getBarColor());
		Color oldForeground = gc.getForeground();
		gc.setForeground(frameColor);
		gc.drawRectangle(h, v, width, height);
		frameColor.dispose();
		gc.setAlpha(alpha);
		gc.setBackground(oldBackground);
		gc.setForeground(oldForeground);
	}
}
