/*******************************************************************************
 * Copyright (c) 2014, 2015 École Polytechnique de Montréal
 *
 * 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
 *
 * Contributors:
 *   Geneviève Bastien - Initial API and implementation
 *******************************************************************************/

package org.eclipse.tracecompass.analysis.os.linux.core.kernel;

import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.analysis.os.linux.core.model.ProcessStatus;
import org.eclipse.tracecompass.common.core.NonNullUtils;
import org.eclipse.tracecompass.internal.analysis.os.linux.core.kernel.Attributes;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
import org.eclipse.tracecompass.statesystem.core.StateSystemUtils;
import org.eclipse.tracecompass.statesystem.core.StateSystemUtils.QuarkIterator;
import org.eclipse.tracecompass.statesystem.core.exceptions.AttributeNotFoundException;
import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException;
import org.eclipse.tracecompass.statesystem.core.exceptions.TimeRangeException;
import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
import org.eclipse.tracecompass.statesystem.core.statevalue.ITmfStateValue;
import org.eclipse.tracecompass.statesystem.core.statevalue.ITmfStateValue.Type;

import com.google.common.collect.ImmutableSet;

/**
 * Information provider utility class that retrieves thread-related information
 * from a Linux Kernel Analysis
 *
 * @author Geneviève Bastien
 * @since 2.0
 */
public final class KernelThreadInformationProvider {

    private KernelThreadInformationProvider() {
    }

    /**
     * Get the ID of the thread running on the CPU at time ts
     *
     * TODO: This method may later be replaced by an aspect, when the aspect can
     * resolve to something that is not an event
     *
     * @param module
     *            The kernel analysis instance to run this method on
     * @param cpuId
     *            The CPU number the process is running on
     * @param ts
     *            The timestamp at which we want the running process
     * @return The TID of the thread running on CPU cpuId at time ts or
     *         {@code null} if either no thread is running or we do not know.
     */
    public static @Nullable Integer getThreadOnCpu(KernelAnalysisModule module, long cpuId, long ts) {
        ITmfStateSystem ss = module.getStateSystem();
        if (ss == null) {
            return null;
        }
        try {
            int cpuQuark = ss.getQuarkAbsolute(Attributes.CPUS, Long.toString(cpuId), Attributes.CURRENT_THREAD);
            ITmfStateInterval interval = ss.querySingleState(ts, cpuQuark);
            ITmfStateValue val = interval.getStateValue();
            if (val.getType().equals(Type.INTEGER)) {
                return val.unboxInt();
            }
        } catch (AttributeNotFoundException | StateSystemDisposedException | TimeRangeException e) {
        }
        return null;
    }

    /**
     * The the threads that have been scheduled on the given CPU(s), for the
     * given time range. Threads with TID 0 (swapper threads) will never be
     * included.
     *
     * @param module
     *            The kernel analysis module to query
     * @param cpus
     *            The list of cpus
     * @param rangeStart
     *            The start of the time range
     * @param rangeEnd
     *            The end of the time range
     * @return A set of all the thread IDs that are run on said CPUs on the time
     *         range. Empty set if there is no thread on the CPUs in this time
     *         range. Null if the information is not available.
     * @since 2.5
     */
    public static @Nullable Set<Integer> getThreadsOfCpus(KernelAnalysisModule module, Collection<Long> cpus, long rangeStart, long rangeEnd) {
        ITmfStateSystem ss = module.getStateSystem();
        if (ss == null) {
            return null;
        }

        Set<Long> uniqueCpus = ImmutableSet.copyOf(cpus);

        int threadsQuark = ss.optQuarkAbsolute(Attributes.THREADS);
        if (threadsQuark == ITmfStateSystem.INVALID_ATTRIBUTE) {
            return null;
        }

        List<Integer> threadQuarks = ss.getSubAttributes(threadsQuark, false);
        return threadQuarks.stream()
                /*
                 * Keep only the quarks of threads that are on at least one of the
                 * wanted CPUs' run queue.
                 */
                .filter(threadQuark -> {
                    int threadCurrentCpuQuark = ss.optQuarkRelative(threadQuark, Attributes.CURRENT_CPU_RQ);
                    if (threadCurrentCpuQuark == ITmfStateSystem.INVALID_ATTRIBUTE) {
                        return false;
                    }

                    /* Check if the thread was seen on any of the requested CPUs. */
                    QuarkIterator it = new QuarkIterator(ss, threadCurrentCpuQuark, rangeStart, rangeEnd);
                    while (it.hasNext()) {
                        Object o = it.next().getValue();
                        if (o instanceof Number && uniqueCpus.contains(((Number) o).longValue())) {
                            return true;
                        }
                    }
                    return false;
                })

                /* Convert the thread quarks to their corresponding TIDs */
                .map(ss::getAttributeName)
                /* Ignore swapper threads */
                .filter(attribName -> !attribName.startsWith(Attributes.THREAD_0_PREFIX))
                .map(Integer::valueOf)
                .collect(Collectors.toSet());
    }

