/*******************************************************************************
 * Copyright (c) 2015, 2016 EfficiOS Inc., Alexandre Montplaisir
 *
 * 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.lttng2.ust.core.analysis.debuginfo;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.OptionalLong;
import java.util.Set;
import java.util.TreeSet;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.internal.lttng2.ust.core.analysis.debuginfo.UstDebugInfoStateProvider;
import org.eclipse.tracecompass.lttng2.ust.core.trace.LttngUstTrace;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
import org.eclipse.tracecompass.statesystem.core.StateSystemUtils;
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.tmf.core.analysis.requirements.TmfAbstractAnalysisRequirement;
import org.eclipse.tracecompass.tmf.core.exceptions.TmfAnalysisException;
import org.eclipse.tracecompass.tmf.core.statesystem.ITmfStateProvider;
import org.eclipse.tracecompass.tmf.core.statesystem.TmfStateSystemAnalysisModule;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.ctf.core.trace.CtfUtils;

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

/**
 * Analysis to provide TMF Callsite information by mapping IP (instruction
 * pointer) contexts to address/line numbers via debug information.
 *
 * @author Alexandre Montplaisir
 * @since 2.0
 */
public class UstDebugInfoAnalysisModule extends TmfStateSystemAnalysisModule {

    /**
     * Analysis ID, it should match that in the plugin.xml file
     */
    public static final String ID = "org.eclipse.linuxtools.lttng2.ust.analysis.debuginfo"; //$NON-NLS-1$

    @Override
    protected ITmfStateProvider createStateProvider() {
        return new UstDebugInfoStateProvider(checkNotNull(getTrace()));
    }

    @Override
    public boolean setTrace(ITmfTrace trace) throws TmfAnalysisException {
        if (!(trace instanceof LttngUstTrace)) {
            return false;
        }
        return super.setTrace(trace);
    }

    /**
     * @since 3.0
     */
    @Override
    @Nullable
    public LttngUstTrace getTrace() {
        return (LttngUstTrace) super.getTrace();
    }

    @Override
    public Iterable<TmfAbstractAnalysisRequirement> getAnalysisRequirements() {
        // TODO specify actual requirements once the requirement-checking is
        // implemented. This analysis needs "ip" and "vpid" contexts.
        return Collections.emptySet();
    }

    @Override
    public boolean canExecute(ITmfTrace trace) {
        /* The analysis can only work with LTTng-UST traces... */
        if (!(trace instanceof LttngUstTrace)) {
            return false;
        }
        LttngUstTrace ustTrace = (LttngUstTrace) trace;
        String tracerName = CtfUtils.getTracerName(ustTrace);
        int majorVersion = CtfUtils.getTracerMajorVersion(ustTrace);
        int minorVersion = CtfUtils.getTracerMinorVersion(ustTrace);

        /* ... taken with UST >= 2.8 ... */
        if (!LttngUstTrace.TRACER_NAME.equals(tracerName)) {
            return false;
        }
        if (majorVersion < 2) {
            return false;
        }
        if (majorVersion == 2 && minorVersion < 8) {
            return false;
        }

        /* ... that respect the ip/vpid contexts requirements. */
        return super.canExecute(trace);
    }

    // ------------------------------------------------------------------------
    // Class-specific operations
    // ------------------------------------------------------------------------

    /**
     * Return all the binaries that were detected in the trace.
     *
     * @return The binaries (executables or libraries) referred to in the trace.
     */
    public Collection<UstDebugInfoBinaryFile> getAllBinaries() {
        ITmfStateSystem ss = getStateSystem();
        if (ss == null) {
            /* State system might not yet be initialized */
            return Collections.emptySet();
        }

        final @NonNull Set<UstDebugInfoBinaryFile> files = new TreeSet<>();
        ImmutableList.Builder<Integer> builder = ImmutableList.builder();
        List<Integer> vpidQuarks = ss.getSubAttributes(ITmfStateSystem.ROOT_ATTRIBUTE, false);
        for (Integer vpidQuark : vpidQuarks) {
            builder.addAll(ss.getSubAttributes(vpidQuark, false));
        }
        List<Integer> baddrQuarks = builder.build();

        try {
            for (Integer baddrQuark : baddrQuarks) {
                int buildIdQuark = ss.getQuarkRelative(baddrQuark, UstDebugInfoStateProvider.BUILD_ID_ATTRIB);
                int debugLinkQuark = ss.getQuarkRelative(baddrQuark, UstDebugInfoStateProvider.DEBUG_LINK_ATTRIB);
                int pathQuark = ss.getQuarkRelative(baddrQuark, UstDebugInfoStateProvider.PATH_ATTRIB);
                int isPICQuark = ss.getQuarkRelative(baddrQuark, UstDebugInfoStateProvider.IS_PIC_ATTRIB);
                long ts = ss.getStartTime();

                /*
                 * Iterate over each mapping there ever was at this base
                 * address.
                 */
                ITmfStateInterval interval = StateSystemUtils.queryUntilNonNullValue(ss, baddrQuark, ts, Long.MAX_VALUE);
                while (interval != null) {
                    ts = interval.getStartTime();

                    ITmfStateValue filePathStateValue = ss.querySingleState(ts, pathQuark).getStateValue();
                    String filePath = filePathStateValue.unboxStr();

                    ITmfStateValue buildIdStateValue = ss.querySingleState(ts, buildIdQuark).getStateValue();
                    String buildId = unboxStrOrNull(buildIdStateValue);

                    ITmfStateValue debuglinkStateValue = ss.querySingleState(ts, debugLinkQuark).getStateValue();
                    String debugLink = unboxStrOrNull(debuglinkStateValue);

                    ITmfStateValue isPICStateValue = ss.querySingleState(ts, isPICQuark).getStateValue();
                    Boolean isPIC = isPICStateValue.unboxInt() != 0;

                    files.add(new UstDebugInfoBinaryFile(filePath, buildId, debugLink, isPIC));

                    /*
                     * Go one past the end of the interval, and perform the
                     * query again to find the next mapping at this address.
                     */
                    ts = interval.getEndTime() + 1;
                    interval = StateSystemUtils.queryUntilNonNullValue(ss, baddrQuark, ts, Long.MAX_VALUE);
                }
            }
        } catch (AttributeNotFoundException | TimeRangeException | StateSystemDisposedException e) {
            /* Oh well, such is life. */
        }
        return files;
    }

