/*******************************************************************************
 * Copyright (c) 2015, 2018 École Polytechnique de Montréal and others
 *
 * 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
 *******************************************************************************/

package org.eclipse.tracecompass.internal.analysis.graph.ui.criticalpath.view;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Phaser;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.tracecompass.analysis.graph.core.criticalpath.CriticalPathModule;
import org.eclipse.tracecompass.internal.analysis.graph.core.dataprovider.CriticalPathDataProvider;
import org.eclipse.tracecompass.internal.analysis.graph.core.dataprovider.CriticalPathEntry;
import org.eclipse.tracecompass.internal.provisional.tmf.ui.widgets.timegraph.BaseDataProviderTimeGraphPresentationProvider;
import org.eclipse.tracecompass.internal.tmf.core.model.filters.FetchParametersUtils;
import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
import org.eclipse.tracecompass.tmf.core.model.filters.TimeQueryFilter;
import org.eclipse.tracecompass.tmf.core.model.timegraph.ITimeGraphArrow;
import org.eclipse.tracecompass.tmf.core.model.timegraph.ITimeGraphDataProvider;
import org.eclipse.tracecompass.tmf.core.model.timegraph.ITimeGraphState;
import org.eclipse.tracecompass.tmf.core.model.tree.ITmfTreeDataModel;
import org.eclipse.tracecompass.tmf.core.response.TmfModelResponse;
import org.eclipse.tracecompass.tmf.core.signal.TmfEndSynchSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
import org.eclipse.tracecompass.tmf.core.signal.TmfStartAnalysisSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfStartSynchSignal;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager;
import org.eclipse.tracecompass.tmf.ui.views.timegraph.BaseDataProviderTimeGraphView;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ILinkEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeGraphEntry;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.TimeGraphEntry;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.TimeLinkEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.widgets.Utils;
import org.eclipse.ui.IWorkbenchActionConstants;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Table;

/**
 * The Critical Path view
 *
 * @author Geneviève Bastien
 * @author Francis Giraldeau
 */
public class CriticalPathView extends BaseDataProviderTimeGraphView {

    // ------------------------------------------------------------------------
    // Constants
    // ------------------------------------------------------------------------

    /** View ID */
    public static final String ID = "org.eclipse.linuxtools.tmf.analysis.graph.ui.criticalpath.view.criticalpathview"; //$NON-NLS-1$

    private static final double NANOINV = 10E-9;

    private static final String COLUMN_PROCESS = Messages.getMessage(Messages.CriticalFlowView_columnProcess);
    private static final String COLUMN_ELAPSED = Messages.getMessage(Messages.CriticalFlowView_columnElapsed);
    private static final String COLUMN_PERCENT = Messages.getMessage(Messages.CriticalFlowView_columnPercent);

    private static final String[] COLUMN_NAMES = new String[] {
            COLUMN_PROCESS,
            COLUMN_ELAPSED,
            COLUMN_PERCENT
    };

    private static final String[] FILTER_COLUMN_NAMES = new String[] {
            COLUMN_PROCESS
    };

    private Phaser fPhaser = new Phaser();

    private class CriticalPathTreeLabelProvider extends TreeLabelProvider {

        @Override
        public String getColumnText(@Nullable Object element, int columnIndex) {
            if (!(element instanceof TimeGraphEntry)) {
                return StringUtils.EMPTY;
            }
            TimeGraphEntry entry = (TimeGraphEntry) element;
            ITmfTreeDataModel model = entry.getEntryModel();
            if (columnIndex == 0) {
                return entry.getName();
            } else if (columnIndex == 1 && model instanceof CriticalPathEntry) {
                return String.format("%.9f", ((CriticalPathEntry) model).getSum() * NANOINV); //$NON-NLS-1$
            } else if (columnIndex == 2 && model instanceof CriticalPathEntry) {
                double percent = ((CriticalPathEntry) model).getPercent();
                return String.format("%.2f", percent * 100); //$NON-NLS-1$
            }
            return StringUtils.EMPTY;
        }
    }

    private class CriticalPathEntryComparator implements Comparator<ITimeGraphEntry> {

        @Override
        public int compare(@Nullable ITimeGraphEntry o1, @Nullable ITimeGraphEntry o2) {
            if ((o1 instanceof TimeGraphEntry) && (o2 instanceof TimeGraphEntry)) {
                ITmfTreeDataModel model1 = ((TimeGraphEntry) o1).getEntryModel();
                ITmfTreeDataModel model2 = ((TimeGraphEntry) o2).getEntryModel();
                if (model1 instanceof CriticalPathEntry && model2 instanceof CriticalPathEntry) {
                    return Long.compare(((CriticalPathEntry) model2).getSum(), ((CriticalPathEntry) model1).getSum());
                }
            }
            return 0;
        }
    }

    /**
     * Constructor
     */
    public CriticalPathView() {
        super(ID, new BaseDataProviderTimeGraphPresentationProvider(), CriticalPathDataProvider.ID);
        setTreeColumns(COLUMN_NAMES);
        setFilterColumns(FILTER_COLUMN_NAMES);
        setTreeLabelProvider(new CriticalPathTreeLabelProvider());
        setEntryComparator(new CriticalPathEntryComparator());
    }

