/*******************************************************************************
 * Copyright (c) 2013, 2014 Ericsson
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Marc-Andre Laperle - Initial API and implementation
 *******************************************************************************/

package org.eclipse.tracecompass.internal.tmf.ui.project.wizards.tracepkg;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.tracecompass.internal.tmf.ui.Activator;
import org.eclipse.tracecompass.tmf.ui.dialog.TmfFileDialogFactory;

/**
 * An abstract wizard page containing common code useful for both import and
 * export trace package wizard pages
 *
 * @author Marc-Andre Laperle
 */
public abstract class AbstractTracePackageWizardPage extends WizardPage {

    private static final int COMBO_HISTORY_LENGTH = 5;
    private static final String STORE_FILE_PATHS_ID = ".STORE_FILEPATHS_ID"; //$NON-NLS-1$

    private final String fStoreFilePathId;
    private final IStructuredSelection fSelection;

    private CheckboxTreeViewer fElementViewer;
    private Button fSelectAllButton;
    private Button fDeselectAllButton;
    private Combo fFilePathCombo;
    private Button fBrowseButton;

    /**
     * Create the trace package wizard page
     *
     * @param pageName
     *            the name of the page
     * @param title
     *            the title for this wizard page, or null if none
     * @param titleImage
     *            the image descriptor for the title of this wizard page, or
     *            null if none
     * @param selection
     *            the current object selection
     */
    protected AbstractTracePackageWizardPage(String pageName, String title, ImageDescriptor titleImage, IStructuredSelection selection) {
        super(pageName, title, titleImage);
        fStoreFilePathId = getName() + STORE_FILE_PATHS_ID;
        fSelection = selection;
    }

    /**
     * Create the element viewer
     *
     * @param compositeParent
     *            the parent composite
     */
    protected void createElementViewer(Composite compositeParent) {
        fElementViewer = new CheckboxTreeViewer(compositeParent, SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER | SWT.CHECK);

        fElementViewer.addCheckStateListener(new ICheckStateListener() {
            @Override
            public void checkStateChanged(CheckStateChangedEvent event) {
                TracePackageElement element = (TracePackageElement) event.getElement();
                if (!element.isEnabled()) {
                    fElementViewer.setChecked(element, element.isChecked());
                } else {
                    setSubtreeChecked(fElementViewer, element, true, event.getChecked());
                }
                maintainCheckIntegrity(element);

                if (element.getParent() != null) {
                    // Uncheck everything in this trace if Trace files are unchecked
                    if (element instanceof TracePackageFilesElement) {
                        if (!element.isChecked()) {
                            setSubtreeChecked(fElementViewer, element.getParent(), false, false);
                        }
                    // Check Trace files if anything else is selected
                    } else if (element.isChecked()) {
                        TracePackageElement parent = element.getParent();
                        while (parent != null) {
                            for (TracePackageElement e : parent.getChildren()) {
                                if (e instanceof TracePackageFilesElement) {
                                    setSubtreeChecked(fElementViewer, e, false, true);
                                    break;
                                }
                            }
                            parent = parent.getParent();
                        }
                    }
                }

                maintainExperimentIntegrity(element);

                updateApproximateSelectedSize();
                updatePageCompletion();
            }

            private void maintainCheckIntegrity(final TracePackageElement element) {
                TracePackageElement parentElement = element.getParent();
                boolean allChecked = true;
                boolean oneChecked = false;
                if (parentElement != null) {
                    if (parentElement.getChildren() != null) {
                        for (TracePackageElement child : parentElement.getChildren()) {
                            if (fElementViewer.getGrayed(child)) {
                                oneChecked = true;
                                allChecked = false;
                                break;
                            }
                            boolean checked = fElementViewer.getChecked(child);
                            oneChecked |= checked;
                            allChecked &= checked;
                        }
                    }
                    if (oneChecked && !allChecked) {
                        fElementViewer.setGrayChecked(parentElement, true);
                    } else {
                        fElementViewer.setGrayed(parentElement, false);
                        fElementViewer.setChecked(parentElement, allChecked);
                    }
                    maintainCheckIntegrity(parentElement);
                }
            }

            private void maintainExperimentIntegrity(final TracePackageElement element) {
                TracePackageElement parent = element;
                while (parent.getParent() != null) {
                    parent = parent.getParent();
                }
                if (parent instanceof TracePackageExperimentElement && fElementViewer.getChecked(parent)) {
                    TracePackageExperimentElement experiment = (TracePackageExperimentElement) parent;
                    // Check all traces of the checked experiment
                    for (String expTrace : experiment.getExpTraces()) {
                        for (TracePackageElement root : (TracePackageElement[]) fElementViewer.getInput()) {
                            if (!(root instanceof TracePackageExperimentElement) && root.getText().equals(expTrace)) {
                                fElementViewer.setSubtreeChecked(root, true);
                                break;
                            }
                        }
                    }
                } else if (!(parent instanceof TracePackageExperimentElement) && !fElementViewer.getChecked(parent)) {
                    // Uncheck all experiments that contain unchecked trace
                    for (TracePackageElement root : (TracePackageElement[]) fElementViewer.getInput()) {
                        if (root instanceof TracePackageExperimentElement) {
                            TracePackageExperimentElement experiment = (TracePackageExperimentElement) root;
                            if (experiment.getExpTraces().contains(parent.getText())) {
                                fElementViewer.setSubtreeChecked(experiment, false);
                            }
                        }
                    }
                }
            }
        });

        GridData layoutData = new GridData(GridData.FILL_BOTH);
        fElementViewer.getTree().setLayoutData(layoutData);
        fElementViewer.setContentProvider(new TracePackageContentProvider());
        fElementViewer.setLabelProvider(new TracePackageLabelProvider());
    }

