/*******************************************************************************
 * 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 - Jeff Briggs, Henry Hughes, Ryan Morse
 *******************************************************************************/

package org.eclipse.linuxtools.systemtap.graphing.ui.wizards.graph;

import java.text.MessageFormat;

import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.linuxtools.internal.systemtap.graphing.ui.Localization;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;

/**
 * @since 2.1
 */
public class SelectGraphAndSeriesWizardPage extends WizardPage implements
        Listener {
    private Button[] btnGraphs;
    private SelectGraphAndSeriesWizard wizard;

    private Text txtTitle;
    private Combo cboXItem;
    private Combo[] cboYItems;
    private Label[] lblYItems;
    private GraphModel model;
    private boolean[] deleted;

    public SelectGraphAndSeriesWizardPage() {
        super("selectGraphAndSeries"); //$NON-NLS-1$
        setTitle(Localization.getString("SelectGraphAndSeriesWizardPage.SelectGraphAndSeries")); //$NON-NLS-1$
    }

    @Override
    public void createControl(Composite parent) {
        wizard = (SelectGraphAndSeriesWizard) getWizard();
        model = wizard.model;
        boolean edit = wizard.isEditing();

        // Set the layout data
        Composite comp = new Composite(parent, SWT.NONE);
        comp.setLayout(new GridLayout());

        Group cmpGraphOptsGraph = new Group(comp, SWT.SHADOW_ETCHED_IN);
        cmpGraphOptsGraph.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false));
        RowLayout rowLayout = new RowLayout();
        rowLayout.type = SWT.HORIZONTAL;
        rowLayout.spacing = 10;
        cmpGraphOptsGraph.setLayout(rowLayout);
        cmpGraphOptsGraph.setText(Localization.getString("SelectGraphAndSeriesWizardPage.Graph")); //$NON-NLS-1$

        String[] graphIDs = GraphFactory.getAvailableGraphs(wizard.model.getDataSet());
        int btnGraphSelected = -1;
        btnGraphs = new Button[graphIDs.length];
        for (int i = 0; i < btnGraphs.length; i++) {
            btnGraphs[i] = new Button(cmpGraphOptsGraph, SWT.RADIO);
            btnGraphs[i].setImage(GraphFactory.getGraphImage(graphIDs[i]));
            btnGraphs[i].addListener(SWT.Selection, this);
            btnGraphs[i].setData(graphIDs[i]);
            btnGraphs[i].setToolTipText(GraphFactory.getGraphName(btnGraphs[i].getData().toString()) + "\n\n" + //$NON-NLS-1$
                    GraphFactory.getGraphDescription(btnGraphs[i].getData().toString()));
            if (btnGraphSelected == -1 && wizard.isEditing() && graphIDs[i].equals(wizard.model.getGraphID())) {
                btnGraphs[i].setSelection(true);
                btnGraphSelected = i;
            }
        }

        ScrolledComposite scrolledComposite = new ScrolledComposite(comp, SWT.V_SCROLL | SWT.BORDER);
        scrolledComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
        scrolledComposite.setExpandHorizontal(true);

        Composite cmpGraphOptsSeries = new Composite(scrolledComposite, SWT.NONE);
        cmpGraphOptsSeries.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
        cmpGraphOptsSeries.setLayout(new GridLayout(2, false));
        scrolledComposite.setContent(cmpGraphOptsSeries);

        Label lblTitle = new Label(cmpGraphOptsSeries, SWT.NONE);
        lblTitle.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
        lblTitle.setText(Localization.getString("SelectGraphAndSeriesWizardPage.Title")); //$NON-NLS-1$
        txtTitle = new Text(cmpGraphOptsSeries, SWT.BORDER);
        txtTitle.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
        if (edit) {
            txtTitle.setText(model.getGraphData().title);
        }
        txtTitle.addModifyListener(e -> checkErrors(false));

        // Add the data series widgets
        String[] labels = model.getSeries();

        cboYItems = new Combo[!edit ? labels.length : Math.max(labels.length, model.getYSeries().length)];
        lblYItems = new Label[cboYItems.length];
        deleted = new boolean[cboYItems.length + 1];

        Label lblXItem = new Label(cmpGraphOptsSeries, SWT.NONE);
        lblXItem.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
        lblXItem.setText(Localization.getString("SelectGraphAndSeriesWizardPage.XSeries")); //$NON-NLS-1$
        cboXItem = new Combo(cmpGraphOptsSeries, SWT.DROP_DOWN | SWT.READ_ONLY);
        cboXItem.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
        cboXItem.addSelectionListener(new ComboSelectionListener());
        cboXItem.add(Localization.getString("SelectGraphAndSeriesWizardPage.RowID")); //$NON-NLS-1$

        for (int i = 0; i < cboYItems.length; i++) {
            lblYItems[i] = new Label(cmpGraphOptsSeries, SWT.NONE);
            lblYItems[i].setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
            lblYItems[i].setText(MessageFormat.format(Localization.getString("SelectGraphAndSeriesWizardPage.YSeries"), i)); //$NON-NLS-1$
            cboYItems[i] = new Combo(cmpGraphOptsSeries, SWT.DROP_DOWN | SWT.READ_ONLY);
            cboYItems[i].setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
            cboYItems[i].addSelectionListener(new ComboSelectionListener());

            if (i > 0) {
                cboYItems[i].add(Localization.getString("SelectGraphAndSeriesWizardPage.NA")); //$NON-NLS-1$
                cboYItems[i].setVisible(false);
                lblYItems[i].setVisible(false);
            }
        }

        for (int j, i = 0; i < labels.length; i++) {
            cboXItem.add(labels[i]);
            for (j = 0; j < lblYItems.length; j++) {
                cboYItems[j].add(labels[i]);
            }
        }

        int selected;
        if (!edit) {
            cboXItem.select(0);
            cboYItems[0].select(0);
        } else {
            selected = model.getXSeries();
            if (selected < labels.length) {
                cboXItem.select(selected + 1);
            } else {
                cboXItem.add(Localization.getString("SelectGraphAndSeriesWizardPage.Deleted"), 0); //$NON-NLS-1$
                cboXItem.select(0);
                deleted[0] = true;
            }
            selected = model.getYSeries()[0];
            if (selected < labels.length) {
                cboYItems[0].select(selected);
            } else {
                cboYItems[0].add(Localization.getString("SelectGraphAndSeriesWizardPage.Deleted"), 0); //$NON-NLS-1$
                cboYItems[0].select(0);
                deleted[1] = true;
            }
        }
        boolean cvisible = true;
        for (int i = 1; i < cboYItems.length; i++) {
            if (!edit || model.getYSeries().length <= i) {
                cboYItems[i].select(selected = 0);
            } else {
                selected = model.getYSeries()[i];
                if (selected < labels.length) {
                    cboYItems[i].select(selected + 1);
                } else {
                    cboYItems[i].add(Localization
                            .getString("SelectGraphAndSeriesWizardPage.Deleted"), 0); //$NON-NLS-1$
                    cboYItems[i].select(0);
                    deleted[i + 1] = true;
                }
            }
            cboYItems[i].setVisible(cvisible);
            lblYItems[i].setVisible(cvisible);
            cvisible = (selected > 0);
        }

        // Select one of the graph types by default, rather than blank choice
        if (!edit) {
            btnGraphs[0].setSelection(true);
            saveDataToModelGraph(graphIDs[0]);
        } else if (btnGraphSelected == -1) {
            saveDataToModelGraph(null);
        }

        cmpGraphOptsSeries.pack();
        setControl(comp);
        checkErrors(true);
    }

    @Override
    public void handleEvent(Event event) {
        if (event.widget instanceof Button) {
            saveDataToModelGraph(((Button) event.widget).getData().toString());
            checkErrors(false);
            wizard.getContainer().updateButtons();
        }
    }

    @Override
    public boolean canFlipToNextPage() {
        return false;
    }

    @Override
    public boolean isPageComplete() {
        return saveDataToModelSeries();
    }

    /**
     * Saves the choice of graph type to the model.
     * @param selected The ID of the selected graph.
     */
    private void saveDataToModelGraph(String selected) {
        model.setGraph(selected);
    }

    /**
     * Saves all information pertaining to series data & naming to the model.
     * @return <code>true</code> if there are no conflicts in series data selection,
     * <code>false</code> otherwise. In the case of the latter, no data is saved.
     */
    private boolean saveDataToModelSeries() {
        if (getErrorMessage() == null) {
            model.setTitle(txtTitle.getText());
            model.setXSeries(cboXItem.getSelectionIndex() - 1);

            int i, count;
            for (i = 1, count = 1; i < cboYItems.length
                    && 0 != cboYItems[i].getSelectionIndex(); i++) {
                count++;
            }

            int[] ySeries = new int[count];
            ySeries[0] = cboYItems[0].getSelectionIndex();
            for (i = 1; i < count; i++) {
                ySeries[i] = cboYItems[i].getSelectionIndex() - 1;
            }
            model.setYSeries(ySeries);
            return true;
        }
        return false;
    }

    private void markAsDuplicate(Combo item, Boolean bad) {
        item.setForeground(item.getDisplay().getSystemColor(
                bad ? SWT.COLOR_RED : SWT.COLOR_BLACK));
    }

    private boolean isSeriesUnique() {
        Combo item = cboXItem;
        int i = 0;
        do {
            if (item.isVisible() && item.getForeground().equals(
                            item.getDisplay().getSystemColor(SWT.COLOR_RED))) {
                return false;
            }
            if (i == cboYItems.length) {
                return true;
            }
            item = cboYItems[i++];
        } while (true);
    }

    /**
     * Checks for conflicts in data selection, and marks them. (An example of a conflict
     * is two Y-series fields set to the same output value.)
     * @return <code>true</code> if there is no conflict, <code>false</code> otherwise.
     */
    private boolean findAndMarkDuplicates() {
        boolean foundDuplicate = false;

        // Undo duplicate marking, as it is to be updated.
        markAsDuplicate(cboXItem, false);
        for (int i = 0; i < cboYItems.length; i++) {
            markAsDuplicate(cboYItems[i], false);
        }

        for (int j, i = 0; i < cboYItems.length; i++) {
            if (cboYItems[i].isVisible() && !deleted[i + 1]) {

                // Find duplicates by comparing selection indices. Every combo has an
                // extra selection before column names (Row Num or NA), except Y-series 0.
                int offset = (i == 0 ? 1 : 0);

                for (j = i + 1; j < cboYItems.length; j++) {
                    try {
                        if (!deleted[j + 1]
                                && cboYItems[j].isVisible()
                                && cboYItems[i].getSelectionIndex() + offset == cboYItems[j]
                                        .getSelectionIndex()) {
                            markAsDuplicate(cboYItems[i], true);
                            markAsDuplicate(cboYItems[j], true);
                            foundDuplicate = true;
                        }
                    } catch (Exception e) {
                        // If a cboYItem has no item selected, don't mark any duplicates. Ignore.
                    }
                }

                if (deleted[0]) {
                    continue;
                }
                try {
                    int selection = cboYItems[i].getSelectionIndex() + offset;
                    if (selection != 0 && selection == cboXItem.getSelectionIndex()) {
                        markAsDuplicate(cboYItems[i], true);
                        markAsDuplicate(cboXItem, true);
                        foundDuplicate = true;
                    }
                } catch (Exception e) {
                    // Ignore for same reason as above.
                }
            }
        }
        return !foundDuplicate;
    }

    /**
     * Checks for deleted/unselected series entries.
     * @return <code>true if some value is not selected, <code>false</code> otherwise.
     */
    private boolean isSeriesDeleted() {
        for (int i = 0; i < deleted.length; i++) {
            if (deleted[i]) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void dispose() {
        super.dispose();
        if (null != btnGraphs) {
            for (int i = 0; i < btnGraphs.length; i++) {
                btnGraphs[i].dispose();
            }
        }
        btnGraphs = null;

        if (null != txtTitle) {
            txtTitle.dispose();
        }
        txtTitle = null;

        if (null != cboXItem) {
            cboXItem.dispose();
        }
        cboXItem = null;
        if (null != cboYItems) {
            for (int i = 0; i < cboYItems.length; i++) {
                if (null != cboYItems[i]) {
                    cboYItems[i].dispose();
                }
                cboYItems[i] = null;
                if (null != lblYItems[i]) {
                    lblYItems[i].dispose();
                }
                lblYItems[i] = null;
            }
        }
        cboYItems = null;
        lblYItems = null;
        model = null;
    }

    /**
     * This class is responsible for updating the menu elements whenever
     * the user interacts with them. Namely, it checks for naming errors
     * and invalid series selections, and handles display of Y-series combo boxes.
     */
    private class ComboSelectionListener extends SelectionAdapter {
        @Override
        public void widgetSelected(SelectionEvent e) {
            Combo source = (Combo) e.getSource();
            if (cboXItem.equals(source)) {
                if (deleted[0] && cboXItem.getSelectionIndex() != 0) {
                    cboXItem.remove(0);
                    deleted[0] = false;
                }
            } else {
                for (int i = 0; i < cboYItems.length; i++) {
                    if (deleted[i + 1] && cboYItems[i].equals(source)
                            && cboYItems[i].getSelectionIndex() != 0) {
                        cboYItems[i].remove(0);
                        deleted[i + 1] = false;
                        break;
                    }
                }
                boolean setVisible = true;
                if (GraphFactory.isMultiGraph(model.getGraphID())) {
                    for (int i = 1; i < cboYItems.length; i++) {
                        cboYItems[i].setVisible(setVisible);
                        lblYItems[i].setVisible(setVisible);
                        if (!setVisible && deleted[i + 1]) {
                            cboYItems[i].remove(0);
                            deleted[i + 1] = false;
                            cboYItems[i].select(0);
                        }
                        if (deleted[i + 1]
                                || (cboYItems[i].getSelectionIndex() > 0
                                        && cboYItems[i].isVisible())) {
                            setVisible = true;
                        } else {
                            setVisible = false;
                        }
                    }
                }
            }
            checkErrors(true);
        }
    }

    private void checkErrors(boolean markDuplicates) {
        boolean isUnique = markDuplicates ? findAndMarkDuplicates() : isSeriesUnique();
        if (model.getGraphID() == null) {
            setErrorMessage(Localization
                    .getString("SelectGraphAndSeriesWizardPage.NoGraphType")); //$NON-NLS-1$
        } else if (!isUnique) {
            setErrorMessage(Localization
                    .getString("SelectGraphAndSeriesWizardPage.SeriesNotUnique")); //$NON-NLS-1$
        } else if (isSeriesDeleted()) {
            setErrorMessage(Localization
                    .getString("SelectGraphAndSeriesWizardPage.SeriesDeleted")); //$NON-NLS-1$
        } else if (txtTitle.getText().length() == 0) {
            setErrorMessage(Localization
                    .getString("SelectGraphAndSeriesWizardPage.TitleNotSet")); //$NON-NLS-1$
        } else {
            setErrorMessage(null);
        }
        getWizard().getContainer().updateButtons();
    }

}