/*******************************************************************************
 * Copyright (c) 2015 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.internal.lttng2.ust.core.analysis.debuginfo;

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

import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.common.core.log.TraceCompassLog;
import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils;
import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils.ScopeLog;
import org.eclipse.tracecompass.common.core.process.ProcessUtils;
import org.eclipse.tracecompass.tmf.core.event.lookup.TmfCallsite;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Iterables;

/**
 * Utility class to get file name, function/symbol name and line number from a
 * given offset. In TMF this is represented as a {@link TmfCallsite}.
 *
 * @author Alexandre Montplaisir
 */
public final class FileOffsetMapper {

    private static final Logger LOGGER = TraceCompassLog.getLogger(FileOffsetMapper.class);

    private static final String DISCRIMINATOR = "\\(discriminator.*\\)"; //$NON-NLS-1$
    private static final String ADDR2LINE_EXECUTABLE = "addr2line"; //$NON-NLS-1$

    private static final long CACHE_SIZE = 1000;

    private FileOffsetMapper() {}

    /**
     * Class representing an offset in a specific file
     */
    private static class FileOffset {

        private final String fFilePath;
        private final @Nullable String fBuildId;
        private final long fOffset;

        public FileOffset(String filePath, @Nullable String buildId, long offset) {
            fFilePath = filePath;
            fBuildId = buildId;
            fOffset = offset;
        }

        @Override
        public int hashCode() {
            return Objects.hash(fFilePath, fBuildId, fOffset);
        }

        @Override
        public boolean equals(@Nullable Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            FileOffset other = (FileOffset) obj;
            return Objects.equals(fFilePath, other.fFilePath) &&
                    Objects.equals(fBuildId, other.fBuildId) &&
                    Objects.equals(fOffset, other.fOffset);
        }

        @Override
        public String toString() {
            return new ToStringBuilder(this)
                    .append("fFilePath", fFilePath) //$NON-NLS-1$
                    .append("fBuildId", fBuildId) //$NON-NLS-1$
                    .append("fOffset", String.format("0x%h", fOffset)) //$NON-NLS-1$ //$NON-NLS-2$
                    .toString();
        }
    }


    /**
     * Generate the callsite from a given binary file and address offset.
     *
     * Due to function inlining, it is possible for one offset to actually have
     * multiple call sites. We will return the most precise one (inner-most) we
     * have available.
     *
     * @param file
     *            The binary file to look at
     * @param buildId
     *            The expected buildId of the binary file (is not verified at
     *            the moment)
     * @param offset
     *            The memory offset in the file
     * @return The corresponding call site
     */
    public static @Nullable TmfCallsite getCallsiteFromOffset(File file, @Nullable String buildId, long offset) {
       Iterable<Addr2lineInfo> output = getAddr2lineInfo(file, buildId, offset);
       if (output == null || Iterables.isEmpty(output)) {
           return null;
       }
       Addr2lineInfo info = Iterables.getLast(output);
       String sourceFile = info.fSourceFileName;
       Long sourceLine = info.fSourceLineNumber;

       if (sourceFile == null) {
           /* Not enough information to provide a callsite */
           return null;
       }
       return new TmfCallsite(sourceFile, sourceLine);
    }

    /**
     * Get the function/symbol name corresponding to binary file and offset.
     *
     * @param file
     *            The binary file to look at
     * @param buildId
     *            The expected buildId of the binary file (is not verified at
     *            the moment)
     * @param offset
     *            The memory offset in the file
     * @return The corresponding function/symbol name
     */
    public static @Nullable String getFunctionNameFromOffset(File file, @Nullable String buildId, long offset) {
        /*
         * TODO We are currently also using 'addr2line' to resolve function
         * names, which requires the binary's DWARF information to be available.
         * A better approach would be to use the binary's symbol table (if it is
         * not stripped), since this is usually more readily available than
         * DWARF.
         */
        Iterable<Addr2lineInfo> output = getAddr2lineInfo(file, buildId, offset);
        if (output == null || Iterables.isEmpty(output)) {
            return null;
        }
        Addr2lineInfo info = Iterables.getLast(output);
        return info.fFunctionName;
    }

    // ------------------------------------------------------------------------
    // Utility methods making use of 'addr2line'
    // ------------------------------------------------------------------------

    /**
     * Value used in addr2line output to represent unknown function names or
     * source files.
     */
    private static final String UNKNOWN_VALUE = "??"; //$NON-NLS-1$

