/*******************************************************************************
 * Copyright (c) 2013, 2019 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
 *******************************************************************************/
package org.eclipse.tracecompass.analysis.profiling.core.callstack;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.analysis.profiling.core.callgraph.ICallGraphProvider;
import org.eclipse.tracecompass.analysis.profiling.core.callstack.CallStackHostUtils.TraceHostIdResolver;
import org.eclipse.tracecompass.analysis.profiling.core.callstack.CallStackSeries.IThreadIdResolver;
import org.eclipse.tracecompass.analysis.timing.core.segmentstore.IAnalysisProgressListener;
import org.eclipse.tracecompass.internal.analysis.profiling.core.callgraph.CallGraphAnalysis;
import org.eclipse.tracecompass.internal.analysis.profiling.core.callstack.SymbolAspect;
import org.eclipse.tracecompass.internal.tmf.core.analysis.callsite.CallsiteAnalysis;
import org.eclipse.tracecompass.segmentstore.core.ISegment;
import org.eclipse.tracecompass.segmentstore.core.ISegmentStore;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
import org.eclipse.tracecompass.tmf.core.event.aspect.ITmfEventAspect;
import org.eclipse.tracecompass.tmf.core.event.aspect.TmfCpuAspect;
import org.eclipse.tracecompass.tmf.core.event.aspect.TmfDeviceAspect;
import org.eclipse.tracecompass.tmf.core.exceptions.TmfAnalysisException;
import org.eclipse.tracecompass.tmf.core.segment.ISegmentAspect;
import org.eclipse.tracecompass.tmf.core.statesystem.TmfStateSystemAnalysisModule;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;

/**
 * The base classes for analyses who want to populate the CallStack state
 * system.
 *
 * @author Matthew Khouzam
 */
public abstract class CallStackAnalysis extends TmfStateSystemAnalysisModule implements IFlameChartProvider, ICallGraphProvider {

    /** CallStack stack-attribute
     * @since 1.1*/
    public static final String CALL_STACK = "CallStack"; //$NON-NLS-1$

    private static final String[] DEFAULT_PROCESSES_PATTERN = new String[] { CallStackStateProvider.PROCESSES, "*" }; //$NON-NLS-1$

    private static final String[] DEFAULT_THREADS_PATTERN = new String[] { "*" }; //$NON-NLS-1$

    private final CallGraphAnalysis fCallGraphAnalysis;

    private final ListenerList<IAnalysisProgressListener> fListeners = new ListenerList<>(ListenerList.IDENTITY);

    private @Nullable CallStackSeries fCallStacks = null;
    private final List<String[]> fPatterns;

    private boolean fAutomaticCallgraph;

    /**
     * Abstract constructor (should only be called via the sub-classes'
     * constructors.
     */
    protected CallStackAnalysis() {
        super();
        fCallGraphAnalysis = new CallGraphAnalysis(this);
        fPatterns = ImmutableList.of(getProcessesPattern(), getThreadsPattern());
    }

    /**
     * The quark pattern, relative to the root, to get the list of attributes
     * representing the different processes of a trace.
     * <p>
     * If the trace does not define processes, an empty array can be returned.
     * <p>
     * The pattern is passed as-is to
     * {@link org.eclipse.tracecompass.statesystem.core.ITmfStateSystem#getQuarks(String...)}.
     * <p>
     * Override this method if the state system attributes do not match the
     * default pattern defined by {@link CallStackStateProvider}.
     *
     * @return The quark pattern to find the process attributes
     */
    public String[] getProcessesPattern() {
        return DEFAULT_PROCESSES_PATTERN;
    }

    /**
     * Resolve the device ID if applicable. A device is the hardware context the trace is running
     * on. An example would be CPU, GPU, DSP or even FPGA. This could allow device centric analyses
     * such as the Callsite analysis to enrich the view.
     *
     * @param quark
     *            quark of the state system to query
     * @param timestamp
     *            time stamp to query
     * @return the device ID
     * @since 1.2
     */
    public @Nullable Long resolveDeviceId(int quark, long timestamp) {
        return null;
    }


    /**
     * Resolve the device type if applicable. A device is the hardware context
     * the trace is running on. An example would be CPU, GPU, DSP or even FPGA.
     * This could allow device centric analyses such as the
     * {@link CallsiteAnalysis} to enrich the view.
     *
     * @implNote the default implementation is associating the callstack to a
     *           CPU first, then any device type or "unknown" if the trace has
     *           no device information. An implementer could override this to
     *           give a custom implementation with ordering, for example, if
     *           GPUs are higher priority, however, it is up to them to make
     *           sure the data is coherent.
     *
     * @param quark
     *            quark of the state system to query
     * @param timestamp
     *            time stamp to query
     * @return the device type
     * @since 1.2
     */
    public @Nullable String resolveDeviceType(int quark, long timestamp) {
        ITmfTrace trace = getTrace();
        if (trace != null) {
            for (ITmfEventAspect<?> aspect : trace.getEventAspects()) {
                if (aspect instanceof TmfCpuAspect) {
                    return ((TmfCpuAspect) aspect).getDeviceType();
                }
            }
            for (ITmfEventAspect<?> aspect : trace.getEventAspects()) {
                if (aspect instanceof TmfDeviceAspect) {
                    return ((TmfDeviceAspect) aspect).getDeviceType();
                }
            }
        }
        return "unknown"; //$NON-NLS-1$
    }

