/*--------------------------------------------------------------*
  Copyright (C) 2006-2015 OpenSim Ltd.

  This file is distributed WITHOUT ANY WARRANTY. See the file
  'License' for details on this and other legal matters.
*--------------------------------------------------------------*/

package org.omnetpp.scave.charting;

import java.util.List;

import org.eclipse.draw2d.Graphics;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.RGB;
import org.omnetpp.common.canvas.ICoordsMapping;
import org.omnetpp.common.canvas.LargeGraphics;
import org.omnetpp.common.canvas.LargeRect;
import org.omnetpp.common.canvas.RectangularArea;
import org.omnetpp.scave.charting.dataset.IGroupsSeriesDataset;
import org.omnetpp.scave.charting.properties.PlotProperty.BarPlacement;

/**
 * The content area of a bar plot.
 */
class Bars {
    private Rectangle rect = new Rectangle(0,0,1,1);

    double barBaseline;
    BarPlacement barPlacement;
    RGB barBaselineColor;

    // coordinates of the bars, in group/series order
    // groups are sorted according to the x coordinate
    // y coordinates might be logarithmic
    private RectangularArea[][] bars;

    private BarPlot parent;

    public Bars(BarPlot parent) {
        this.parent = parent;
    }

    public Rectangle getRectangle() {
        return rect;
    }

    public Rectangle layout(Graphics graphics, Rectangle rect) {
        this.rect = rect.getCopy();
        return rect;
    }

    public void drawSingle(Graphics graphics, ICoordsMapping coordsMapping, int series) {
        graphics.pushState();

        Rectangle clip = graphics.getClip(new Rectangle());
        int numSeries = bars[0].length;
        int[] indices = getGroupSeriessInRectangle(clip);
        for (int i = indices[0]; i <= indices[1]; ++i) {
            int group = i / numSeries;
            if (series == i % numSeries)
                drawBar(graphics, group, series, coordsMapping);
        }

        graphics.popState();
    }

    public void draw(Graphics graphics, ICoordsMapping coordsMapping) {
        if (bars != null) {
            // The series have to be drawn in reverse order so they appear
            // correctly in "Overlap" mode.
            List<Integer> enabledIndices = parent.legend.getEnabledItemIndices();
            for (int i = enabledIndices.size()-1; i >= 0; --i)
                drawSingle(graphics, coordsMapping, enabledIndices.get(i));
        }
    }

    public void drawBaseline(Graphics graphics, ICoordsMapping coordsMapping) {
        double baseline = getTransformedBaseline();
        if (!Double.isInfinite(baseline)) {
            graphics.pushState();

            long y = coordsMapping.toCanvasY(baseline);
            graphics.setForegroundColor(new Color(barBaselineColor));
            LargeGraphics.drawLine(graphics, rect.x + 1, y, rect.x + rect.width - 1, y);

            graphics.popState();
        }
    }

    protected void drawBar(Graphics graphics, int group, int series, ICoordsMapping coordsMapping) {
        LargeRect rect = getBarRectangle(group, series, coordsMapping);
        rect.width = Math.max(rect.width, 1);
        rect.height = Math.max(rect.height, 1);
        graphics.setBackgroundColor(getBarColor(series));
        graphics.setForegroundColor(getBarOutlineColor(series));
        LargeGraphics.fillRectangle(graphics, rect.x, rect.y, rect.width, rect.height);
        if (rect.width >= 4 && rect.height >= 3) {
            LargeGraphics.drawRectangle(graphics, rect.x, rect.y, rect.width, rect.height);
        }
    }

    protected int[] getGroupSeriessInRectangle(org.eclipse.draw2d.geometry.Rectangle rect) {
        int[] result = new int[2];
        result[0] = getGroupSeries(rect.x, true);
        result[1] = getGroupSeries(rect.x + rect.width, false);
        return result;
    }

    private int getGroupSeries(double x, boolean before) {
        int numGroups = parent.getDataset().getGroupCount();
        int numSeries = parent.getDataset().getSeriesCount();
        return before ? 0 : (numGroups*numSeries-1);
    }

    public int findGroupSeries(double x, double y) {
        IGroupsSeriesDataset dataset = parent.getDataset();
        if (dataset == null || bars == null)
            return -1;
        int numGroups = parent.getDataset().getGroupCount();
        int numSeries = parent.getDataset().getSeriesCount();

        for (int group = 0; group < numGroups; ++group)
            // search seriess in Z-order
            for (int series : parent.legend.getEnabledItemIndices())
                if (bars[group][series].contains(x, y))
                    return group * numSeries + series;

        return -1;
    }

    protected Color getBarColor(int series) {
        RGB color = parent.getEffectiveBarColor(parent.getKeyFor(series));
        return new Color(color);
    }

    protected Color getBarOutlineColor(int series) {
        RGB color = parent.getEffectiveBarOutlineColor(parent.getKeyFor(series));
        return new Color(color);
    }

    protected LargeRect getBarRectangle(int group, int series, ICoordsMapping coordsMapping) {
        RectangularArea bar = bars[group][series];
        double top =  bar.maxY;
        double bottom = bar.minY;
        double left = bar.minX;
        double right = bar.maxX;

        if (Double.isInfinite(top))
            top = top < 0.0 ? parent.chartArea.minY : parent.chartArea.maxY;
        if (Double.isInfinite(bottom))
            bottom = bottom < 0.0 ? parent.chartArea.minY : parent.chartArea.maxY;

        long x = coordsMapping.toCanvasX(left);
        long y = coordsMapping.toCanvasY(top);
        long width = coordsMapping.toCanvasDistX(right - left);
        long height = coordsMapping.toCanvasDistY(top - bottom);
        return new LargeRect(x, y, width, height);
    }

