/*******************************************************************************
 * 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 java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Event;
import org.swtchart.Chart;
import org.swtchart.IAxis;
import org.swtchart.IDisposeListener;
import org.swtchart.IErrorBar;
import org.swtchart.ISeries;
import org.swtchart.ISeriesLabel;
import org.swtchart.Range;
import org.swtchart.IAxis.Direction;
import org.swtchart.internal.axis.Axis;
import org.swtchart.internal.compress.ICompress;

/**
 * Series.
 */
abstract public class Series implements ISeries {

    /** the default series type */
    protected static final SeriesType DEFAULT_SERIES_TYPE = SeriesType.LINE;

    /** the x series */
    protected double[] xSeries;

    /** the y series */
    protected double[] ySeries;

    /** the minimum value of x series */
    protected double minX;

    /** the maximum value of x series */
    protected double maxX;

    /** the minimum value of y series */
    protected double minY;

    /** the maximum value of y series */
    protected double maxY;

    /** the series id */
    protected String id;

    /** the compressor */
    protected ICompress compressor;

    /** the x axis id */
    protected int xAxisId;

    /** the y axis id */
    protected int yAxisId;

    /** the visibility of series */
    protected boolean visible;

    /** the state indicating whether x series are monotone increasing */
    protected boolean isXMonotoneIncreasing;

    /** the series type */
    protected SeriesType type;

    /** the series label */
    protected SeriesLabel seriesLabel;

    /** the x error bar */
    protected ErrorBar xErrorBar;

    /** the y error bar */
    protected ErrorBar yErrorBar;

    /** the chart */
    protected Chart chart;

    /** the state indicating if the series is a stacked type */
    protected boolean stackEnabled;

    /** the stack series */
    protected double[] stackSeries;

    /** the state indicating if the type of X series is <tt>Date</tt> */
    private boolean isDateSeries;

    /** the state indicating if the series is visible in legend */
    private boolean visibleInLegend;

    /** the series description */
    private String description;

    /** the list of dispose listeners */
    private List<IDisposeListener> listeners;

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

