/****************************************************************
 * Copyright (c) 2006, 2018 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *     Red Hat Inc. - ongoing maintenance
 ****************************************************************
 */
package org.eclipse.linuxtools.systemtap.graphing.ui.charts;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.linuxtools.internal.systemtap.graphing.ui.GraphingUIPlugin;
import org.eclipse.linuxtools.internal.systemtap.graphing.ui.preferences.GraphingPreferenceConstants;
import org.eclipse.linuxtools.systemtap.graphing.core.adapters.IAdapter;
import org.eclipse.linuxtools.systemtap.graphing.ui.charts.listeners.AbstractChartMouseMoveListener;
import org.eclipse.linuxtools.systemtap.structures.listeners.IUpdateListener;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.swtchart.Chart;
import org.swtchart.ITitle;

/**
 * A {@link Composite} that provides the common members and the framework to build one chart.
 * @author Qi Liang
 */
public abstract class AbstractChartBuilder extends Composite implements IUpdateListener {

    /**
     * Font name for all titles, labels, and values.
     */
    protected final static String FONT_NAME = "MS Sans Serif"; //$NON-NLS-1$
    protected int maxItems;
    protected double scale = 1.0;
    /**
     * @since 3.0
     */
    protected double scaleY = 1.0;
    /**
     * @since 3.0
     */
    protected double scroll = 1.0;
    /**
     * @since 3.0
     */
    protected double scrollY = 1.0;

    /**
     * Provides data for chart.
     */
    protected IAdapter adapter = null;

    protected static final Color WHITE = Display.getDefault().getSystemColor(SWT.COLOR_WHITE);
    protected static final Color BLACK = Display.getDefault().getSystemColor(SWT.COLOR_BLACK);
    protected static final Color RED = Display.getDefault().getSystemColor(SWT.COLOR_RED);

    protected static final Color[] COLORS = {
        RED,
        Display.getDefault().getSystemColor(SWT.COLOR_GREEN),
        Display.getDefault().getSystemColor(SWT.COLOR_BLUE),
        Display.getDefault().getSystemColor(SWT.COLOR_YELLOW),
        Display.getDefault().getSystemColor(SWT.COLOR_MAGENTA),
        Display.getDefault().getSystemColor(SWT.COLOR_CYAN),
        BLACK,
        new Color(Display.getDefault(), 64, 128, 128),
        new Color(Display.getDefault(), 255, 165, 0),
        new Color(Display.getDefault(), 128, 128, 128),
    };
    /**
     * Chart instance.
     */
    protected Chart chart = null;

    /**
     * Chart title.
     */
    protected String title = null;

    private List<IUpdateListener> listeners = new ArrayList<>();

    /**
     * The mouse listener that watches for MouseMove events over a specified region.
     * It is null by default.
     * @since 3.0
     */
    protected AbstractChartMouseMoveListener chartMouseMoveListener = null;

    /**
     * If a mouse listener is registered, returns a message with details on the mouse's
     * current position on the chart. This method is primarily used for testing purposes.
     * @return A String message if a mouse listener is registered; null otherwise.
     * @since 3.2
     */
    public String getMouseMessage() {
        return chartMouseMoveListener != null ? chartMouseMoveListener.getMouseMessage() : null;
    }

    /**
     * A reference to the SystemTap Graphing preference store.
     * @since 3.0
     */
    protected IPreferenceStore store;

    /**
     * Updates the chart with properties read from user-set preferences. It is called automatically
     * whenever a change is made to SystemTap Graphing preferences.
     * @param event The update event containing details on the preference that was changed.
     * @since 3.0
     */
    protected void updateProperties(PropertyChangeEvent event) {
        if (event.getProperty().equals(GraphingPreferenceConstants.P_VIEWABLE_DATA_ITEMS)
                || event.getProperty().equals(GraphingPreferenceConstants.P_MAX_DATA_ITEMS)) {
            maxItems = Math.min(store.getInt(GraphingPreferenceConstants.P_VIEWABLE_DATA_ITEMS),
                    store.getInt(GraphingPreferenceConstants.P_MAX_DATA_ITEMS));
            updateDataSet();
        }
    }
    private IPropertyChangeListener propertyChangeListener;

    /**
     * Constructs a chart builder and associates it to one data set.
     * @param adapter An {@link IAdapter} for reading from the chart's data set.
     * @param parent The parent {@link Composite} that will contain this chart builder.
     * @param style The style of the chart to construct.
     * @param title The title of the chart to construct.
     */
    public AbstractChartBuilder(IAdapter adapter, Composite parent, int style, String title) {
        super(parent, style);
        this.adapter = adapter;
        this.title = title;
        this.setLayout(new FillLayout());

        store = GraphingUIPlugin.getDefault().getPreferenceStore();
        maxItems = Math.min(store.getInt(GraphingPreferenceConstants.P_VIEWABLE_DATA_ITEMS),
                store.getInt(GraphingPreferenceConstants.P_MAX_DATA_ITEMS));

        propertyChangeListener = event -> updateProperties(event);
        store.addPropertyChangeListener(propertyChangeListener);
    }

    @Override
    public void dispose() {
        store.removePropertyChangeListener(propertyChangeListener);
        propertyChangeListener = null;
        super.dispose();
    }

