/*******************************************************************************
 * Copyright (c) 2016, 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.internal.analysis.profiling.core.callgraph;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;

import org.eclipse.core.runtime.IProgressMonitor;
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.CallStackAnalysis;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException;
import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
import org.eclipse.tracecompass.tmf.core.analysis.TmfAbstractAnalysisModule;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;

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

/**
 * Call stack analysis used to create a segment for each call function from an
 * entry/exit event. It builds a segment tree from the state system. An example
 * taken from the Fibonacci trace's callStack shows the structure of the segment
 * tree given by this analysis:
 *
 * <pre>
 * (Caller)  main
 *            ↓↑
 * (Callee) Fibonacci
 *           ↓↑    ↓↑
 *      Fibonacci Fibonacci
 *         ↓↑         ↓↑
 *         ...        ...
 * </pre>
 *
 * @author Sonia Farrah
 */
public class CallGraphAnalysis extends TmfAbstractAnalysisModule implements ICallGraphProvider {

    /**
     * ID
     */
    public static final String ID = "org.eclipse.tracecompass.internal.analysis.profiling.core.callgraph.callgraphanalysis"; //$NON-NLS-1$

    // ------------------------------------------------------------------------
    // Attributes
    // ------------------------------------------------------------------------

    /**
     * The List of thread nodes. Each thread has a virtual node having the root
     * function as children
     */
    private List<ThreadNode> fThreadNodes = new ArrayList<>();

    private final @Nullable CallStackAnalysis fCallStackAnalysis;

    /**
     * Protected constructor, without the analysis
     */
    protected CallGraphAnalysis() {
        super();
        fCallStackAnalysis = null;
    }

    /**
     * Default constructor
     *
     * @param callStackAnalysis
     *            The callstack analysis this callgraph will be built upon
     */
    public CallGraphAnalysis(CallStackAnalysis callStackAnalysis) {
        super();
        fCallStackAnalysis = callStackAnalysis;
    }

    @Override
    public @NonNull String getHelpText() {
        String msg = Messages.CallGraphAnalysis_Description;
        return (msg != null) ? msg : super.getHelpText();
    }

    @Override
    public @NonNull String getHelpText(@NonNull ITmfTrace trace) {
        return getHelpText();
    }

    @Override
    public boolean canExecute(ITmfTrace trace) {
        /*
         * FIXME: change to !Iterables.isEmpty(getDependentAnalyses()) when
         * analysis dependencies work better
         */
        return true;
    }

    @Override
    protected Iterable<IAnalysisModule> getDependentAnalyses() {
        CallStackAnalysis callStackAnalysis = fCallStackAnalysis;
        if (callStackAnalysis == null) {
            throw new NullPointerException("If the analysis is not set, this method should not be called"); //$NON-NLS-1$
        }
        return Collections.singleton(callStackAnalysis);
    }

    @Override
    protected boolean executeAnalysis(@Nullable IProgressMonitor monitor) {
        ITmfTrace trace = getTrace();
        if (monitor == null || trace == null) {
            return false;
        }
        CallStackAnalysis callstackModule = fCallStackAnalysis;
        if (callstackModule == null) {
            return false;
        }
        callstackModule.schedule();
        callstackModule.waitForCompletion(monitor);
        // TODO:Look at updates while the state system's being built
        String[] threadsPattern = callstackModule.getThreadsPattern();
        String[] processesPattern = callstackModule.getProcessesPattern();
        ITmfStateSystem ss = callstackModule.getStateSystem();
        if (ss == null || !iterateOverStateSystem(ss, threadsPattern, processesPattern, monitor)) {
            return false;
        }
        monitor.worked(1);
        monitor.done();
        return true;

    }

    /**
     * Iterate over the process of the state system,then iterate over the
     * different threads of each process.
     *
     * @param ss
     *            The state system
     * @param threadsPattern
     *            The threads pattern
     * @param processesPattern
     *            The processes pattern
     * @param monitor
     *            The monitor
     * @return true if completed successfully
     */
    @VisibleForTesting
    protected boolean iterateOverStateSystem(ITmfStateSystem ss, String[] threadsPattern, String[] processesPattern, IProgressMonitor monitor) {
        List<Integer> processQuarks = ss.getQuarks(processesPattern);
        Map<ThreadNode, List<Integer>> mainAttribs = new HashMap<>();
        for (int processQuark : processQuarks) {
            int processId = getProcessId(ss, processQuark, ss.getCurrentEndTime());
            for (int threadQuark : ss.getQuarks(processQuark, threadsPattern)) {
                int callStackQuark = ss.optQuarkRelative(threadQuark, CallStackAnalysis.CALL_STACK);
                if (callStackQuark == ITmfStateSystem.INVALID_ATTRIBUTE) {
                    continue;
                }
                List<Integer> subAttributes = ss.getSubAttributes(callStackQuark, false);
                if (subAttributes.isEmpty()) {
                    continue;
                }
                String threadName = ss.getAttributeName(threadQuark);
                long threadId = getProcessId(ss, threadQuark, ss.getStartTime());
                AbstractCalledFunction initSegment = CalledFunctionFactory.create(0, 0, -1, threadName, processId, null);
                ThreadNode init = new ThreadNode(initSegment, 0, threadId);
                fThreadNodes.add(init);
                mainAttribs.put(init, subAttributes);

            }
        }
        iterateOverCallStack2D(ss, mainAttribs, monitor);
        return true;
    }