    /**
     * Predicate indicating if a thread state value is considered active or not.
     */
    private static final Predicate<ITmfStateValue> IS_STATE_VALUE_ACTIVE = stateValue -> {
        return !(stateValue.isNull() ||
                stateValue.equals(ProcessStatus.UNKNOWN.getStateValue()) ||
                stateValue.equals(ProcessStatus.WAIT_BLOCKED.getStateValue()) ||
                stateValue.equals(ProcessStatus.WAIT_UNKNOWN.getStateValue()));
    };

    /**
     * Return all the threads that are considered active in the given time range.
     * Threads with TID 0 (swapper threads) will never be included.
     *
     * @param module
     *            The kernel analysis module to query
     * @param rangeStart
     *            The start of the time range
     * @param rangeEnd
     *            The end of the time range
     * @return A set of all the thread IDs that are considered active in the time
     *         range. Empty set if there are none or if unavailable for the time
     *         range.
     * @since 2.5
     */
    public static Set<Integer> getActiveThreadsForRange(KernelAnalysisModule module, long rangeStart, long rangeEnd) {
        ITmfStateSystem ss = module.getStateSystem();
        if (ss == null) {
            return Collections.emptySet();
        }

        // Check the time range to avoid throwing an exception
        long start = Math.max(rangeStart, ss.getStartTime());
        long end = Math.min(rangeEnd, ss.getCurrentEndTime());
        if (start > end) {
            return Collections.emptySet();
        }

        int threadsQuark = ss.optQuarkAbsolute(Attributes.THREADS);
        if (threadsQuark == ITmfStateSystem.INVALID_ATTRIBUTE) {
            return Collections.emptySet();
        }

        List<ITmfStateInterval> fullQueryAtStart;
        try {
            fullQueryAtStart = ss.queryFullState(start);
        } catch (StateSystemDisposedException e) {
            return Collections.emptySet();
        }


        List<Integer> threadQuarks = ss.getSubAttributes(threadsQuark, false);
        return threadQuarks.stream()
                /*
                 * Keep only the quarks of threads that are considered active at
                 * some point in the time range.
                 */
                .filter(threadQuark -> {
                    /*
                     * If the thread was active at range start, we can already
                     * consider it active.
                     */
                    ITmfStateInterval intervalAtStart = fullQueryAtStart.get(threadQuark);
                    if (IS_STATE_VALUE_ACTIVE.test(intervalAtStart.getStateValue())) {
                        return true;
                    }

                    /*
                     * If it was inactive, and it remains in the exact same
                     * state for the whole time range, we can conclude it is
                     * inactive for the whole range.
                     *
                     * Note this will not catch cases where the threads goes
                     * from one inactive state to another, this will be found
                     * with the range query below.
                     */
                    if (intervalAtStart.getEndTime() >= end) {
                        return false;
                    }

                    QuarkIterator it = new QuarkIterator(ss, threadQuark, start, end);
                    while (it.hasNext()) {
                        ITmfStateInterval interval = it.next();
                        if (IS_STATE_VALUE_ACTIVE.test(interval.getStateValue())) {
                            return true;
                        }
                    }
                    /* We haven't found an active state value in the whole range. */
                    return false;
                })

                /* Convert the thread quarks to their corresponding TIDs */
                .map(ss::getAttributeName)
                /* Ignore swapper threads */
                .filter(attribName -> !attribName.startsWith(Attributes.THREAD_0_PREFIX))
                .map(Integer::parseInt)
                .collect(Collectors.toSet());
    }

    /**
     * Get the TIDs of the threads from an analysis
     *
     * @param module
     *            The kernel analysis instance to run this method on
     * @return The set of TIDs corresponding to the threads
     */
    public static Collection<Integer> getThreadIds(KernelAnalysisModule module) {
        ITmfStateSystem ss = module.getStateSystem();
        if (ss == null) {
            return Collections.emptySet();
        }
        int threadQuark;
        try {
            threadQuark = ss.getQuarkAbsolute(Attributes.THREADS);
            Set<@NonNull Integer> tids = new TreeSet<>();
            for (Integer quark : ss.getSubAttributes(threadQuark, false)) {
                final @NonNull String attributeName = ss.getAttributeName(quark);
                tids.add(attributeName.startsWith(Attributes.THREAD_0_PREFIX) ? 0 : Integer.parseInt(attributeName));
            }
            return tids;
        } catch (AttributeNotFoundException e) {
            // Do Nothing
        }
        return Collections.emptySet();
    }