    /**
     * Builds one chart.
     */
    public void build() {
        createChart();
        buildPlot();
        buildLegend();
        buildTitle();
        buildXAxis();
        buildYAxis();
        buildXSeries();
        buildYSeries();
        updateDataSet();
    }

    /**
     * Creates chart instance.
     */
    protected void createChart() {
        this.chart = new Chart(this, getStyle());
    }

    /**
     * Builds plot.
     */
    private void buildPlot() {
        this.chart.setBackground(WHITE);
        this.chart.setBackgroundInPlotArea(WHITE);
    }

    /**
     * Builds X axis.
     */
    protected abstract void buildXAxis();

    /**
     * Builds Y axis.
     */
    protected abstract void buildYAxis();

    /**
     * Builds X series.
     */
    protected abstract void buildXSeries();

    /**
     * Builds Y series.
     */
    protected abstract void buildYSeries();

    public abstract void updateDataSet();

    /**
     * Builds legend.
     *
     */
    private void buildLegend() {
        chart.getLegend().setPosition(SWT.RIGHT);
    }

    /**
     * Builds the chart title.
     */
    private void buildTitle() {
        ITitle ctitle = chart.getTitle();
        ctitle.setForeground(BLACK);
        ctitle.setText(this.title);
    }

    /**
     * Returns the chart instance.
     *
     * @return the chart instance
     */
    public Chart getChart() {
        return chart;
    }

    public void setScale(double scale) {
        if (scale < 0) {
            this.scale = 0;
        } else if (scale > 1) {
            this.scale = 1;
        } else {
            this.scale = scale;
        }
        handleUpdateEvent();
    }

    /**
     * @since 3.0
     * @return The current horizontal scale of the chart.
     */
    public double getScale() {
        return this.scale;
    }

    /**
     * @param scale The desired vertical scale of the chart.
     * @since 3.0
     */
    public void setScaleY(double scale) {
        if (scale < 0) {
            this.scaleY = 0;
        } else if (scale > 1) {
            this.scaleY = 1;
        } else {
            this.scaleY = scale;
        }
        handleUpdateEvent();
    }

    /**
     * @since 3.0
     * @return The current vertical scale of the chart.
     */
    public double getScaleY() {
        return this.scaleY;
    }

    /**
     * @param scroll The desired horizontal scroll of the chart.
     * @since 3.0
     */
    public void setScroll(double scroll) {
        if (scroll < 0) {
            this.scroll = 0;
        } else if (scroll > 1) {
            this.scroll = 1;
        } else {
            this.scroll = scroll;
        }
        handleUpdateEvent();
    }

    /**
     * @since 3.0
     * @return The current horizontal scroll of the chart.
     */
    public double getScroll() {
        return this.scroll;
    }

    /**
     * @param scroll The desired vertical scroll of the chart.
     * @since 3.0
     */
    public void setScrollY(double scroll) {
        if (scroll < 0) {
            this.scrollY = 0;
        } else if (scroll > 1) {
            this.scrollY = 1;
        } else {
            this.scrollY = scroll;
        }
        handleUpdateEvent();
    }

    /**
     * @since 3.0
     * @return The current vertical scroll of the chart.
     */
    public double getScrollY() {
        return this.scrollY;
    }

    /**
     * Converts a value into its {@link Double} equivalent.
     * @param o The object to convert to a {@link Double}.
     * @return The object in the form of a {@link Double}. May be <code>null</code>
     * if conversion is not possible, or if the object was null in the first place.
     * @since 3.0
     */
    protected Double getDoubleOrNullValue(Object o) {
        if (o == null) {
            return null;
        }
        if (o instanceof Integer) {
            return ((Integer)o).doubleValue();
        }
        if (o instanceof Double) {
            return (Double) o;
        }
        try {
            return Double.valueOf(o.toString());
        } catch (NumberFormatException e) {
            return null;
        }
    }

    @Override
    public void handleUpdateEvent() {
        if (chart != null && !chart.isDisposed()) {
            repaint();
        }
    }

    /**
     * @param l A {@link IUpdateListener} to register with this chart.
     * @since 3.0
     */
    public void addUpdateListener(IUpdateListener l) {
        listeners.add(l);
    }

    /**
     * @param l A previously-registered {@link IUpdateListener} to remove.
     * @return <code>true</code> if the listener was removed,
     * <code>false</code> otherwise (such as when the provided
     * listener was not already registered).
     * @since 3.0
     */
    public boolean removeUpdateListener(IUpdateListener l) {
        return listeners.remove(l);
    }

    private void repaint() {
        getDisplay().asyncExec(() -> {
		    if (!chart.isDisposed()) {
		        updateDataSet();
		        for (IUpdateListener l : listeners) {
		            l.handleUpdateEvent();
		        }
		    }
		});
    }

    /**
     * Given an array of label name strings, returns a new array in which all duplicate labels
     * have been given unique names.
     * @param labels An array of label names.
     * @return A new array containing unique label names.
     * @since 3.0
     */
    protected String[] getUniqueNames(String[] labels) {
        Set<String> labelsUnique = new LinkedHashSet<>();
        for (String label : labels) {
            int count = 1;
            while (!labelsUnique.add(makeCountedLabel(label, count))) {
                count++;
            }
        }
        return labelsUnique.toArray(new String[labels.length]);
    }

    private String makeCountedLabel(String original, int count) {
        return count <= 1 ? original : original.concat(String.format(" (%d)", count)); //$NON-NLS-1$
    }

}
