/*******************************************************************************
 * Copyright (c) 2013, 2018 Red Hat Inc. and others.
 * 
 * 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:
 *    Andrew Ferrazzutti <aferrazz@redhat.com> - initial API and implementation
 *******************************************************************************/
package org.eclipse.linuxtools.internal.systemtap.graphing.ui.charts;

import org.eclipse.swt.widgets.Composite;
import org.swtchart.Chart;
import org.swtchart.IAxis;
import org.swtchart.ISeries;
import org.swtchart.Range;

/**
 * @since 3.0
 */
public class BarChart extends Chart {

    private final static int MIN_LABEL_SIZE = Messages.BarChartBuilder_LabelTrimTag.length();
    private final int fontSize;

    private double[][] actualYSeries;
    private String[] fullLabels = null;
    private IAxis xAxis = null;

    private boolean updateSuspended = false;
    @Override
	public void suspendUpdate(boolean suspend) {
        if (updateSuspended == suspend) {
            return;
        }
        updateSuspended = suspend;

        // make sure that chart is updated
        if (!suspend) {
            updateLayout();
        }
    }

    public BarChart(Composite parent, int style) {
        super(parent, style);
        fontSize = getFont().getFontData()[0].getHeight();
        xAxis = getAxisSet().getXAxis(0);
        xAxis.enableCategory(true);
        xAxis.setCategorySeries(new String[]{""}); //$NON-NLS-1$
    }

    /**
     * Sets the BarChart's x-axis category labels such that labels won't get
     * cut off if there isn't enough room to display them fully. Use this
     * instead of accessing the chart's x-axis and setting its category
     * series directly.
     * @param series The category series.
     */
    public void setCategorySeries(String[] series) {
        xAxis.setCategorySeries(series);
        fullLabels = xAxis.getCategorySeries();
    }

    /**
     * Return the y-value of the specified series' bar. Use this instead of referring directly
     * to the chart's series values with {@link #getSeriesSet()} and {@link ISeries#getYSeries()}.
     * @param series The index of the bar series to get data from.
     * @param barIndex The index of the bar to get the y-value of.
     * @return The y-value of the specified bar.
     */
    public double getBarValue(int series, int barIndex) {
        return actualYSeries[series][barIndex];
    }

    /**
     * Returns a list of the full (non-trimmed) label names of each bar.
     * Use this instead of accessing the x-axis' category series, which
     * may contain trimmed label names.
     * @return An array containing the names of each bar in the chart.
     */
    public String[] getCategorySeries() {
        String[] copiedCategorySeries = null;

        if (fullLabels != null) {
            copiedCategorySeries = new String[fullLabels.length];
            System.arraycopy(fullLabels, 0, copiedCategorySeries, 0,
                    fullLabels.length);
        }

        return copiedCategorySeries;
    }

    @Override
    public void updateLayout() {
        if (updateSuspended) {
            return;
        }

        // If the x-axis and its labels are set, ensure that their contents fit the width of each label.
        if (fullLabels != null) {
            hideBars();
            String[] labels = xAxis.getCategorySeries();
            if (labels != null && labels.length > 0) {
                String[] trimmedLabels = null;
                trimmedLabels = fitLabels(fullLabels);

                // Only update labels if their trimmed contents are different than their current contents.
                for (int i = 0; i < fullLabels.length; i++) {
                    if (!trimmedLabels[i].equals(labels[i])) {
                        labels = trimmedLabels;
                        break;
                    }
                }
                if (labels == trimmedLabels) {
                    // setCategorySeries triggers an unnecessary call to updateLayout, so prevent it.
                    updateSuspended = true;
                    xAxis.setCategorySeries(labels);
                    updateSuspended = false;
                }
            }
        }
        super.updateLayout();
    }

    /**
     * Given an array of label names, return a new set of names that have been trimmed down
     * in order to fit in the chart axis without getting cut off.
     * @param labels An array of label names that may be trimmed.
     * @return A new array containing label names that have been trimmed to fit in the axis display.
     */
    private String[] fitLabels(String[] labels) {
        Range range = xAxis.getRange();
        int maxLabelSize = (int) Math.max(getClientArea().width / (Math.max(range.upper - range.lower, 1) * fontSize), MIN_LABEL_SIZE);

        String[] trimlabels = new String[labels.length];
        for (int i = 0; i < labels.length; i++) {
            if (labels[i].length() > maxLabelSize) {
                trimlabels[i] = labels[i].substring(0, maxLabelSize - MIN_LABEL_SIZE)
                        .concat(Messages.BarChartBuilder_LabelTrimTag);
            } else {
                trimlabels[i] = labels[i];
            }
        }
        return trimlabels;
    }

    /*
     * Workaround for EBZ #427019: out-of-bounds bars appear, so change their values to
     * keep them properly hidden. Save actual bar values elsewhere so they can be retrieved.
     */
    private void hideBars() {
        Range rangeX = xAxis.getRange();
        double bottomY = getAxisSet().getYAxis(0).getRange().lower;
        double nonNegBottomY = Math.max(0, bottomY);
        ISeries[] allSeries = getSeriesSet().getSeries();
        actualYSeries = new double[allSeries.length][];

        for (int i = 0, n = allSeries.length; i < n; i++) {
            double[] yseries = allSeries[i].getYSeries();
            if (yseries == null) {
                return;
            }

            // Store the true values of the bars into a different array, so they
            // can be displayed later if need be.
            actualYSeries[i] = new double[yseries.length];
            System.arraycopy(yseries, 0, actualYSeries[i], 0, yseries.length);

            for (int x = (int) rangeX.lower; x <= (int) rangeX.upper; x++) {
                if (yseries[x] < bottomY) {
                    yseries[x] = bottomY;
                }
            }
            if (yseries.length - 1 > rangeX.upper) {
                yseries[(int) rangeX.upper + 1] = nonNegBottomY;
            }
            allSeries[i].setYSeries(yseries);
        }
    }

}
