/*******************************************************************************
 * Copyright (c) 2008-2016 SWTChart project. All rights reserved.
 *
 * This code is distributed under the terms of the Eclipse Public License v1.0
 * which is available at http://www.eclipse.org/legal/epl-v10.html
 *******************************************************************************/
package org.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.swtchart.Chart;
import org.swtchart.IAxis.Direction;
import org.swtchart.IBarSeries;
import org.swtchart.Range;
import org.swtchart.internal.axis.Axis;
import org.swtchart.internal.compress.CompressBarSeries;
import org.swtchart.internal.compress.CompressScatterSeries;

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

    /** 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 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;
        type = SeriesType.BAR;

        compressor = new CompressBarSeries();
    }

    /*
     * @see IBarSeries#getBarWidthStyle(BarWidthStyle)
     */
    public BarWidthStyle getBarWidthStyle(BarWidthStyle style) {
        return barWidthStyle;
    }

    /*
     * @see IBarSeries#setBarWidthStyle(BarWidthStyle)
     */
    public void setBarWidthStyle(BarWidthStyle style) {
        this.barWidthStyle = style;
    }

    /*
     * @see IBarSeries#getBarWidth()
     */
    public int getBarWidth() {
        return barWidth;
    }

    /*
     * @see IBarSeries#setBarWidth(int)
     */
    public void setBarWidth(int width) {
        if (padding <= 0) {
            SWT.error(SWT.ERROR_INVALID_ARGUMENT);
        }
        this.barWidth = width;
    }

    /*
     * @see IBarSeries#getBarPadding()
     */
    public int getBarPadding() {
        return padding;
    }

    /*
     * @see IBarSeries#setBarPadding(int)
     */
    public void setBarPadding(int padding) {
        if (padding < 0 || padding > 100) {
            SWT.error(SWT.ERROR_INVALID_ARGUMENT);
        }
        this.padding = padding;
    }

    /*
     * @see IBarSeries#getBarColor()
     */
    public Color getBarColor() {
        if (barColor.isDisposed()) {
            barColor = Display.getDefault().getSystemColor(DEFAULT_BAR_COLOR);
        }
        return barColor;
    }

    /*
     * @see IBarSeries#setBarColor(Color)
     */
    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;
        }
    }

    /*
     * @see IBarSeries#getBounds()
     */
    public Rectangle[] getBounds() {
        Rectangle[] compressedBounds = getBoundsForCompressedSeries();
        if (((Axis) chart.getAxisSet().getXAxis(xAxisId)).isValidCategoryAxis()) {
            return compressedBounds;
        }

        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
     */
    private 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) {
                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;
    }

    /*
     * @see Series#setCompressor()
     */
    @Override
    protected void setCompressor() {
        if (isXMonotoneIncreasing) {
            compressor = new CompressBarSeries();
        } else {
            compressor = new CompressScatterSeries();
        }
    }

    /*
     * @see Series#getAdjustedRange(Axis, int)
     */
    @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 lowerRiserWidth = getRiserWidth(xSeries, 0, axis, minX, maxX);
            double upperRiserWidth = getRiserWidth(xSeries, xSeries.length - 1,
                    axis, minX, maxX);
            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() ? minY : 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) {

        // 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("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);
    }

    /*
     * @see Series#draw(GC, int, int, Axis, Axis)
     */
    @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[] 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);
    }
}