    /** A class that represents a time range for an interval or function */
    private static class CallgraphRange {
        private final long fStart;
        private final long fEnd;

        public CallgraphRange(long start, long end) {
            fStart = start;
            fEnd = end;
        }

        /* Is there a common range with the other range */
        public boolean overlap(CallgraphRange other) {
            if (fStart <= other.fEnd && fEnd >= other.fStart) {
                return true;
            }
            return false;
        }

        /* Do 2 time ranges overlap or are they contiguous */
        public boolean overlapOrContiguous(CallgraphRange other) {
            // Do these overlap
            if (overlap(other)) {
                return true;
            }
            // Are they contiguous
            if (fStart - 1 == other.fEnd || fEnd + 1 == other.fStart) {
                return true;
            }
            return false;
        }

        /* Is the other range fully included in this range */
        public boolean includes(CallgraphRange other) {
            return (fStart <= other.fStart && fEnd >= other.fEnd);
        }

        /* Is the interval fully included in this range */
        public boolean includes(ITmfStateInterval interval) {
            return (fStart <= interval.getStartTime() && fEnd >= interval.getEndTime());
        }

        /*
         * Get the callgraph range that represents the intersection with the
         * other range
         */
        public @Nullable CallgraphRange getIntersection(CallgraphRange other) {
            if (fStart > other.fEnd || fEnd < other.fStart) {
                return null;
            }
            return new CallgraphRange(Math.max(fStart, other.fStart), Math.min(fEnd, other.fEnd));
        }

        /*
         * Return a range that is the union of this and the other range. It
         * supposes that both ranges overlap or are contiguous
         */
        public CallgraphRange getUnion(CallgraphRange other) {
            return new CallgraphRange(Math.min(fStart, other.fStart), Math.max(fEnd, other.fEnd));
        }

    }

    /** A class that associates a range with a function */
    private static class FunctionCall {
        CallgraphRange fRange;
        AbstractCalledFunction fFunc;

        public FunctionCall(CallgraphRange range, AbstractCalledFunction function) {
            fRange = range;
            fFunc = function;
        }
    }

    /** Represent a callgraph level in the algorithm */
    private static class CallGraphLevel {

        private final ThreadNode fThreadNode;
        private final List<CallgraphRange> fRanges = new ArrayList<>();
        private final Map<AggregatedCalledFunction, FunctionCall> fAggregated = new HashMap<>();
        private final List<ITmfStateInterval> fOrphanedIntervals = new ArrayList<>();
        private final @Nullable CallGraphLevel fParent;
        private final int fDepth;
        private @Nullable CallGraphLevel fChild = null;

        public CallGraphLevel(ThreadNode threadNode, int depth, @Nullable CallGraphLevel parent) {
            fThreadNode = threadNode;
            fDepth = depth;
            fParent = parent;
        }

        public void addInterval(ITmfStateInterval interval) {
            fOrphanedIntervals.add(interval);
        }

        public void setChild(CallGraphLevel childLvl) {
            fChild = childLvl;
        }

        public void setCovered(CallgraphRange newRange) {
            // Try to get the biggest range including the new one from all the
            // others
            List<CallgraphRange> toRemove = new ArrayList<>();
            CallgraphRange addRange = newRange;
            for (CallgraphRange range : fRanges) {
                if (newRange.overlapOrContiguous(range)) {
                    addRange = addRange.getUnion(range);
                    toRemove.add(range);
                }
            }
            for (CallgraphRange range : toRemove) {
                fRanges.remove(range);
            }
            fRanges.add(addRange);
        }

        private @Nullable AggregatedCalledFunction findAggregated(CallgraphRange range) {
            for (Entry<AggregatedCalledFunction, FunctionCall> entry : fAggregated.entrySet()) {
                if (entry.getValue().fRange.includes(range)) {
                    return entry.getKey();
                }
            }
            return null;
        }

        /* Find an aggregated call in the parent that spans this range */
        public @Nullable AggregatedCalledFunction findParentAggregated(CallgraphRange range) {
            CallGraphLevel parent = fParent;
            if (parent == null) {
                return fThreadNode;
            }
            return parent.findAggregated(range);
        }

