/*******************************************************************************
 * Copyright (c) 2009, 2018 STMicroelectronics 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:
 *    Marzia Maugeri <marzia.maugeri@st.com> - initial API and implementation
 *******************************************************************************/
package org.eclipse.linuxtools.dataviewers.abstractviewers;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.viewers.IContentProvider;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.linuxtools.binutils.utils.STSymbolManager;
import org.eclipse.linuxtools.dataviewers.STDataViewersActivator;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Item;

/**
 * This comparator is used to export the data of a STViewer to CSV format file
 */

public class STDataViewersCSVExporter {

    private final AbstractSTViewer stViewer;

    private PrintStream ps = null;

    private ISTDataViewersField[] fields = null;

    private List<Object> expandedElts = null;

    private Comparator<Object> comparator;

    private Object input = null;

    private IContentProvider contentProvider = null;

    private IDialogSettings state = null;

    private IProgressMonitor monitor;

    private String filePath = null;

    private String csvSeparator = null;

    private String csvChildMarker = null;

    private String csvLastChildMarker = null;

    private String csvChildLink = null;

    private String csvNoChildLink = null;

    private String csvLeafMarker = null;

    private String csvNodeMarker = null;

    private boolean expandAll = false;

    private boolean showHiddenColumns = false;

    private boolean exportTreePrefix = false;

    /**
     * It creates a new instance of exporter
     *
     * @param stViewer
     */
    public STDataViewersCSVExporter(AbstractSTViewer stViewer) {
        this.stViewer = stViewer;
        this.ps = System.out;
        this.state = stViewer.getViewerSettings()
                .getSection(STDataViewersCSVExporterConstants.TAG_SECTION_CSV_EXPORTER);
        restoreState();
    }

    /**
     * Gets from the .setting the export parameters
     */
    public void restoreState() {
        if (state == null) {
            resetState();
            return;
        }

        try {
            filePath = state.get(STDataViewersCSVExporterConstants.TAG_EXPORTER_OUTPUT_FILE_PATH);
            if (filePath == null) {
                resetState();
                return;
            }

            csvSeparator = state.get(STDataViewersCSVExporterConstants.TAG_EXPORTER_SEPARATOR);
            if (csvSeparator == null) {
                resetState();
                return;
            }

            csvChildMarker = state.get(STDataViewersCSVExporterConstants.TAG_EXPORTER_CHILD_MARKER);
            if (csvChildMarker == null) {
                resetState();
                return;
            }

            csvLastChildMarker = state.get(STDataViewersCSVExporterConstants.TAG_EXPORTER_LAST_CHILD_MARKER);
            if (csvLastChildMarker == null) {
                resetState();
                return;
            }

            csvChildLink = state.get(STDataViewersCSVExporterConstants.TAG_EXPORTER_CHILD_LINK);
            if (csvChildLink == null) {
                resetState();
                return;
            }

            csvNoChildLink = state.get(STDataViewersCSVExporterConstants.TAG_EXPORTER_NO_CHILD_LINK);
            if (csvNoChildLink == null) {
                resetState();
                return;
            }

            csvLeafMarker = state.get(STDataViewersCSVExporterConstants.TAG_EXPORTER_LEAF_MARKER);
            if (csvLeafMarker == null) {
                resetState();
                return;
            }

            csvNodeMarker = state.get(STDataViewersCSVExporterConstants.TAG_EXPORTER_NODE_MARKER);
            if (csvNodeMarker == null) {
                resetState();
                return;
            }

            String temp = state.get(STDataViewersCSVExporterConstants.TAG_EXPORTER_EXPAND_ALL);
            if (temp == null) {
                resetState();
                return;
            } else {
                expandAll = Boolean.parseBoolean(temp);
            }

            temp = state.get(STDataViewersCSVExporterConstants.TAG_EXPORTER_SHOW_HIDDEN_COLUMNS);
            if (temp == null) {
                resetState();
                return;
            } else {
                showHiddenColumns = Boolean.parseBoolean(temp);
            }

            if (isTreeViewerExporter()) {
                temp = state.get(STDataViewersCSVExporterConstants.TAG_EXPORTER_TREE_PREFIX);
                if (temp == null) {
                    resetState();
                    return;
                } else {
                    exportTreePrefix = Boolean.parseBoolean(temp);
                }
            } else {
                exportTreePrefix = false;
            }

        } catch (NumberFormatException nfe) {
            IStatus s = new Status(IStatus.WARNING, STDataViewersActivator.PLUGIN_ID,
                    "Invalid parameter, resetting configuration!\n" + nfe.getMessage(), nfe);
            logStatus(s);
            resetState();
        }

    }