        this.chart = chart;
        this.id = id;
        xAxisId = 0;
        yAxisId = 0;
        visible = true;
        type = DEFAULT_SERIES_TYPE;
        stackEnabled = false;
        isXMonotoneIncreasing = true;
        seriesLabel = new SeriesLabel();
        xErrorBar = new ErrorBar();
        yErrorBar = new ErrorBar();
        visibleInLegend = true;
        listeners = new ArrayList<IDisposeListener>();
        xSeries = new double[0];
        ySeries = new double[0];
    }

    /*
     * @see ISeries#getId()
     */
    public String getId() {
        return id;
    }

    /*
     * @see ISeries#setVisible(boolean)
     */
    public void setVisible(boolean visible) {
        if (this.visible == visible) {
            return;
        }

        this.visible = visible;

        ((SeriesSet) chart.getSeriesSet()).updateStackAndRiserData();
    }

    /*
     * @see ISeries#isVisible()
     */
    public boolean isVisible() {
        return visible;
    }

    /*
     * @see ISeries#getType()
     */
    public SeriesType getType() {
        return type;
    }

    /*
     * @see ISeries#isStackEnabled()
     */
    public boolean isStackEnabled() {
        return stackEnabled;
    }

    /*
     * @see ISeries#enableStack(boolean)
     */
    public void enableStack(boolean enabled) {
        if (enabled && minY < 0) {
            throw new IllegalStateException(
                    "Stacked series cannot contain minus values.");
        }

        if (stackEnabled == enabled) {
            return;
        }

        stackEnabled = enabled;

        ((SeriesSet) chart.getSeriesSet()).updateStackAndRiserData();
    }

    /*
     * @see ISeries#setXSeries(double[])
     */
    public void setXSeries(double[] series) {

        if (series == null) {
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
            return; // to suppress warning...
        }

        xSeries = new double[series.length];
        System.arraycopy(series, 0, xSeries, 0, series.length);
        isDateSeries = false;

        if (xSeries.length == 0) {
            return;
        }

        // find the min and max value of x series
        minX = xSeries[0];
        maxX = xSeries[0];
        for (int i = 1; i < xSeries.length; i++) {
            if (minX > xSeries[i]) {
                minX = xSeries[i];
            }
            if (maxX < xSeries[i]) {
                maxX = xSeries[i];
            }

            if (xSeries[i - 1] > xSeries[i]) {
                isXMonotoneIncreasing = false;
            }
        }

        setCompressor();

        compressor.setXSeries(xSeries);
        compressor.setYSeries(ySeries);

        if (minX <= 0) {
            IAxis axis = chart.getAxisSet().getXAxis(xAxisId);
            if (axis != null) {
                axis.enableLogScale(false);
            }
        }
    }

    /*
     * @see ISeries#getXSeries()
     */
    public double[] getXSeries() {
        double[] copiedSeries = new double[xSeries.length];
        System.arraycopy(xSeries, 0, copiedSeries, 0, xSeries.length);

        return copiedSeries;
    }

    /*
     * @see ISeries#setYSeries(double[])
     */
    public void setYSeries(double[] series) {

        if (series == null) {
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
            return; // to suppress warning...
        }

        ySeries = new double[series.length];
        System.arraycopy(series, 0, ySeries, 0, series.length);

        if (ySeries.length == 0) {
            return;
        }

        // find the min and max value of y series
        minY = ySeries[0];
        maxY = ySeries[0];
        for (int i = 1; i < ySeries.length; i++) {
            if (minY > ySeries[i]) {
                minY = ySeries[i];
            }
            if (maxY < ySeries[i]) {
                maxY = ySeries[i];
            }
        }

        if (xSeries.length != series.length) {
            xSeries = new double[series.length];
            for (int i = 0; i < series.length; i++) {
                xSeries[i] = i;
            }
            minX = xSeries[0];
            maxX = xSeries[xSeries.length - 1];
            isXMonotoneIncreasing = true;
        }

        setCompressor();

        compressor.setXSeries(xSeries);
        compressor.setYSeries(ySeries);

        if (minX <= 0) {
            IAxis axis = chart.getAxisSet().getXAxis(xAxisId);
            if (axis != null) {
                axis.enableLogScale(false);
            }
        }
        if (minY <= 0) {
            IAxis axis = chart.getAxisSet().getYAxis(yAxisId);
            if (axis != null) {
                axis.enableLogScale(false);
            }
            stackEnabled = false;
        }
    }

    /*
     * @see ISeries#getYSeries()
     */
    public double[] getYSeries() {
        double[] copiedSeries = new double[ySeries.length];
        System.arraycopy(ySeries, 0, copiedSeries, 0, ySeries.length);

        return copiedSeries;
    }

    /*
     * @see ISeries#setXDateSeries(Date[])
     */
    public void setXDateSeries(Date[] series) {
        if (series == null) {
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
            return; // to suppress warning...
        }

        double[] xDateSeries = new double[series.length];
        for (int i = 0; i < series.length; i++) {
            xDateSeries[i] = series[i].getTime();
        }
        setXSeries(xDateSeries);
        isDateSeries = true;
    }

    /*
     * @see ISeries#getXDateSeries()
     */
    public Date[] getXDateSeries() {
        if (!isDateSeries) {
            return new Date[0];
        }

        Date[] series = new Date[xSeries.length];
        for (int i = 0; i < series.length; i++) {
            series[i] = new Date((long) xSeries[i]);
        }
        return series;
    }

    /**
     * Gets the state indicating if date series is set.
     *
     * @return true if date series is set
     */
    public boolean isDateSeries() {
        return isDateSeries;
    }

    /**
     * Gets the state indicating if the series is valid stack series.
     *
     * @return true if the series is valid stack series
     */
    public boolean isValidStackSeries() {
        return stackEnabled
                && stackSeries != null
                && stackSeries.length > 0
                && !chart.getAxisSet().getYAxis(yAxisId).isLogScaleEnabled()
                && ((Axis) chart.getAxisSet().getXAxis(xAxisId))
                        .isValidCategoryAxis();
    }

    /**
     * Gets the X range of series.
     *
     * @return the X range of series
     */
    public Range getXRange() {
        return new Range(minX, maxX);
    }

    /**
     * Gets the adjusted range to show all series in screen. This range includes
     * the size of plot like symbol or bar.
     *
     * @param axis
     *            the axis
     * @param length
     *            the axis length in pixels
     * @return the adjusted range
     */
    abstract public Range getAdjustedRange(Axis axis, int length);

    /**
     * Gets the Y range of series.
     *
     * @return the Y range of series
     */
    public Range getYRange() {
        double min = minY;
        double max = maxY;
        Axis xAxis = (Axis) chart.getAxisSet().getXAxis(xAxisId);
        if (isValidStackSeries() && xAxis.isValidCategoryAxis()) {
            for (int i = 0; i < stackSeries.length; i++) {
                if (max < stackSeries[i]) {
                    max = stackSeries[i];
                }
            }
        }
        return new Range(min, max);
    }

    /**
     * Gets the compressor.
     *
     * @return the compressor
     */
    protected ICompress getCompressor() {
        return compressor;
    }

    /**
     * Sets the compressor.
     */
    abstract protected void setCompressor();

    /*
     * @see ISeries#getXAxisId()
     */
    public int getXAxisId() {
        return xAxisId;
    }

    /*
     * @see ISeries#setXAxisId(int)
     */
    public void setXAxisId(int id) {
        if (xAxisId == id) {
            return;
        }

        IAxis axis = chart.getAxisSet().getXAxis(xAxisId);

        if (minX <= 0 && axis != null && axis.isLogScaleEnabled()) {
            chart.getAxisSet().getXAxis(xAxisId).enableLogScale(false);
        }

        xAxisId = id;

        ((SeriesSet) chart.getSeriesSet()).updateStackAndRiserData();
    }

    /*
     * @see ISeries#getYAxisId()
     */
    public int getYAxisId() {
        return yAxisId;
    }

    /*
     * @see ISeries#setYAxisId(int)
     */
    public void setYAxisId(int id) {
        yAxisId = id;
    }

    /*
     * @see ISeries#getLabel()
     */
    public ISeriesLabel getLabel() {
        return seriesLabel;
    }

    /*
     * @see ISeries#getXErrorBar()
     */
    public IErrorBar getXErrorBar() {
        return xErrorBar;
    }

    /*
     * @see ISeries#getYErrorBar()
     */
    public IErrorBar getYErrorBar() {
        return yErrorBar;
    }

    /**
     * Sets the stack series
     *
     * @param stackSeries
     *            The stack series
     */
    protected void setStackSeries(double[] stackSeries) {
        this.stackSeries = stackSeries;
    }

    /*
     * @see ISeries#getPixelCoordinates(int)
     */
    public Point getPixelCoordinates(int index) {

        // get the horizontal and vertical axes
        IAxis hAxis;
        IAxis vAxis;
        if (chart.getOrientation() == SWT.HORIZONTAL) {
            hAxis = chart.getAxisSet().getXAxis(xAxisId);
            vAxis = chart.getAxisSet().getYAxis(yAxisId);
        } else if (chart.getOrientation() == SWT.VERTICAL) {
            hAxis = chart.getAxisSet().getYAxis(yAxisId);
            vAxis = chart.getAxisSet().getXAxis(xAxisId);
        } else {
            throw new IllegalStateException("unknown chart orientation"); //$NON-NLS-1$
        }

        // get the pixel coordinates
        return new Point(getPixelCoordinate(hAxis, index), getPixelCoordinate(
                vAxis, index));
    }

    /**
     * Gets the pixel coordinates with given axis and series index.
     *
     * @param axis
     *            the axis
     * @param index
     *            the series index
     * @return the pixel coordinates
     */
    private int getPixelCoordinate(IAxis axis, int index) {

        // get the data coordinate
        double dataCoordinate;
        if (axis.getDirection() == Direction.X) {
            if (axis.isCategoryEnabled()) {
                dataCoordinate = index;
            } else {
                if (index < 0 || xSeries.length <= index) {
                    throw new IllegalArgumentException(
                            "Series index is out of range."); //$NON-NLS-1$
                }
                dataCoordinate = xSeries[index];
            }
        } else if (axis.getDirection() == Direction.Y) {
            if (isValidStackSeries()) {
                if (index < 0 || stackSeries.length <= index) {
                    throw new IllegalArgumentException(
                            "Series index is out of range."); //$NON-NLS-1$
                }
                dataCoordinate = stackSeries[index];
            } else {
                if (index < 0 || ySeries.length <= index) {
                    throw new IllegalArgumentException(
                            "Series index is out of range."); //$NON-NLS-1$
                }
                dataCoordinate = ySeries[index];
            }
        } else {
            throw new IllegalStateException("unknown axis direction"); //$NON-NLS-1$
        }

        // get the pixel coordinate
        return axis.getPixelCoordinate(dataCoordinate);
    }

    /**
     * Gets the range with given margin.
     *
     * @param lowerPlotMargin
     *            the lower margin in pixels
     * @param upperPlotMargin
     *            the upper margin in pixels
     * @param length
     *            the axis length in pixels
     * @param axis
     *            the axis
     * @param range
     *            the range
     * @return the range with margin
     */
    protected Range getRangeWithMargin(int lowerPlotMargin,
            int upperPlotMargin, int length, Axis axis, Range range) {
        if (length == 0) {
            return range;
        }

        int lowerPixelCoordinate = axis.getPixelCoordinate(range.lower,
                range.lower, range.upper)
                + lowerPlotMargin
                * (axis.isHorizontalAxis() ? -1 : 1);
        int upperPixelCoordinate = axis.getPixelCoordinate(range.upper,
                range.lower, range.upper)
                + upperPlotMargin
                * (axis.isHorizontalAxis() ? 1 : -1);

        double lower = axis.getDataCoordinate(lowerPixelCoordinate,
                range.lower, range.upper);
        double upper = axis.getDataCoordinate(upperPixelCoordinate,
                range.lower, range.upper);

        return new Range(lower, upper);
    }

    /*
     * @see ISeries#setVisibleInLegend(boolean)
     */
    public void setVisibleInLegend(boolean visible) {
        visibleInLegend = visible;
    }

    /*
     * @see ISeries#isVisibleInLegend()
     */
    public boolean isVisibleInLegend() {
        return visibleInLegend;
    }

    /*
     * @see ISeries#setDescription(String)
     */
    public void setDescription(String description) {
        this.description = description;
    }

    /*
     * @see ISeries#getDescription()
     */
    public String getDescription() {
        return description;
    }

    /**
     * Disposes SWT resources.
     */
    protected void dispose() {
        for (IDisposeListener listener : listeners) {
            listener.disposed(new Event());
        }
    }

    /*
     * @see IAxis#addDisposeListener(IDisposeListener)
     */
    public void addDisposeListener(IDisposeListener listener) {
        listeners.add(listener);
    }

    /**
     * Draws series.
     *
     * @param gc
     *            the graphics context
     * @param width
     *            the width to draw series
     * @param height
     *            the height to draw series
     */
    public void draw(GC gc, int width, int height) {

        if (!visible || width < 0 || height < 0 || xSeries.length == 0
                || ySeries.length == 0) {
            return;
        }

        Axis xAxis = (Axis) chart.getAxisSet().getXAxis(getXAxisId());
        Axis yAxis = (Axis) chart.getAxisSet().getYAxis(getYAxisId());
        if (xAxis == null || yAxis == null) {
            return;
        }

        draw(gc, width, height, xAxis, yAxis);
    }

    /**
     * Draws series.
     *
     * @param gc
     *            the graphics context
     * @param width
     *            the width to draw series
     * @param height
     *            the height to draw series
     * @param xAxis
     *            the x axis
     * @param yAxis
     *            the y axis
     */
    abstract protected void draw(GC gc, int width, int height, Axis xAxis,
            Axis yAxis);
}
