/*******************************************************************************
 * Copyright (c) 2008, 2018 Red Hat, Inc.
 * 
 * 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:
 *    Elliott Baron <ebaron@redhat.com> - initial API and implementation
 *******************************************************************************/
package org.eclipse.linuxtools.internal.valgrind.massif;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.IFontProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.linuxtools.dataviewers.charts.actions.SaveChartAction;
import org.eclipse.linuxtools.internal.valgrind.massif.MassifSnapshot.SnapshotType;
import org.eclipse.linuxtools.internal.valgrind.massif.charting.ChartEditorInput;
import org.eclipse.linuxtools.internal.valgrind.massif.charting.HeapChart;
import org.eclipse.linuxtools.valgrind.ui.IValgrindToolView;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.plugin.AbstractUIPlugin;

public class MassifViewPart extends ViewPart implements IValgrindToolView {

    private static final String TITLE_STACKS = Messages
    .getString("MassifViewPart.Stacks"); //$NON-NLS-1$
    private static final String TITLE_EXTRA = Messages
    .getString("MassifViewPart.Extra_Heap"); //$NON-NLS-1$
    private static final String TITLE_USEFUL = Messages
    .getString("MassifViewPart.Useful_Heap"); //$NON-NLS-1$
    private static final String TITLE_TOTAL = Messages
    .getString("MassifViewPart.Total"); //$NON-NLS-1$
    private static final String TITLE_TIME = Messages
    .getString("MassifViewPart.Time"); //$NON-NLS-1$
    private static final String TITLE_NUMBER = Messages
    .getString("MassifViewPart.Snapshot"); //$NON-NLS-1$
    private static final String TREE_ACTION = MassifPlugin.PLUGIN_ID
    + ".treeAction"; //$NON-NLS-1$
    public static final String CHART_ACTION = MassifPlugin.PLUGIN_ID
    + ".chartAction"; //$NON-NLS-1$
    public static final String PID_ACTION = MassifPlugin.PLUGIN_ID
    + ".pidAction"; //$NON-NLS-1$
    public static final String SAVE_CHART_ACTION = MassifPlugin.PLUGIN_ID
    + ".saveChartAction"; //$NON-NLS-1$

    private MassifOutput output;
    private Integer pid;

    private Composite top;
    private StackLayout stackLayout;
    private TableViewer viewer;
    private MassifTreeViewer treeViewer;
    private MassifHeapTreeNode[] nodes;
    private String chartName;

    private static final int COLUMN_SIZE = 125;

    private Action treeAction;
    private MassifPidMenuAction pidAction;
    private SaveChartAction saveChartAction;

    private List<ChartEditorInput> chartInputs;

    @Override
    public void createPartControl(Composite parent) {
        chartInputs = new ArrayList<>();

        top = new Composite(parent, SWT.NONE);
        stackLayout = new StackLayout();
        top.setLayout(stackLayout);
        top.setLayoutData(new GridData(GridData.FILL_BOTH));

        viewer = new TableViewer(top, SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL
                | SWT.FULL_SELECTION);

        Table table = viewer.getTable();
        table.setLayoutData(new GridData(GridData.FILL_BOTH));


        String[] columnTitles = { TITLE_NUMBER, TITLE_TIME, TITLE_TOTAL,
                TITLE_USEFUL, TITLE_EXTRA, TITLE_STACKS };

        for (int i = 0; i < columnTitles.length; i++) {
            TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE);
            column.getColumn().setText(columnTitles[i]);
            column.getColumn().setWidth(COLUMN_SIZE);
            column.getColumn().setResizable(true);
            column.getColumn().addSelectionListener(getHeaderListener());
        }
        table.setHeaderVisible(true);
        table.setLinesVisible(true);

        viewer.setContentProvider(new ArrayContentProvider());
        viewer.setLabelProvider(new MassifLabelProvider());

        treeViewer = new MassifTreeViewer(top);
        treeViewer.getViewer().getControl().setLayoutData(new GridData(GridData.FILL_BOTH));

        viewer.addDoubleClickListener(event -> {
		    MassifSnapshot snapshot = (MassifSnapshot) ((IStructuredSelection) event
		            .getSelection()).getFirstElement();
		    if (snapshot.getType() != SnapshotType.EMPTY) {
		        treeAction.setChecked(true);
		        setTopControl(treeViewer.getViewer().getControl());
		        treeViewer.getViewer().setSelection(new StructuredSelection(snapshot
		                .getRoot()), true);
		        treeViewer.getViewer().expandToLevel(snapshot.getRoot(),
		                AbstractTreeViewer.ALL_LEVELS);
		    }
		});