    /**
     * Get the parent process ID of a thread
     *
     * @param module
     *            The kernel analysis instance to run this method on
     * @param threadId
     *            The thread ID of the process for which to get the parent
     * @param ts
     *            The timestamp at which to get the parent
     * @return The parent PID or {@code null} if the PPID is not found.
     */
    public static @Nullable Integer getParentPid(KernelAnalysisModule module, Integer threadId, long ts) {
        ITmfStateSystem ss = module.getStateSystem();
        if (ss == null) {
            return null;
        }
        Integer ppidNode;
        try {
            ppidNode = ss.getQuarkAbsolute(Attributes.THREADS, threadId.toString(), Attributes.PPID);
            ITmfStateInterval ppidInterval = ss.querySingleState(ts, ppidNode);
            ITmfStateValue ppidValue = ppidInterval.getStateValue();

            if (ppidValue.getType().equals(Type.INTEGER)) {
                return Integer.valueOf(ppidValue.unboxInt());
            }
        } catch (AttributeNotFoundException | StateSystemDisposedException | TimeRangeException e) {
        }
        return null;
    }

    /**
     * Get the process ID of a thread
     *
     * @param module
     *            The kernel analysis instance to run this method on
     * @param threadId
     *            The ID of the thread for which to get the process ID
     * @param ts
     *            The timestamp at which to get the parent
     * @return The process ID or {@code null} if the pid is not found.
     * @since 2.5
     */
    public static @Nullable Integer getProcessId(KernelAnalysisModule module, Integer threadId, long ts) {
        ITmfStateSystem ss = module.getStateSystem();
        if (ss == null) {
            return null;
        }
        try {
            int pidNode = ss.optQuarkAbsolute(Attributes.THREADS, threadId.toString());
            if (pidNode == ITmfStateSystem.INVALID_ATTRIBUTE) {
                /* The thread is invalid, return null */
                return null;
            }
            pidNode = ss.optQuarkRelative(pidNode, Attributes.PID);
            if (pidNode == ITmfStateSystem.INVALID_ATTRIBUTE) {
                /* The attribute is not there, thread is the process */
                return threadId;
            }
            ITmfStateInterval pidInterval = ss.querySingleState(ts, pidNode);
            Object pid = pidInterval.getValue();

            if (pid instanceof Integer) {
                return (Integer) pid;
            }
        } catch (StateSystemDisposedException | TimeRangeException e) {
        }
        return null;
    }

    /**
     * Get the executable name of the thread ID. If the thread ID was used
     * multiple time or the name changed in between, it will return the last
     * name the thread has taken, or {@code null} if no name is found
     *
     * @param module
     *            The kernel analysis instance to run this method on
     * @param threadId
     *            The thread ID of the process for which to get the name
     * @return The last executable name of this process, or {@code null} if not
     *         found
     *
     *         TODO: use
     *         {@link #getExecutableName(KernelAnalysisModule, Integer, long)}
     *         to handle TID reuse
     */
    public static @Nullable String getExecutableName(KernelAnalysisModule module, Integer threadId) {
        ITmfStateSystem stateSystem = module.getStateSystem();
        if(stateSystem == null) {
            return null;
        }
        return getExecutableName(module, threadId, stateSystem.getCurrentEndTime());
    }

    /**
     * Get the executable name of the thread ID at a specified time. If the
     * thread ID was used multiple time or the name changed in between, it will
     * return the last name the thread has taken before the given timestamp, or
     * {@code null} if no name is found before the time.
     *
     * In other words, if:
     * <ul>
     * <li>trace start < <em>ts</em> < start of the first process: the TID was
     * never seen to be used in this trace, and
     * {@link #getExecutableName(KernelAnalysisModule, Integer, long)} returns
     * null</li>
     * <li><em>ts</em> > trace end: the name of the last thread to run is returned.
     * (could be null if the tid was never used)</li>
     * <li>thread start > <em>ts</em> > thread end: the thread name will be the
     * thread name of the process at that time. This may be a parent as the
     * thread inherrits the parent's name until it is changed.</li>
     * <li>thread end > <em>ts</em> > next thread start: the last valid
     * executable name is returned</li>
     * </ul>
     *
     * @param module
     *            The kernel analysis instance to run this method on
     * @param threadId
     *            The thread ID of the process for which to get the name
     * @param ts
     *            timestamp to query at. It will look up the thread name of the
     *            current or closest previous lifetime
     * @return The last executable name of this process for a given time, or
     *         {@code null} if not found.
     * @since 4.1
     */
    public static @Nullable String getExecutableName(KernelAnalysisModule module, Integer threadId, long ts) {
        ITmfStateSystem ss = module.getStateSystem();
        if (ss == null) {
            return null;
        }
        int execNameNode = ss.optQuarkAbsolute(Attributes.THREADS, threadId.toString(), Attributes.EXEC_NAME);
        if (execNameNode == ITmfStateSystem.INVALID_ATTRIBUTE) {
            return null;
        }
        QuarkIterator reversedIterator = new QuarkIterator(ss, execNameNode, ts);
        while (reversedIterator.hasPrevious()) {
            Object value = reversedIterator.previous().getValue();
            if (value instanceof String) {
                return (String) value;
            }
        }
        return null;
    }