    /**
     * Create the input for the element viewer
     *
     * @return the input for the element viewer
     */
    protected abstract Object createElementViewerInput();

    /**
     * Create the file path group that allows the user to type or browse for a
     * file path
     *
     * @param parent
     *            the parent composite
     * @param label
     *            the label to describe the file path (i.e. import/export)
     * @param fileDialogStyle
     *            SWT.OPEN or SWT.SAVE
     */
    protected void createFilePathGroup(Composite parent, String label, final int fileDialogStyle) {

        Composite filePathSelectionGroup = new Composite(parent, SWT.NONE);
        GridLayout layout = new GridLayout();
        layout.numColumns = 3;
        filePathSelectionGroup.setLayout(layout);
        filePathSelectionGroup.setLayoutData(new GridData(
                GridData.HORIZONTAL_ALIGN_FILL | GridData.VERTICAL_ALIGN_FILL));

        Label destinationLabel = new Label(filePathSelectionGroup, SWT.NONE);
        destinationLabel.setText(label);

        fFilePathCombo = new Combo(filePathSelectionGroup, SWT.SINGLE
                | SWT.BORDER);
        GridData data = new GridData(GridData.HORIZONTAL_ALIGN_FILL
                | GridData.GRAB_HORIZONTAL);
        data.grabExcessHorizontalSpace = true;
        fFilePathCombo.setLayoutData(data);

        fBrowseButton = new Button(filePathSelectionGroup,
                SWT.PUSH);
        fBrowseButton.setText(org.eclipse.tracecompass.internal.tmf.ui.project.wizards.tracepkg.Messages.TracePackage_Browse);
        fBrowseButton.addListener(SWT.Selection, event -> handleFilePathBrowseButtonPressed(fileDialogStyle));
        setButtonLayoutData(fBrowseButton);
    }

    /**
     * Update the page with the file path the current file path selection
     */
    protected abstract void updateWithFilePathSelection();