    /**
     * Save into the .setting the export parameters
     */
    public void saveState() {
        if (state == null) {
            state = stViewer.getViewerSettings().addNewSection(
                    STDataViewersCSVExporterConstants.TAG_SECTION_CSV_EXPORTER);
        }

        state.put(STDataViewersCSVExporterConstants.TAG_EXPORTER_OUTPUT_FILE_PATH, filePath);
        state.put(STDataViewersCSVExporterConstants.TAG_EXPORTER_SEPARATOR, csvSeparator);
        state.put(STDataViewersCSVExporterConstants.TAG_EXPORTER_CHILD_MARKER, csvChildMarker);
        state.put(STDataViewersCSVExporterConstants.TAG_EXPORTER_LAST_CHILD_MARKER, csvLastChildMarker);
        state.put(STDataViewersCSVExporterConstants.TAG_EXPORTER_CHILD_LINK, csvChildLink);
        state.put(STDataViewersCSVExporterConstants.TAG_EXPORTER_NO_CHILD_LINK, csvNoChildLink);
        state.put(STDataViewersCSVExporterConstants.TAG_EXPORTER_LEAF_MARKER, csvLeafMarker);
        state.put(STDataViewersCSVExporterConstants.TAG_EXPORTER_NODE_MARKER, csvNodeMarker);
        state.put(STDataViewersCSVExporterConstants.TAG_EXPORTER_EXPAND_ALL, expandAll);
        state.put(STDataViewersCSVExporterConstants.TAG_EXPORTER_SHOW_HIDDEN_COLUMNS, showHiddenColumns);
        if (isTreeViewerExporter()) {
            state.put(STDataViewersCSVExporterConstants.TAG_EXPORTER_TREE_PREFIX, exportTreePrefix);
        } else {
            state.put(STDataViewersCSVExporterConstants.TAG_EXPORTER_TREE_PREFIX, false);
        }
    }

    /**
     * Sets to default the parameters used during the export
     */
    public void resetState() {
        filePath = STDataViewersCSVExporterConstants.DEFAULT_EXPORTER_OUTPUT_FILE_PATH;
        csvSeparator = STDataViewersCSVExporterConstants.DEFAULT_EXPORTER_SEPARATOR;
        csvChildMarker = STDataViewersCSVExporterConstants.DEFAULT_EXPORTER_CHILD_MARKER;
        csvLastChildMarker = STDataViewersCSVExporterConstants.DEFAULT_EXPORTER_LAST_CHILD_MARKER;
        csvChildLink = STDataViewersCSVExporterConstants.DEFAULT_EXPORTER_CHILD_LINK;
        csvNoChildLink = STDataViewersCSVExporterConstants.DEFAULT_EXPORTER_NO_CHILD_LINK;
        csvLeafMarker = STDataViewersCSVExporterConstants.DEFAULT_EXPORTER_LEAF_MARKER;
        csvNodeMarker = STDataViewersCSVExporterConstants.DEFAULT_EXPORTER_NODE_MARKER;
        expandAll = STDataViewersCSVExporterConstants.DEFAULT_EXPORTER_EXPAND_ALL;
        showHiddenColumns = STDataViewersCSVExporterConstants.DEFAULT_EXPORTER_SHOW_HIDDEN_COLUMNS;

        if (isTreeViewerExporter()) {
            exportTreePrefix = STDataViewersCSVExporterConstants.DEFAULT_EXPORTER_TREE_PREFIX;
        } else {
            exportTreePrefix = false;
        }
    }