    /**
     * Get the priority of this thread at time ts
     *
     * @param module
     *            The kernel analysis instance to run this method on
     * @param threadId
     *            The ID of the thread to query
     * @param ts
     *            The timestamp at which to query
     * @return The priority of the thread or <code>-1</code> if not available
     */
    public static int getThreadPriority(KernelAnalysisModule module, int threadId, long ts) {
        ITmfStateSystem ss = module.getStateSystem();
        if (ss == null) {
            return -1;
        }
        int prioQuark = ss.optQuarkAbsolute(Attributes.THREADS, String.valueOf(threadId), Attributes.PRIO);
        if (prioQuark == ITmfStateSystem.INVALID_ATTRIBUTE) {
            return -1;
        }
        try {
            return ss.querySingleState(ts, prioQuark).getStateValue().unboxInt();
        } catch (StateSystemDisposedException e) {
            return -1;
        }
    }

    /**
     * Get the status intervals for a given thread with a resolution
     *
     * @param module
     *            The kernel analysis instance to run this method on
     * @param threadId
     *            The ID of the thread to get the intervals for
     * @param start
     *            The start time of the requested range
     * @param end
     *            The end time of the requested range
     * @param resolution
     *            The resolution or the minimal time between the requested
     *            intervals. If interval times are smaller than resolution, only
     *            the first interval is returned, the others are ignored.
     * @param monitor
     *            A progress monitor for this task
     * @return The list of status intervals for this thread, an empty list is
     *         returned if either the state system is {@code null} or the quark
     *         is not found
     */
    public static List<ITmfStateInterval> getStatusIntervalsForThread(KernelAnalysisModule module, Integer threadId, long start, long end, long resolution, IProgressMonitor monitor) {
        ITmfStateSystem ss = module.getStateSystem();
        if (ss == null) {
            return Collections.emptyList();
        }

        try {
            int threadQuark = ss.getQuarkAbsolute(Attributes.THREADS, threadId.toString());
            List<ITmfStateInterval> statusIntervals = StateSystemUtils.queryHistoryRange(ss, threadQuark, Math.max(start, ss.getStartTime()), Math.min(end - 1, ss.getCurrentEndTime()), resolution, monitor);
            return statusIntervals;
        } catch (AttributeNotFoundException | StateSystemDisposedException | TimeRangeException e) {
            // Do Nothing
        }
        return Collections.emptyList();
    }

    /**
     * Get an iterator for the status intervals of a given thread in a time range
     *
     * @param module
     *            The kernel analysis instance to run this method on
     * @param threadId
     *            The ID of the thread to get the intervals for
     * @param start
     *            The start time of the requested range
     * @param end
     *            The end time of the requested range
     * @param resolution
     *            The resolution, ie the number of nanoseconds between kernel status
     *            queries. A value lower or equal to 1 will return all intervals
     * @return The list of status intervals for this thread, an empty list is
     *         returned if either the state system is {@code null} or the quark
     *         is not found
     * @since 3.0
     */
    public static Iterator<ITmfStateInterval> getStatusIntervalsForThread(KernelAnalysisModule module, Integer threadId, long start, long end, long resolution) {
        ITmfStateSystem ss = module.getStateSystem();
        if (ss == null) {
            return NonNullUtils.checkNotNull(Collections.emptyListIterator());
        }

        int threadQuark = ss.optQuarkAbsolute(Attributes.THREADS, threadId.toString());
        if (threadQuark == ITmfStateSystem.INVALID_ATTRIBUTE) {
            return NonNullUtils.checkNotNull(Collections.emptyListIterator());
        }
        return new StateSystemUtils.QuarkIterator(ss, threadQuark, start, end - 1, resolution);
    }

}