    /**
     * Get the binary file (executable or library) that corresponds to a given
     * instruction pointer, at a given time.
     *
     * @param ts
     *            The timestamp
     * @param vpid
     *            The VPID of the process we are querying for
     * @param ip
     *            The instruction pointer of the trace event. Normally comes
     *            from a 'ip' context.
     * @return A {@link UstDebugInfoLoadedBinaryFile} object, describing the
     *         binary file and its base address.
     * @noreference Meant to be used internally by
     *              {@link UstDebugInfoBinaryAspect} only.
     */
    @VisibleForTesting
    public @Nullable UstDebugInfoLoadedBinaryFile getMatchingFile(long ts, long vpid, long ip) {
        try {
            final ITmfStateSystem ss = getStateSystem();
            if (ss == null) {
                /* State system might not yet be initialized */
                return null;
            }

            List<Integer> possibleBaddrQuarks = ss.getQuarks(String.valueOf(vpid), "*"); //$NON-NLS-1$
            List<ITmfStateInterval> state = ss.queryFullState(ts);

            /* Get the most probable base address from all the known ones */
            OptionalLong potentialBaddr = possibleBaddrQuarks.stream()
                    /* Keep only currently (at ts) mapped objects. */
                    .filter(quark -> Objects.equals(1, state.get(quark).getValue()))
                    .map(ss::getAttributeName)
                    .mapToLong(Long::parseLong)
                    .filter(baddr -> baddr <= ip)
                    .max();

            if (!potentialBaddr.isPresent()) {
                return null;
            }

            long baddr = potentialBaddr.getAsLong();
            final int baddrQuark = ss.getQuarkAbsolute(String.valueOf(vpid),
                                                       String.valueOf(baddr));

            final int memszQuark = ss.getQuarkRelative(baddrQuark, UstDebugInfoStateProvider.MEMSZ_ATTRIB);
            final long memsz = state.get(memszQuark).getStateValue().unboxLong();

            /* Make sure the 'ip' fits the range of this object. */
            if (!(ip < baddr + memsz)) {
                /*
                 * Not the correct memory range after all. We do not have
                 * information about the library that was loaded here.
                 */
                return null;
            }

            final int pathQuark = ss.getQuarkRelative(baddrQuark, UstDebugInfoStateProvider.PATH_ATTRIB);
            String filePath = state.get(pathQuark).getStateValue().unboxStr();

            final int buildIdQuark = ss.getQuarkRelative(baddrQuark, UstDebugInfoStateProvider.BUILD_ID_ATTRIB);
            ITmfStateValue buildIdValue = state.get(buildIdQuark).getStateValue();
            String buildId = unboxStrOrNull(buildIdValue);

            final int debugLinkQuark = ss.getQuarkRelative(baddrQuark, UstDebugInfoStateProvider.DEBUG_LINK_ATTRIB);
            ITmfStateValue debugLinkValue = state.get(debugLinkQuark).getStateValue();
            String debugLink = unboxStrOrNull(debugLinkValue);

            final int isPicQuark = ss.getQuarkRelative(baddrQuark, UstDebugInfoStateProvider.IS_PIC_ATTRIB);
            boolean isPic = state.get(isPicQuark).getStateValue().unboxInt() != 0;

            // The baddrQuark interval lasts for the time this file is loaded
            ITmfStateInterval validityInterval = state.get(baddrQuark);

            return new UstDebugInfoLoadedBinaryFile(baddr, filePath, buildId, debugLink, isPic, validityInterval.getStartTime(), validityInterval.getEndTime());

        } catch (AttributeNotFoundException | TimeRangeException | StateSystemDisposedException e) {
            /* Either the data is not available yet, or incomplete. */
            return null;
        }
    }

    private static @Nullable String unboxStrOrNull(ITmfStateValue value) {
        return (value.isNull() ? null : value.unboxStr());
    }
}