        stackLayout.topControl = viewer.getControl();
        top.layout();
    }

    private String getUnitString(MassifSnapshot[] snapshots) {
        String result;
        MassifSnapshot snapshot = snapshots[0];
        switch (snapshot.getUnit()) {
        case BYTES:
            result = "B"; //$NON-NLS-1$
            break;
        case INSTRUCTIONS:
            result = "i"; //$NON-NLS-1$
            break;
        default:
            result = "ms"; //$NON-NLS-1$
        break;
        }
        return result;
    }

    private SelectionListener getHeaderListener() {
		return SelectionListener.widgetSelectedAdapter(e -> {
			TableColumn column = (TableColumn) e.widget;
			Table table = viewer.getTable();
			if (column.equals(table.getSortColumn())) {
				int direction = table.getSortDirection() == SWT.UP ? SWT.DOWN : SWT.UP;
				table.setSortDirection(direction);
			} else {
				table.setSortDirection(SWT.UP);
			}
			table.setSortColumn(column);
			viewer.setComparator(new ViewerComparator() {
				@Override
				public int compare(Viewer viewer, Object o1, Object o2) {
					Table table = ((TableViewer) viewer).getTable();
					int direction = table.getSortDirection();
					MassifSnapshot s1 = (MassifSnapshot) o1;
					MassifSnapshot s2 = (MassifSnapshot) o2;
					long result;
					TableColumn column = table.getSortColumn();
					if (column.getText().equals(TITLE_NUMBER)) {
						result = s1.getNumber() - s2.getNumber();
					} else if (column.getText().startsWith(TITLE_TIME)) {
						result = s1.getTime() - s2.getTime();
					} else if (column.getText().equals(TITLE_TOTAL)) {
						result = s1.getTotal() - s2.getTotal();
					} else if (column.getText().equals(TITLE_USEFUL)) {
						result = s1.getHeapBytes() - s2.getHeapBytes();
					} else if (column.getText().equals(TITLE_EXTRA)) {
						result = s1.getHeapExtra() - s2.getHeapExtra();
					} else {
						result = s1.getStacks() - s2.getStacks();
					}

					// ascending / descending
					result = direction == SWT.UP ? result : -result;

					// overflow check
					if (result > Integer.MAX_VALUE) {
						result = Integer.MAX_VALUE;
					} else if (result < Integer.MIN_VALUE) {
						result = Integer.MIN_VALUE;
					}
					return (int) result;
				}
			});
		});
    }

    @Override
    public IAction[] getToolbarActions() {
        pidAction = new MassifPidMenuAction(this);
        pidAction.setId(PID_ACTION);

        Action chartAction = new Action(
                Messages.getString("MassifViewPart.Display_Heap_Allocation"), IAction.AS_PUSH_BUTTON) { //$NON-NLS-1$
            @Override
            public void run() {
                ChartEditorInput input = getChartInput(pid);
                if (input != null) {
                    displayChart(input);
                }
            }
        };
        chartAction.setId(CHART_ACTION);
        chartAction.setImageDescriptor(AbstractUIPlugin.imageDescriptorFromPlugin(
                MassifPlugin.PLUGIN_ID, "icons/linecharticon.gif")); //$NON-NLS-1$
        chartAction.setToolTipText(Messages
                .getString("MassifViewPart.Display_Heap_Allocation")); //$NON-NLS-1$

        saveChartAction = new SaveChartAction();
        saveChartAction.setId(SAVE_CHART_ACTION);

        treeAction = new Action(
                Messages.getString("MassifViewPart.Show_Heap_Tree"), IAction.AS_CHECK_BOX) { //$NON-NLS-1$
            @Override
            public void run() {
                if (isChecked()) {
                    stackLayout.topControl = treeViewer.getViewer().getControl();
                    top.layout();
                } else {
                    stackLayout.topControl = viewer.getControl();
                    top.layout();
                }
            }
        };
        treeAction.setId(TREE_ACTION);
        treeAction.setImageDescriptor(AbstractUIPlugin.imageDescriptorFromPlugin(
                MassifPlugin.PLUGIN_ID, "icons/call_hierarchy.gif")); //$NON-NLS-1$
        treeAction.setToolTipText(Messages
                .getString("MassifViewPart.Show_Heap_Tree")); //$NON-NLS-1$

        return new IAction[] { pidAction, chartAction, saveChartAction, treeAction };
    }

    private void createChart(MassifSnapshot[] snapshots) {
        String title = chartName + " [PID: " + pid + "]";  //$NON-NLS-1$//$NON-NLS-2$
        HeapChart chart = new HeapChart(snapshots, title);

        String name = getInputName(title);
        ChartEditorInput input = new ChartEditorInput(chart, this, name, pid);
        chartInputs.add(input);

        // open the editor
        displayChart(input);
    }

    private void displayChart(final ChartEditorInput chartInput) {
        Display.getDefault().syncExec(() -> {
		    try {
		        IWorkbenchPage page = PlatformUI.getWorkbench()
		        .getActiveWorkbenchWindow().getActivePage();
		        page.openEditor(chartInput, MassifPlugin.EDITOR_ID);
		    } catch (PartInitException e) {
		        e.printStackTrace();
		    }
		});
        saveChartAction.setChart(chartInput.getChart().getChartControl(), chartInput.getName());
    }

    private String getInputName(String description) {
        String launchName;
        try {
            launchName = description.substring(0, description
                    .indexOf("[massif")); //$NON-NLS-1$
        }
        catch(IndexOutOfBoundsException e) {
            launchName = "(No chart title)"; //$NON-NLS-1$
        }
        return launchName.trim();
    }

    @Override
    public void setFocus() {
        viewer.getTable().setFocus();
    }

    @Override
    public void refreshView() {
        if (output != null && pid != null) {
            saveChartAction.setChart(null);
            MassifSnapshot[] snapshots = output.getSnapshots(pid);
            pidAction.setPids(output.getPids());
            if (snapshots != null) {
                viewer.setInput(snapshots);

                String timeWithUnit = TITLE_TIME + " (" + getUnitString(snapshots) + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                for (TableColumn column : viewer.getTable().getColumns()) {
                    if (column.getText().startsWith(TITLE_TIME)) {
                        column.setText(timeWithUnit);
                    }
                    viewer.getTable().layout(true);
                }
                MassifSnapshot[] detailed = getDetailed(snapshots);
                nodes = new MassifHeapTreeNode[detailed.length];
                for (int i = 0; i < detailed.length; i++) {
                    nodes[i] = detailed[i].getRoot();
                }
                treeViewer.getViewer().setInput(nodes);

                // create and display chart
                if (snapshots.length > 0) {
                    ChartEditorInput input = getChartInput(pid);
                    if (input == null) {
                        createChart(snapshots);
                    }
                    else {
                        displayChart(input);
                    }
                }
            }
        }
    }

    @Override
    public void dispose() {
        // Close all chart editors to keep Valgrind output consistent throughout workbench
        IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
        if (page != null) {
            for (IEditorInput input : chartInputs) {
                IEditorPart part = page.findEditor(input);
                if (part != null) {
                    page.closeEditor(part, false);
                }
            }
        }
        super.dispose();
    }

    public void setTopControl(Control control) {
        stackLayout.topControl = control;
        top.layout(true);
    }

    public void setOutput(MassifOutput output) {
        this.output = output;
    }

    public MassifOutput getOutput() {
        return output;
    }

    public void setPid(Integer pid) {
        this.pid = pid;
    }

    public Integer getPid() {
        return pid;
    }

    public MassifSnapshot[] getSnapshots() {
        return output != null && pid != null ? output.getSnapshots(pid) : null;
    }

    public TableViewer getTableViewer() {
        return viewer;
    }

    public MassifTreeViewer getTreeViewer() {
        return treeViewer;
    }

    private static class MassifLabelProvider extends LabelProvider implements
    ITableLabelProvider, IFontProvider {

        @Override
        public Image getColumnImage(Object element, int columnIndex) {
            Image image = null;
            if (columnIndex == 0) {
                MassifSnapshot snapshot = (MassifSnapshot) element;
                switch (snapshot.getType()) {
                case EMPTY:
                    break;
                case PEAK:
                case DETAILED:
                    image = AbstractUIPlugin
                    .imageDescriptorFromPlugin(MassifPlugin.PLUGIN_ID,
                            "icons/call_hierarchy.gif").createImage(); //$NON-NLS-1$
                }
            }
            return image;
        }

        @Override
        public String getColumnText(Object element, int columnIndex) {
            MassifSnapshot snapshot = (MassifSnapshot) element;
            DecimalFormat df = new DecimalFormat("#,##0"); //$NON-NLS-1$
            switch (columnIndex) {
            case 0:
                return df.format(snapshot.getNumber());
            case 1:
                return df.format(snapshot.getTime());
            case 2:
                return df.format(snapshot.getTotal());
            case 3:
                return df.format(snapshot.getHeapBytes());
            case 4:
                return df.format(snapshot.getHeapExtra());
            default:
                return df.format(snapshot.getStacks());
            }
        }

        @Override
        public Font getFont(Object element) {
            Font font = null;
            MassifSnapshot snapshot = (MassifSnapshot) element;
            switch (snapshot.getType()) {
            case EMPTY:
            case DETAILED:
                break;
            case PEAK:
                font = JFaceResources.getFontRegistry().getBold(
                        JFaceResources.DIALOG_FONT);
            }
            return font;
        }
    }

    public void setChartName(String chartName) {
        this.chartName = chartName;
    }

    private MassifSnapshot[] getDetailed(MassifSnapshot[] snapshots) {
        ArrayList<MassifSnapshot> list = new ArrayList<>();
        for (MassifSnapshot snapshot : snapshots) {
            if (snapshot.getType() != SnapshotType.EMPTY) {
                list.add(snapshot);
            }
        }
        return list.toArray(new MassifSnapshot[list.size()]);
    }

    private ChartEditorInput getChartInput(Integer pid) {
        ChartEditorInput result = null;
        for (int i = 0; i < chartInputs.size(); i++) {
            ChartEditorInput input = chartInputs.get(i);
            if (input.getPid().equals(pid)) {
                result = input;
            }
        }
        return result;
    }
}