    private void logStatus(IStatus s) {
        STDataViewersActivator.getDefault().getLog().log(s);
    }

    public void exportTo(String filePath) {
        setFilePath(filePath);
        export();
    }

    public void exportTo(String filePath, IProgressMonitor monitor) {
        setFilePath(filePath);
        export(monitor);
    }

    public void export() {
        export(new NullProgressMonitor());
    }

    public void export(IProgressMonitor monitor) {

        // monitoring
        this.monitor = monitor;

        monitor.beginTask("Exporting to CSV (file: " + getFilePath() + ")", IProgressMonitor.UNKNOWN);

        // save this configuration
        saveState();

        // init the printStream
        initPrintStream(filePath);

        // get the viewer's parameters
        monitor.subTask("collecting viewer's parameters...");
        collectViewerParameters();

        // start dumping to csv file
        dumpColumnHeaders();

        if (input != null) {
            if (contentProvider instanceof ITreeContentProvider) {
                ITreeContentProvider treeContentProvider = (ITreeContentProvider) contentProvider;
                Object[] topElements = treeContentProvider.getElements(input);

                if (topElements != null) {

                    // monitoring
                    int work = topElements.length;
                    for (Object element : topElements) {
                        if (treeContentProvider.hasChildren(element)) {
                            work += treeContentProvider.getChildren(element).length;
                        }
                    }
                    monitor.beginTask("Exporting to CSV (file: " + getFilePath() + ")", work);
                    monitor.subTask("exporting tree data...");

                    // monitoring stuff
                    int tempWork = 0;
                    int workFloor = topElements.length / 100;
                    if (workFloor == 0) {
                        workFloor = 1;
                    }

                    // exporting tree
                    Arrays.sort(topElements, comparator);
                    for (int i = 0; i < topElements.length; i++) {
                        dumpTreeData(treeContentProvider, topElements[i], "", i == topElements.length - 1, true);

                        // monitoring
                        tempWork++;
                        if (tempWork >= workFloor) {
                            if (monitor.isCanceled()) {
                                ps.close();
                                return;
                            }
                            monitor.worked(tempWork);
                            tempWork = 0;
                        }
                    }

                    // monitoring
                    monitor.worked(tempWork);
                }

            } else if (contentProvider instanceof IStructuredContentProvider) {
                IStructuredContentProvider structContentProvider = (IStructuredContentProvider) contentProvider;
                Object[] topElements = structContentProvider.getElements(input);

                if (topElements != null) {

                    // monitoring
                    monitor.beginTask("Exporting to CSV (file: " + getFilePath() + ")", topElements.length);
                    monitor.subTask("exporting table data...");

                    // monitoring stuff
                    int tempWork = 0;
                    int workFloor = topElements.length / 100;
                    if (workFloor == 0) {
                        workFloor = 1;
                    }

                    // exporting table
                    Arrays.sort(topElements, comparator);
                    for (int n = 0; n < topElements.length; n++) {
                        if (n < topElements.length - 1) {
                            dumpNodeData(topElements[n], csvChildMarker);
                        } else {
                            dumpNodeData(topElements[n], csvLastChildMarker);
                        }

                        // monitoring
                        tempWork++;
                        if (tempWork >= workFloor) {
                            if (monitor.isCanceled()) {
                                ps.close();
                                return;
                            }
                            monitor.worked(tempWork);
                            tempWork = 0;
                        }
                    }

                    // monitoring
                    monitor.worked(tempWork);
                }
            }
        }

        // close the stream
        ps.close();

        // end monitoring
        monitor.done();

        //FIXME EK-LINUXTOOLS: IFile c = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(new Path(getFilePath()));
        IFile c = STSymbolManager.sharedInstance.findFileFromPath(new Path(getFilePath()));
        if (c != null) {
            try {
                c.refreshLocal(1, new NullProgressMonitor());
            } catch (CoreException e) {
                STDataViewersActivator.getDefault().getLog().log(e.getStatus());
            }
        }

    }