    protected RectangularArea calculatePlotArea() {
        final double widthBar = 1.0;
        final double hgapMinor = 0.5;
        final double hgapMajor = 0.25;
        final double horizontalInset = 1.0;   // left/right inset relative to the bars' width
        final double verticalInset = 0.1;   // top inset relative to the height of the highest bar

        IGroupsSeriesDataset dataset = parent.getDataset();
        bars = null;
        if (dataset == null)
            return new RectangularArea(0, 0, 1, 1);

        int numSeries = dataset.getSeriesCount();
        int numGroups = dataset.getGroupCount();
        double baseline = getTransformedBaseline();

        if (Double.isInfinite(baseline)) {
            double newBaseline = baseline < 0.0 ? Double.POSITIVE_INFINITY : Double.NEGATIVE_INFINITY;
            for (int group = 0; group < numGroups; ++group)
                for (int series : parent.legend.getEnabledItemIndices()) {
                    double value = transformValue(dataset.getValue(series, group));
                    if (!Double.isNaN(value) && !Double.isInfinite(value)) {
                        if (baseline < 0.0)
                            newBaseline = Math.min(newBaseline, value);
                        else
                            newBaseline = Math.max(newBaseline, value);
                    }
                }
            baseline = newBaseline;
            if (Double.isInfinite(baseline))
                return new RectangularArea(0, 0, 1, 1);
        }

        RectangularArea plotArea = new RectangularArea(0, baseline, 0, baseline);

        double x = horizontalInset * widthBar;
        double y = 0;
        bars = new RectangularArea[numGroups][];
        for (int group = 0; group < numGroups; ++group) {
            y = 0;
            bars[group] = new RectangularArea[numSeries];
            for (int series = 0; series < numSeries; ++series) {
                RectangularArea bar = bars[group][series] = new RectangularArea();
                double value = parent.getDataset().getValue(series, group);

                // calculate x coordinates
                switch (barPlacement) {
                case Aligned:
                    bar.minX = x;
                    bar.maxX = x + widthBar;
                    x += widthBar;
                    if (series < numSeries - 1)
                        x += hgapMinor;
                    else if (group < numGroups - 1)
                        x += hgapMajor * (widthBar * numSeries + (hgapMinor * (numSeries - 1)));
                    break;
                case Overlap:
                    bar.minX = x + widthBar * series / 2.0;
                    bar.maxX = bar.minX + widthBar * numSeries / 2.0;
                    if (series == numSeries - 1 && group < numGroups - 1)
                        x += widthBar * numSeries + hgapMajor * widthBar * numSeries;
                    break;
                case InFront:
                case Stacked:
                    bar.minX = x;
                    bar.maxX = x + widthBar * numSeries;
                    if (series == numSeries - 1 && group < numGroups - 1)
                        x += widthBar * numSeries + hgapMajor * widthBar * numSeries;
                    break;
                }

                // calculate y coordinates
                switch (barPlacement) {
                case Aligned:
                case InFront:
                case Overlap:
                    y = transformValue(value);
                    if (y > baseline) {
                        bar.minY = baseline;
                        bar.maxY = y;
                    }
                    else if (y < baseline) {
                        bar.minY = y;
                        bar.maxY = baseline;
                    }
                    else
                        bar.minY = bar.maxY = baseline;
                    break;
                case Stacked:
                    value = Math.abs(value);
                    if (!parent.legend.isItemEnabled(series))
                        value = 0;
                    bar.minY = transformValue(y);
                    bar.maxY = transformValue(y + value);
                    y += value;
                    break;
                }

                // extend plot area with the new bar
                addRectangle(plotArea, bar, !parent.legend.isItemEnabled(series));
            }
        }

        if (plotArea.minY > plotArea.maxY)
            return new RectangularArea(0, 0, 1, 1);

        // set minimum height (empty plot, or each value == barBaseline)
        if (plotArea.minY == plotArea.maxY) {
            plotArea.minY -= 0.5;
            plotArea.maxY += 0.5;
        }
        // add vertical insets
        double height = plotArea.maxY - plotArea.minY;
        plotArea.maxX += horizontalInset * widthBar;
        if (plotArea.minY < baseline)
            plotArea.minY -= verticalInset * height;
        if (plotArea.maxY > baseline)
            plotArea.maxY += verticalInset * height;
        return plotArea;
    }

    private void addRectangle(RectangularArea area, RectangularArea rect, boolean onlyX) {
        if (!Double.isNaN(rect.minX) && !Double.isInfinite(rect.minX))
            area.minX = Math.min(area.minX, rect.minX);
        if (!onlyX && !Double.isNaN(rect.minY) && !Double.isInfinite(rect.minY))
            area.minY = Math.min(area.minY, rect.minY);
        if (!Double.isNaN(rect.maxX) && !Double.isInfinite(rect.maxX))
            area.maxX = Math.max(area.maxX, rect.maxX);
        if (!onlyX && !Double.isNaN(rect.maxY) && !Double.isInfinite(rect.maxY))
            area.maxY = Math.max(area.maxY, rect.maxY);
    }

    protected double transformValue(double y) {
        return parent.transformY(y);
    }

    protected double getTransformedBaseline() {
        double baseline = barPlacement == BarPlacement.Stacked ? 0.0 : barBaseline;
        baseline = transformValue(baseline);
        return Double.isNaN(baseline) ? Double.NEGATIVE_INFINITY : baseline;
    }
}