    /**
     * Creates the buttons for selecting all or none of the elements.
     *
     * @param parent
     *            the parent control
     * @return the button group
     */
    protected Composite createButtonsGroup(Composite parent) {

        // top level group
        Composite buttonComposite = new Composite(parent, SWT.NONE);

        GridLayout layout = new GridLayout();
        layout.numColumns = 3;
        buttonComposite.setLayout(layout);
        buttonComposite.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_FILL
                | GridData.HORIZONTAL_ALIGN_FILL));

        fSelectAllButton = new Button(buttonComposite, SWT.PUSH);
        fSelectAllButton.setText(org.eclipse.tracecompass.internal.tmf.ui.project.wizards.tracepkg.Messages.TracePackage_SelectAll);

        SelectionListener listener = new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                setAllChecked(fElementViewer, true, true);
                updateApproximateSelectedSize();
                updatePageCompletion();
            }
        };
        fSelectAllButton.addSelectionListener(listener);

        fDeselectAllButton = new Button(buttonComposite, SWT.PUSH);
        fDeselectAllButton.setText(org.eclipse.tracecompass.internal.tmf.ui.project.wizards.tracepkg.Messages.TracePackage_DeselectAll);

        listener = new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                setAllChecked(fElementViewer, true, false);
                updateApproximateSelectedSize();
                updatePageCompletion();
            }
        };
        fDeselectAllButton.addSelectionListener(listener);

        return buttonComposite;
    }

    /**
     * Restore widget values to the values that they held last time this wizard
     * was used to completion.
     */
    protected void restoreWidgetValues() {
        IDialogSettings settings = getDialogSettings();
        if (settings != null) {
            String[] directoryNames = settings.getArray(fStoreFilePathId);
            if (directoryNames == null || directoryNames.length == 0) {
                return;
            }

            for (int i = 0; i < directoryNames.length; i++) {
                fFilePathCombo.add(directoryNames[i]);
            }
        }
    }

    /**
     * Save widget values to Dialog settings
     */
    protected void saveWidgetValues() {
        IDialogSettings settings = getDialogSettings();
        if (settings != null) {
            // update directory names history
            String[] directoryNames = settings.getArray(fStoreFilePathId);
            if (directoryNames == null) {
                directoryNames = new String[0];
            }

            directoryNames = addToHistory(directoryNames, getFilePathValue());
            settings.put(fStoreFilePathId, directoryNames);
        }
    }

    /**
     * Determine if the page is complete and update the page appropriately.
     */
    protected void updatePageCompletion() {
        boolean pageComplete = determinePageCompletion();
        setPageComplete(pageComplete);
        if (pageComplete) {
            setErrorMessage(null);
        }
    }

    /**
     * Determine if the page is completed or not
     *
     * @return true if the page is completed, false otherwise
     */
    protected boolean determinePageCompletion() {
        return fElementViewer.getCheckedElements().length > 0 && !getFilePathValue().isEmpty();
    }

    /**
     * Handle error status
     *
     * @param status
     *            the error status
     */
    protected void handleErrorStatus(IStatus status) {

        Throwable exception = status.getException();
        String message = status.getMessage().length() > 0 ? status.getMessage() : org.eclipse.tracecompass.internal.tmf.ui.project.wizards.tracepkg.Messages.TracePackage_ErrorOperation;

        if (!status.isMultiStatus()) {
            handleError(message, exception);
            return;
        }

        // Build a string with all the children status messages, exception
        // messages and stack traces
        StringBuilder sb = new StringBuilder();
        for (IStatus childStatus : status.getChildren()) {
            StringBuilder childSb = new StringBuilder();
            if (!childStatus.getMessage().isEmpty()) {
                childSb.append(childStatus.getMessage() + '\n');
            }

            Throwable childException = childStatus.getException();
            if (childException != null) {
                String reason = childException.getMessage();
                // Some system exceptions have no message
                if (reason == null) {
                    reason = childException.toString();
                }

                String stackMessage = ExceptionUtils.getStackTrace(childException);
                if (stackMessage == null) {
                    stackMessage = reason;
                }

                childSb.append(stackMessage);
            }

            if (childSb.length() > 0) {
                childSb.insert(0, '\n');
                sb.append(childSb.toString());
            }
        }

        // ErrorDialog only prints the call stack for a CoreException
        exception = new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, IStatus.ERROR, sb.toString(), null));
        final Status statusWithException = new Status(IStatus.ERROR, Activator.PLUGIN_ID, IStatus.ERROR, org.eclipse.tracecompass.internal.tmf.ui.project.wizards.tracepkg.Messages.TracePackage_ErrorMultipleProblems, exception);

        Activator.getDefault().logError(message, exception);
        ErrorDialog.openError(getContainer().getShell(), org.eclipse.tracecompass.internal.tmf.ui.project.wizards.tracepkg.Messages.TracePackage_InternalErrorTitle, message, statusWithException);
    }

    /**
     * Handle errors occurring in the wizard operations
     *
     * @param message
     *            the error message
     * @param exception
     *            the exception attached to the message
     */
    protected void handleError(String message, Throwable exception) {
        Activator.getDefault().logError(message, exception);
        displayErrorDialog(message, exception);
    }

    private void displayErrorDialog(String message, Throwable exception) {
        if (exception == null) {
            final Status s = new Status(IStatus.ERROR, Activator.PLUGIN_ID, message);
            ErrorDialog.openError(getContainer().getShell(), org.eclipse.tracecompass.internal.tmf.ui.project.wizards.tracepkg.Messages.TracePackage_InternalErrorTitle, null, s);
            return;
        }

        String reason = exception.getMessage();
        // Some system exceptions have no message
        if (reason == null) {
            reason = exception.toString();
        }

        String stackMessage = ExceptionUtils.getStackTrace(exception);
        if (stackMessage == null || stackMessage.isEmpty()) {
            stackMessage = reason;
        }

        // ErrorDialog only prints the call stack for a CoreException
        CoreException coreException = new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, IStatus.ERROR, stackMessage, exception));
        final Status s = new Status(IStatus.ERROR, Activator.PLUGIN_ID, IStatus.ERROR, reason, coreException);
        ErrorDialog.openError(getContainer().getShell(), org.eclipse.tracecompass.internal.tmf.ui.project.wizards.tracepkg.Messages.TracePackage_InternalErrorTitle, message, s);
    }

    /**
     * A version of setSubtreeChecked that is aware of isEnabled
     *
     * @param viewer
     *            the viewer
     * @param element
     *            the element
     * @param enabledOnly
     *            if only enabled elements should be considered
     * @param checked
     *            true if the item should be checked, and false if it should be
     *            unchecked
     */
    protected static void setSubtreeChecked(CheckboxTreeViewer viewer, TracePackageElement element, boolean enabledOnly, boolean checked) {
        if (!enabledOnly || element.isEnabled()) {
            viewer.setChecked(element, checked);
            if (checked) {
                viewer.setGrayed(element, false);
            }
            element.setChecked(checked);
            if (element.getChildren() != null) {
                for (TracePackageElement child : element.getChildren()) {
                    setSubtreeChecked(viewer, child, enabledOnly, checked);
                }
            }
        }
    }

    /**
     * Sets all items in the element viewer to be checked or unchecked
     *
     * @param viewer
     *            the viewer
     * @param enabledOnly
     *            if only enabled elements should be considered
     * @param checked
     *            whether or not items should be checked
     */
    protected static void setAllChecked(CheckboxTreeViewer viewer, boolean enabledOnly, boolean checked) {
        TreeItem[] items = viewer.getTree().getItems();
        for (int i = 0; i < items.length; i++) {
            Object element = items[i].getData();
            setSubtreeChecked(viewer, (TracePackageElement) element, enabledOnly, checked);
        }
    }

    private static void addToHistory(List<String> history, String newEntry) {
        history.remove(newEntry);
        history.add(0, newEntry);

        // since only one new item was added, we can be over the limit
        // by at most one item
        if (history.size() > COMBO_HISTORY_LENGTH) {
            history.remove(COMBO_HISTORY_LENGTH);
        }
    }

    private static String[] addToHistory(String[] history, String newEntry) {
        ArrayList<String> l = new ArrayList<>(Arrays.asList(history));
        addToHistory(l, newEntry);
        String[] r = new String[l.size()];
        l.toArray(r);
        return r;
    }

    /**
     * Open an appropriate file dialog so that the user can specify a file to
     * import/export
     * @param fileDialogStyle
     */
    private void handleFilePathBrowseButtonPressed(int fileDialogStyle) {
        FileDialog dialog = TmfFileDialogFactory.create(getContainer().getShell(), fileDialogStyle | SWT.SHEET);
        dialog.setFilterExtensions(new String[] { "*.zip;*.tar.gz;*.tar;*.tgz", "*.*" }); //$NON-NLS-1$ //$NON-NLS-2$
        dialog.setText(Messages.TracePackage_FileDialogTitle);
        String currentSourceString = getFilePathValue();
        int lastSeparatorIndex = currentSourceString.lastIndexOf(File.separator);
        if (lastSeparatorIndex != -1) {
            dialog.setFilterPath(currentSourceString.substring(0, lastSeparatorIndex));
        }
        String selectedFileName = dialog.open();

        if (selectedFileName != null) {
            setFilePathValue(selectedFileName);
            updateWithFilePathSelection();
        }
    }

    /**
     * Get the current file path value
     *
     * @return the current file path value
     */
    protected String getFilePathValue() {
        return fFilePathCombo.getText().trim();
    }

    /**
     * Set the file path value
     *
     * @param value
     *            file path value
     */
    protected void setFilePathValue(String value) {
        fFilePathCombo.setText(value);
        updatePageCompletion();
    }

    /**
     * Update the approximate size of the selected elements
     */
    protected void updateApproximateSelectedSize() {
    }

    /**
     * Get the element tree viewer
     *
     * @return the element tree viewer
     */
    protected CheckboxTreeViewer getElementViewer() {
        return fElementViewer;
    }

    /**
     * Get the file path combo box
     *
     * @return the file path combo box
     */
    protected Combo getFilePathCombo() {
        return fFilePathCombo;
    }

    /**
     * Get the object selection when the wizard was created
     *
     * @return the object selection
     */
    protected IStructuredSelection getSelection() {
        return fSelection;
    }
}