    private void initPrintStream(String filePath) {
        try {
            File outputFile = new File(filePath);

            if (outputFile.exists())
                outputFile.delete();

            outputFile.createNewFile();

            this.ps = new PrintStream(outputFile);

        } catch (IOException e) {
            Status s = new Status(IStatus.ERROR, STDataViewersActivator.PLUGIN_ID,
                    "Invalid file! Dumping to stdout...\n" + e.getMessage(), e);
            logStatus(s);
            this.ps = System.out;
        } catch (NullPointerException e) {
            Status s = new Status(IStatus.ERROR, STDataViewersActivator.PLUGIN_ID,
                    "File has not been set! Dumping to stdout...\n" + e.getMessage(), e);
            logStatus(s);
            this.ps = System.out;
        }
    }

    private void collectViewerParameters() {
        Display.getDefault().syncExec(() -> {
		    // getting columns with the right order
		    Item[] unOrdColumns = stViewer.getColumns();
		    Item[] columns = new Item[unOrdColumns.length];
		    int[] columnOrder = stViewer.getColumnOrder();
		    for (int i1 = 0; i1 < unOrdColumns.length; i1++) {
		        columns[i1] = unOrdColumns[columnOrder[i1]];
		    }

		    // getting fields mapping
		    // (before removing hidden columns)
		    Map<Item, ISTDataViewersField> fieldsMap = new HashMap<>();
		    for (Item column : columns) {
		        fieldsMap.put(column, (ISTDataViewersField) column.getData());
		    }

		    // creating a GUI-thread-independent comparator
		    comparator = new CSVDataComparator(stViewer.getTableSorter(), fieldsMap);

		    // getting only visible columns, using the column order
		    if (!showHiddenColumns) {
		        int[] columnsState = stViewer.getHideShowManager().getColumnsState();
		        List<Item> enabledColumns = new ArrayList<>();
		        for (int i2 = 0; i2 < columnsState.length; i2++) {
		            if (columnsState[columnOrder[i2]] == STDataViewersHideShowManager.STATE_SHOWN) {
		                enabledColumns.add(columns[i2]);
		            }
		        }
		        columns = enabledColumns.toArray(new Item[enabledColumns.size()]);
		    }

		    // collecting fields from columns (ordered & shown)
		    fields = new ISTDataViewersField[columns.length];
		    for (int i3 = 0; i3 < columns.length; i3++) {
		        fields[i3] = (ISTDataViewersField) columns[i3].getData();
		    }

		    // getting input object
		    input = stViewer.getViewer().getInput();

		    // getting content provider
		    contentProvider = stViewer.getViewer().getContentProvider();

		    // getting expanded elements if necessary
		    expandedElts = new ArrayList<>();
		    if (!expandAll && stViewer.getViewer() instanceof TreeViewer) {
		        TreeViewer tv = (TreeViewer) stViewer.getViewer();
		        expandedElts = Arrays.asList(tv.getExpandedElements());
		    }
		});
    }

    private void dumpColumnHeaders() {
        printPrefix("Hierarchy");
        for (int i = 0; i < fields.length; i++) {
            ps.print(fields[i].getColumnHeaderText());
            printSeparator(i, fields.length);
        }
    }