    // ------------------------------------------------------------------------
    // Internal
    // ------------------------------------------------------------------------

    @Override
    protected List<ITimeEvent> createTimeEvents(TimeGraphEntry entry, List<ITimeGraphState> values) {
        // need to override to not have transparent events in the gaps.
        return Lists.transform(values, state -> createTimeEvent(entry, state));
    }

    @Override
    protected List<ILinkEvent> getLinkList(long startTime, long endTime, long resolution, IProgressMonitor monitor) {
        List<@NonNull TimeGraphEntry> traceEntries = getEntryList(getTrace());
        if (traceEntries == null) {
            return Collections.emptyList();
        }
        List<@NonNull ILinkEvent> linkList = new ArrayList<>();
        TimeQueryFilter queryFilter = new TimeQueryFilter(startTime, endTime, 2);

        /*
         * group entries by critical path data provider as several hosts may refer to
         * the same data provider
         */
        Table<ITimeGraphDataProvider<?>, Long, TimeGraphEntry> table = HashBasedTable.create();
        for (TraceEntry traceEntry : Iterables.filter(traceEntries, TraceEntry.class)) {
            for (TimeGraphEntry entry : Utils.flatten(traceEntry)) {
                table.put(traceEntry.getProvider(), entry.getEntryModel().getId(), entry);
            }
        }

        for (Map.Entry<ITimeGraphDataProvider<?>, Map<Long, TimeGraphEntry>> entry : table.rowMap().entrySet()) {
            ITimeGraphDataProvider<?> provider = entry.getKey();
            Map<Long, TimeGraphEntry> map = entry.getValue();
            TmfModelResponse<List<ITimeGraphArrow>> response = provider.fetchArrows(FetchParametersUtils.timeQueryToMap(queryFilter), monitor);
            List<ITimeGraphArrow> model = response.getModel();

            if (monitor.isCanceled()) {
                return null;
            }
            if (model != null) {
                for (ITimeGraphArrow arrow : model) {
                    ITimeGraphEntry src = map.get(arrow.getSourceId());
                    ITimeGraphEntry dst = map.get(arrow.getDestinationId());
                    if (src != null && dst != null) {
                        linkList.add(new TimeLinkEvent(src, dst, arrow.getStartTime(), arrow.getDuration(), arrow.getValue()));
                    }
                }
            }
        }
        return linkList;
    }

    /**
     * Signal handler for analysis started, we need to rebuilt the entry list with
     * updated statistics values for the current graph worker of the critical path
     * module.
     *
     * @param signal
     *            The signal
     */
    @TmfSignalHandler
    public void analysisStarted(TmfStartAnalysisSignal signal) {
        IAnalysisModule analysis = signal.getAnalysisModule();
        if (analysis instanceof CriticalPathModule) {
            CriticalPathModule criticalPath = (CriticalPathModule) analysis;
            /*
             * We need to wait for CriticalPathDataProviderFactory to have
             * received this signal. Create a new phaser and register, and wait
             * for the end synch signal to arrive and advance in a new thread.
             */
            fPhaser = new Phaser();
            fPhaser.register();
            new Thread() {
                @Override
                public void run() {
                    fPhaser.awaitAdvance(0);
                    Collection<ITmfTrace> traces = TmfTraceManager.getTraceSetWithExperiment(getTrace());
                    if (traces.contains(criticalPath.getTrace())) {
                        rebuild();
                    }
                }
            }.start();
        }
    }

    /**
     * Handler for the start synch signal
     *
     * @param signal
     *            Incoming signal
     */
    @TmfSignalHandler
    public void startSynch(TmfStartSynchSignal signal) {
        fPhaser.register();
    }

    /**
     * Handler for the end synch signal
     *
     * @param signal
     *            Incoming signal
     */
    @TmfSignalHandler
    public void endSynch(TmfEndSynchSignal signal) {
        fPhaser.arriveAndDeregister();
    }

    @Override
    protected void fillLocalToolBar(@Nullable IToolBarManager manager) {
        super.fillLocalToolBar(manager);
        if (manager == null) {
            return;
        }
        IAction followArrowBwdAction = getTimeGraphViewer().getFollowArrowBwdAction();
        followArrowBwdAction.setText(Messages.CriticalPathView_followArrowBwdText);
        followArrowBwdAction.setToolTipText(Messages.CriticalPathView_followArrowBwdText);
        manager.appendToGroup(IWorkbenchActionConstants.MB_ADDITIONS, followArrowBwdAction);

        IAction followArrowFwdAction = getTimeGraphViewer().getFollowArrowFwdAction();
        followArrowFwdAction.setText(Messages.CriticalPathView_followArrowFwdText);
        followArrowFwdAction.setToolTipText(Messages.CriticalPathView_followArrowFwdText);
        manager.appendToGroup(IWorkbenchActionConstants.MB_ADDITIONS, followArrowFwdAction);
    }

    @Override
    protected Iterable<ITmfTrace> getTracesToBuild(@Nullable ITmfTrace trace) {
        // we need the critical path module to run on the experiment, not the traces.
        return trace != null ? Collections.singleton(trace) : Collections.emptyList();
    }

}