        private boolean isRangeCovered(CallgraphRange range) {
            for (CallgraphRange coveredRange : fRanges) {
                if (coveredRange.includes(range)) {
                    return true;
                }
            }
            return false;
        }

        /*
         * Recursively adopt all children intervals. Return whether the range is
         * fully covered or if there is still missing information
         */
        public boolean recursiveCoverChildren(CallgraphRange range, AbstractCalledFunction function, AggregatedCalledFunction aggregated) {
            CallGraphLevel child = fChild;
            // Last level, coverage complete
            if (child == null) {
                return true;
            }
            // Set child's already covered ranges (nulls) as covered
            for (CallgraphRange childRange : child.fRanges) {
                CallgraphRange covered = range.getIntersection(childRange);
                if (covered != null) {
                    setCovered(covered);
                }
            }
            List<ITmfStateInterval> toRemove = new ArrayList<>();
            // Look if any orphaned interval in the child is within range
            for (ITmfStateInterval interval : child.fOrphanedIntervals) {
                if (!range.includes(interval)) {
                    continue;
                }
                /*
                 * The interval is within range, process it
                 *
                 * Create a function and aggregated call site for it
                 */
                toRemove.add(interval);
                CallgraphRange childRange = new CallgraphRange(interval.getStartTime(), interval.getEndTime());
                AbstractCalledFunction childFunc = CalledFunctionFactory.create(childRange.fStart, childRange.fEnd + 1, fDepth + 1, Objects.requireNonNull(interval.getValue()), fThreadNode.getProcessId(), function);
                AggregatedCalledFunction childAgg = new AggregatedCalledFunction(childFunc, aggregated);

                /*
                 * Recursively try to adopt intervals in the child
                 *
                 * Is the child fully covered ?
                 */
                if (child.recursiveCoverChildren(childRange, childFunc, childAgg)) {
                    /*
                     * Yes, add the child to the current aggregated call and set
                     * this range as covered in both child and current level
                     */
                    aggregated.addChild(childFunc, childAgg);
                    child.setCovered(childRange);
                    setCovered(childRange);
                } else {
                    /* No, save the new aggregated call in the child */
                    child.fAggregated.put(childAgg, new FunctionCall(childRange, childFunc));
                }
            }
            // Remove orphaned intervals that found a parent
            child.fOrphanedIntervals.removeAll(toRemove);
            return isRangeCovered(range);
        }

        public void tryToCompleteParentCoverage(CallgraphRange range) {
            CallGraphLevel parent = fParent;
            if (parent == null) {
                return;
            }
            parent.tryToCompleteCoverage(range, this);
        }

        private void tryToCompleteCoverage(CallgraphRange range, CallGraphLevel child) {
            List<AggregatedCalledFunction> toRemove = new ArrayList<>();
            /*
             * Look at all the aggregated function to see those that overlap the
             * current range, there could be many in case the range represents a
             * null interval
             */
            for (Entry<AggregatedCalledFunction, FunctionCall> entry : fAggregated.entrySet()) {
                CallgraphRange parentRange = entry.getValue().fRange;
                if (parentRange.overlap(range)) {
                    /*
                     * This function overlaps the range, maybe the child
                     * coverage is complete now
                     */
                    if (child.isRangeCovered(parentRange)) {
                        /*
                         * Function range fully covered in the child, add it to
                         * its parent now and try to complete the parent
                         */
                        toRemove.add(entry.getKey());
                        AggregatedCalledFunction parent = findParentAggregated(parentRange);
                        if (parent == null) {
                            // The parent was already covered, probably because
                            // there were null values above, just ignore
                            continue;
                        }
                        parent.addChild(entry.getValue().fFunc, entry.getKey());
                        setCovered(parentRange);
                        tryToCompleteParentCoverage(parentRange);
                    }
                }
            }
            for (AggregatedCalledFunction agg : toRemove) {
                fAggregated.remove(agg);
            }
        }

        public @Nullable FunctionCall getParentData(AggregatedCalledFunction parentCall) {
            CallGraphLevel parent = fParent;
            if (parent == null) {
                return null;
            }
            return parent.fAggregated.get(parentCall);
        }

        public int getDepth() {
            return fDepth;
        }

        public int getProcessId() {
            return fThreadNode.getProcessId();
        }

    }