    private void dumpTreeData(ITreeContentProvider tcp, Object element, String prefix, boolean isLastChild,
            boolean monitoring) {
        String childMarker = isLastChild ? csvLastChildMarker : csvChildMarker;

        boolean isLeaf = !tcp.hasChildren(element);
        String leafMarker = (isLeaf ? csvLeafMarker : csvNodeMarker);
        dumpNodeData(element, prefix + childMarker + leafMarker);

        if ((expandAll || expandedElts.contains(element)) && !isLeaf) {
            Object[] children = tcp.getChildren(element);
            if (children != null) {

                // monitoring stuff
                int tempWork = 0;
                int workFloor = children.length / 100;
                if (workFloor == 0) {
                    workFloor = 1;
                }

                // exporting children
                Arrays.sort(children, comparator);
                for (int i = 0; i < children.length; i++) {
                    String prefixAdd = isLastChild ? csvNoChildLink : csvChildLink;
                    dumpTreeData(tcp, children[i], prefix + prefixAdd, i == children.length - 1, false);

                    // monitoring
                    if (monitor != null && monitoring) {
                        tempWork++;
                        if (tempWork >= workFloor) {
                            monitor.worked(tempWork);
                            tempWork = 0;
                        }
                    }
                }

                // monitoring
                if (monitor != null && monitoring && tempWork > 0) {
                    monitor.worked(tempWork);
                }
            }
        }
    }

    private void dumpNodeData(Object element, String prefix) {
        for (int i = 0; i < fields.length; i++) {
            printNode(i, prefix, getText(i, element));
            printSeparator(i, fields.length);
        }
    }

    private void printNode(int col, String prefix, String text) {
        if (col == 0) {
            printPrefix(prefix);
        }
        ps.print(text);
    }

    private void printPrefix(String prefix) {
        if (exportTreePrefix) {
            ps.print(prefix);
            ps.print(csvSeparator);
        }
    }

    private void printSeparator(int i, int length) {
        if (i == length - 1) {
            ps.print("\n");
        } else {
            ps.print(csvSeparator);
        }
    }

    private String getText(int i, Object obj) {
        return fields[i].getValue(obj);
    }

    public String getFilePath() {
        return filePath;
    }

    public String getCSVSeparator() {
        return csvSeparator;
    }

    public String getCSVChildMarker() {
        return csvChildMarker;
    }

    public String getCSVLastChildMarker() {
        return csvLastChildMarker;
    }

    public String getCSVChildLink() {
        return csvChildLink;
    }

    public String getCSVNoChildLink() {
        return csvNoChildLink;
    }

    public String getCSVLeafMarker() {
        return csvLeafMarker;
    }

    public String getCSVNodeMarker() {
        return csvNodeMarker;
    }

    public boolean getExpandAll() {
        return expandAll;
    }

    public boolean getShowHiddenColumns() {
        return showHiddenColumns;
    }

    public boolean getExportTreePrefix() {
        return exportTreePrefix;
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }

    public void setCSVSeparator(String separator) {
        csvSeparator = separator;
    }

    public void setCSVChildMarker(String childMarker) {
        csvChildMarker = childMarker;
    }

    public void setCSVLastChildMarker(String lastChildMarker) {
        csvLastChildMarker = lastChildMarker;
    }

    public void setCSVChildLink(String childLink) {
        csvChildLink = childLink;
    }

    public void setCSVNoChildLink(String noChildLink) {
        csvNoChildLink = noChildLink;
    }

    public void setCSVLeafMarker(String leafMarker) {
        csvLeafMarker = leafMarker;
    }

    public void setCSVNodeMarker(String nodeMarker) {
        csvNodeMarker = nodeMarker;
    }

    public void setExpandAll(boolean expandAll) {
        this.expandAll = expandAll;
    }

    public void setShowHiddenColumns(boolean showHiddenColumns) {
        this.showHiddenColumns = showHiddenColumns;
    }

    public void setExportTreePrefix(boolean exportTreePrefix) {
        if (isTreeViewerExporter()) {
            this.exportTreePrefix = exportTreePrefix;
        } else {
            this.exportTreePrefix = false;
        }
    }

    private static class CSVDataComparator extends STDataViewersComparator {

        private final Map<Item, ISTDataViewersField> fieldsMap;

        public CSVDataComparator(STDataViewersComparator other, Map<Item, ISTDataViewersField> fieldsMap) {
            super(other);
            this.fieldsMap = fieldsMap;
        }

        @Override
        protected ISTDataViewersField getField(Item column) {
            return fieldsMap.get(column);
        }

    }

    public boolean isTreeViewerExporter() {
        return stViewer instanceof AbstractSTTreeViewer;
    }
}