    /**
     * Cache of all calls to 'addr2line', so that we can avoid recalling the
     * external process repeatedly.
     *
     * It is static, meaning one cache for the whole application, since the
     * symbols in a file on disk are independent from the trace referring to it.
     */
    private static final LoadingCache<FileOffset, @NonNull Iterable<Addr2lineInfo>> ADDR2LINE_INFO_CACHE;
    static {
        ADDR2LINE_INFO_CACHE = checkNotNull(CacheBuilder.newBuilder()
                .maximumSize(CACHE_SIZE)
                .build(new CacheLoader<FileOffset, @NonNull Iterable<Addr2lineInfo>>() {
                    @Override
                    public @NonNull Iterable<Addr2lineInfo> load(FileOffset fo) {
                        try (ScopeLog sl = new TraceCompassLogUtils.ScopeLog(LOGGER, Level.FINER, "FileOffsetMapper:CacheMiss",  //$NON-NLS-1$
                                "File", fo.fFilePath,  //$NON-NLS-1$
                                "Offset", fo.fOffset, //$NON-NLS-1$
                                "Build id", fo.fBuildId)) { //$NON-NLS-1$
                            return callAddr2line(fo);
                        }
                    }
                }));
    }

    private static class Addr2lineInfo {

        private final @Nullable String fSourceFileName;
        private final @Nullable Long fSourceLineNumber;
        private final @Nullable String fFunctionName;

        public Addr2lineInfo(@Nullable String sourceFileName,  @Nullable String functionName, @Nullable Long sourceLineNumber) {
            fSourceFileName = sourceFileName;
            fSourceLineNumber = sourceLineNumber;
            fFunctionName = functionName;
        }

        @Override
        public String toString() {
            return new ToStringBuilder(this)
                    .append("fSourceFileName", fSourceFileName) //$NON-NLS-1$
                    .append("fSourceLineNumber", fSourceLineNumber) //$NON-NLS-1$
                    .append("fFunctionName", fFunctionName) //$NON-NLS-1$
                    .toString();
        }
    }

    private static @Nullable Iterable<Addr2lineInfo> getAddr2lineInfo(File file, @Nullable String buildId, long offset) {
        try (ScopeLog sl = new TraceCompassLogUtils.ScopeLog(LOGGER, Level.FINER, "FileOffsetMapper:Addr2lineInfo", //$NON-NLS-1$
                "File", file,  //$NON-NLS-1$
                "Offset", offset, //$NON-NLS-1$
                "Build id", buildId)) { //$NON-NLS-1$

            /*
             * TODO We should also eventually verify that the passed buildId
             * matches the file we are attempting to open.
             */
            FileOffset fo = new FileOffset(checkNotNull(file.toString()), buildId, offset);

            @Nullable
            Iterable<Addr2lineInfo> callsites = ADDR2LINE_INFO_CACHE.getUnchecked(fo);
            sl.addData("callsites", callsites); //$NON-NLS-1$
            return callsites;
        }
    }

    private static Iterable<Addr2lineInfo> callAddr2line(FileOffset fo) {
        String filePath = fo.fFilePath;
        long offset = fo.fOffset;

        List<Addr2lineInfo> callsites = new LinkedList<>();

        // FIXME Could eventually use CDT's Addr2line class once it implements --inlines
        List<String> command = Arrays.asList(ADDR2LINE_EXECUTABLE, "-i", "-f", "-C", "-e", filePath, "0x" + Long.toHexString(offset)); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
        List<String> output = ProcessUtils.getOutputFromCommand(command);

        if (output == null) {
            /* Command returned an error */
            return Collections.emptySet();
        }

        /*
         * When passing the -f flag, the output alternates between function
         * names and file/line location.
         */
        boolean oddLine = false; // We flip at the start, first loop will be odd
        String currentFunctionName = null;
        for (String outputLine : output) {
            /* Flip the boolean for the following line */
            oddLine = !oddLine;

            // Remove discriminator part, for example: /build/buildd/glibc-2.21/elf/dl-object.c:78 (discriminator 8)
            outputLine = outputLine.replaceFirst(DISCRIMINATOR, "").trim(); //$NON-NLS-1$

            if (oddLine) {
                /* This is a line indicating the function name */
                if (outputLine.equals(UNKNOWN_VALUE)) {
                    currentFunctionName = null;
                } else {
                    currentFunctionName = outputLine;
                }
            } else {
                /* This is a line indicating a call site */
                String[] elems = outputLine.split(":"); //$NON-NLS-1$
                String fileName = elems[0];
                if (fileName.equals(UNKNOWN_VALUE)) {
                    fileName = null;
                }
                Long lineNumber;
                try {
                    lineNumber = Long.valueOf(elems[1]);
                } catch (NumberFormatException e) {
                    /* Probably a '?' output, meaning unknown line number. */
                    lineNumber = null;
                }
                callsites.add(new Addr2lineInfo(fileName, currentFunctionName, lineNumber));
            }
        }

        return callsites;
    }
}