    private static boolean iterateOverCallStack2D(ITmfStateSystem ss, Map<ThreadNode, List<Integer>> parentAttribs, IProgressMonitor monitor) {
        try {
            long start = ss.getStartTime();
            long end = ss.getCurrentEndTime();

            Map<Integer, CallGraphLevel> attribToLevel = new HashMap<>();
            List<Integer> attributes = new ArrayList<>();

            // Create the levels for all the thread nodes and attributes
            for (Entry<ThreadNode, List<Integer>> entry : parentAttribs.entrySet()) {
                ThreadNode threadNode = entry.getKey();
                List<Integer> subAttributes = entry.getValue();
                attributes.addAll(subAttributes);
                CallGraphLevel prevLevel = null;
                for (int i = 0; i < subAttributes.size(); i++) {
                    CallGraphLevel level = new CallGraphLevel(threadNode, i, prevLevel);
                    if (prevLevel != null) {
                        prevLevel.setChild(level);
                    }
                    prevLevel = level;
                    attribToLevel.put(subAttributes.get(i), level);
                }
            }

            /*
             * Do a 2D query, starting from the end of the state system, the
             * intervals ending last (ie typically the ones of lower depth) will
             * come first, though they are not sorted by end time per se, but as
             * a general trend, the callstack will be parsed from the end.
             */
            for (ITmfStateInterval interval : ss.query2D(attributes, end, start)) {
                if (monitor.isCanceled()) {
                    return false;
                }
                CallGraphLevel level = attribToLevel.get(interval.getAttribute());
                if (level == null) {
                    throw new NullPointerException("The level should not be null, we created it just before!"); //$NON-NLS-1$
                }

                long intervalStart = interval.getStartTime();
                long intervalEnd = interval.getEndTime();
                CallgraphRange range = new CallgraphRange(intervalStart, intervalEnd);
                Object value = interval.getValue();
                /* Is the interval null ? */
                if (value == null) {
                    /*
                     * Yes, there is no function to process at this level so we
                     * set this range as covered
                     */
                    level.setCovered(range);

                } else {
                    /* No, this interval represents a called function */
                    /*
                     * Is there a parent aggregated site already for this
                     * function ?
                     */
                    AggregatedCalledFunction parent = level.findParentAggregated(range);
                    if (parent == null) {
                        /* No, keep this interval for later and continue */
                        level.addInterval(interval);
                        continue;
                    }
                    /*
                     * Yes, create the function and aggregated callsite from
                     * this interval
                     */
                    FunctionCall parentData = level.getParentData(parent);
                    AbstractCalledFunction function = CalledFunctionFactory.create(intervalStart, intervalEnd + 1, level.getDepth(), value, level.getProcessId(), (parentData == null) ? null : parentData.fFunc);
                    AggregatedCalledFunction aggregated = new AggregatedCalledFunction(function, parent);
                    /*
                     * See if there are any children intervals to process and
                     * add to this aggregated site
                     */
                    /*
                     * Do we have all children information for this interval's
                     * function ?
                     */
                    if (!level.recursiveCoverChildren(range, function, aggregated)) {
                        /*
                         * No, save this function to be completed later and
                         * continue
                         */
                        level.fAggregated.put(aggregated, new FunctionCall(range, function));
                        continue;
                    }
                    /*
                     * Yes, add the current site to the parent and set this
                     * range as covered for the current level
                     */
                    parent.addChild(function, aggregated);
                    level.setCovered(range);
                }

                /*
                 * See if we can complete the parent(s) with this new information
                 */
                level.tryToCompleteParentCoverage(range);
            }
        } catch (StateSystemDisposedException e) {
            return false;
        }
        return true;
    }

    @Override
    protected void canceling() {
        // Do nothing
    }

    /**
     * Merged threadnodes
     *
     * @return the merged threadnodes
     */
    public Collection<ThreadNode> getFlameGraph() {
        AbstractCalledFunction initSegment = CalledFunctionFactory.create(0, 0, -1, "", 0, null); //$NON-NLS-1$
        ThreadNode init = new ThreadNode(initSegment, 0, 0);
        fThreadNodes.forEach(
                tn -> tn.getChildren().forEach(
                        child -> init.addChild(initSegment, child.clone())));
        return Collections.singleton(init);

    }

    /**
     * List of thread nodes. Each thread has a virtual node having the root
     * functions called as children.
     *
     * @return The thread nodes
     */
    public List<ThreadNode> getThreadNodes() {
        return ImmutableList.copyOf(fThreadNodes);
    }

    private static int getProcessId(ITmfStateSystem ss, int processQuark, long curTime) {
        if (processQuark != ITmfStateSystem.ROOT_ATTRIBUTE) {
            try {
                ITmfStateInterval interval = ss.querySingleState(curTime, processQuark);
                String processName = ss.getAttributeName(processQuark);
                Object processValue = interval.getValue();
                if (processValue != null && (processValue instanceof Integer || processValue instanceof Long)) {
                    return ((Number) processValue).intValue();
                }
                return Integer.parseInt(processName);
            } catch (StateSystemDisposedException | NumberFormatException e) {
                /* use default processId */
            }
        }
        return -1;
    }

}