    /**
     * The quark pattern, relative to an attribute found by
     * {@link #getProcessesPattern()}, to get the list of attributes
     * representing the threads of a process, or the threads a trace if the
     * process pattern was empty.
     * <p>
     * If the trace does not define threads, an empty array can be returned.
     * <p>
     * This will be passed as-is to
     * {@link org.eclipse.tracecompass.statesystem.core.ITmfStateSystem#getQuarks(int, String...)}.
     * <p>
     * Override this method if the state system attributes do not match the
     * default pattern defined by {@link CallStackStateProvider}.
     *
     * @return The quark pattern to find the thread attributes
     */
    public String[] getThreadsPattern() {
        return DEFAULT_THREADS_PATTERN;
    }

    /**
     * Get the patterns for the the different levels of callstack in the state
     * system. By default, it returns a list of the patterns returned by
     * {@link #getProcessesPattern()} and {@link #getThreadsPattern()}. If the
     * analysis has another pattern hierarchy, this method should be overridden.
     *
     * @return The patterns for the different levels in the state system
     * @since 1.1
     */
    protected List<String[]> getPatterns() {
        return fPatterns;
    }

    // ------------------------------------------------------------------------
    // Method overwrites for sub-modules
    // ------------------------------------------------------------------------

    @Override
    protected boolean executeAnalysis(@Nullable IProgressMonitor monitor) {
        boolean result = super.executeAnalysis(monitor);
        if (!result) {
            return false;
        }
        if (fAutomaticCallgraph) {
            fCallGraphAnalysis.schedule();
        }
        return result;
    }

    @Override
    public boolean setTrace(@NonNull ITmfTrace trace) throws TmfAnalysisException {
        boolean ret = super.setTrace(trace);
        if (!ret) {
            return ret;
        }
        ret = fCallGraphAnalysis.setTrace(trace);
        return ret;
    }

    @Override
    public void dispose() {
        fCallGraphAnalysis.dispose();
        super.dispose();
    }

    // ------------------------------------------------------------------------
    // ISegmentStoreProvider
    // ------------------------------------------------------------------------

    @Override
    public @Nullable CallStackSeries getCallStackSeries() {
        CallStackSeries callstacks = fCallStacks;
        if (callstacks == null) {
            ITmfStateSystem ss = getStateSystem();
            ITmfTrace trace = getTrace();
            if (ss == null || trace == null) {
                return null;
            }
            callstacks = new CallStackSeries(ss, getPatterns(), 0, "", getCallStackHostResolver(trace), getCallStackTidResolver()); //$NON-NLS-1$
            fCallStacks = callstacks;
        }
        return callstacks;
    }

    /**
     * Get the callstack host ID resolver for this instrumented series. The default
     * is to use the host name of the trace.
     *
     * @param trace
     *            The trace this analysis is run on
     * @return The host ID resolver
     * @since 1.1
     */
    protected TraceHostIdResolver getCallStackHostResolver(ITmfTrace trace) {
        return new CallStackHostUtils.TraceHostIdResolver(trace);
    }

    /**
     * Get the callstack TID resolver for this instrumented series. The default is
     * to use the name of the second attribute as the thread ID.
     *
     * @return The thread ID resolver
     * @since 1.1
     */
    protected @Nullable IThreadIdResolver getCallStackTidResolver() {
        return new CallStackSeries.AttributeValueThreadResolver(1);
    }

    @Override
    public @Nullable ISegmentStore<ISegment> getSegmentStore() {
        CallStackSeries series = getCallStackSeries();
        if (series == null) {
            return null;
        }
        return series;
    }

    @Override
    public void addListener(@NonNull IAnalysisProgressListener listener) {
        fListeners.add(listener);
    }

    @Override
    public void removeListener(@NonNull IAnalysisProgressListener listener) {
        fListeners.remove(listener);
    }

    @Override
    public Iterable<ISegmentAspect> getSegmentAspects() {
        return Collections.singletonList(SymbolAspect.SYMBOL_ASPECT);
    }

    /**
     * Returns all the listeners
     *
     * @return latency listeners
     * @since 1.1
     */
    protected Iterable<IAnalysisProgressListener> getListeners() {
        List<IAnalysisProgressListener> listeners = new ArrayList<>();
        for (Object listener : fListeners.getListeners()) {
            if (listener != null) {
                listeners.add((IAnalysisProgressListener) listener);
            }
        }
        return listeners;
    }

    /**
     * Send the segment store to all its listener
     *
     * @param store
     *            The segment store to broadcast
     * @since 1.1
     */
    protected void sendUpdate(final ISegmentStore<ISegment> store) {
        for (IAnalysisProgressListener listener : getListeners()) {
            listener.onComplete(this, store);
        }
    }

    /**
     * Get the callgraph module associated with this callstack
     *
     * @return The callgraph module
     */
    public ICallGraphProvider getCallGraph() {
        return fCallGraphAnalysis;
    }

    /**
     * Set whether the callgraph execution should be triggered automatically after
     * building the callstack or if it should wait to be requested. This is used
     * in benchmark to control when the callgraph module will be built.
     *
     * @param trigger
     *            {@code true} means the callgraph analysis will be executed after
     *            the callstack, {@code false} means it will be executed on demand
     *            only.
     * @since 1.1
     */
    @VisibleForTesting
    public void triggerAutomatically(boolean trigger) {
        fAutomaticCallgraph = trigger;
    }

}