/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ddmuilib;

import com.android.ddmlib.Log;
import com.android.ddmlib.NativeLibraryMapInfo;
import com.android.ddmlib.NativeStackCallInfo;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;

/**
 * Represents an addr2line process to get filename/method information from a
 * memory address.<br>
 * Each process can only handle one library, which should be provided when
 * creating a new process.<br>
 * <br>
 * The processes take some time to load as they need to parse the library files.
 * For this reason, processes cannot be manually started. Instead the class
 * keeps an internal list of processes and one asks for a process for a specific
 * library, using <code>getProcess(String library)<code>.<br></br>
 * Internally, the processes are started in pipe mode to be able to query them
 * with multiple addresses.
 */
public class Addr2Line {
    private static final String ANDROID_SYMBOLS_ENVVAR = "ANDROID_SYMBOLS";

    private static final String LIBRARY_NOT_FOUND_MESSAGE_FORMAT =
            "Unable to locate library %s on disk. Addresses mapping to this library "
          + "will not be resolved. In order to fix this, set the the library search path "
          + "in the UI, or set the environment variable " + ANDROID_SYMBOLS_ENVVAR + ".";

    /**
     * Loaded processes list. This is also used as a locking object for any
     * methods dealing with starting/stopping/creating processes/querying for
     * method.
     */
    private static final HashMap<String, Addr2Line> sProcessCache =
            new HashMap<String, Addr2Line>();

    /**
     * byte array representing a carriage return. Used to push addresses in the
     * process pipes.
     */
    private static final byte[] sCrLf = {
        '\n'
    };

    /** Path to the library */
    private NativeLibraryMapInfo mLibrary;

    /** the command line process */
    private Process mProcess;

    /** buffer to read the result of the command line process from */
    private BufferedReader mResultReader;

    /**
     * output stream to provide new addresses to decode to the command line
     * process
     */
    private BufferedOutputStream mAddressWriter;

    private static final String DEFAULT_LIBRARY_SYMBOLS_FOLDER;
    static {
        String symbols = System.getenv(ANDROID_SYMBOLS_ENVVAR);
        if (symbols == null) {
            DEFAULT_LIBRARY_SYMBOLS_FOLDER = DdmUiPreferences.getSymbolDirectory();
        } else {
            DEFAULT_LIBRARY_SYMBOLS_FOLDER = symbols;
        }
    }

    private static List<String> mLibrarySearchPaths = new ArrayList<String>();

    /**
     * Set the search path where libraries should be found.
     * @param path search path to use, can be a colon separated list of paths if multiple folders
     * should be searched
     */
    public static void setSearchPath(String path) {
        mLibrarySearchPaths.clear();
        mLibrarySearchPaths.addAll(Arrays.asList(path.split(":")));
    }

    /**
     * Returns the instance of a Addr2Line process for the specified library.
     * <br>The library should be in a format that makes<br>
     * <code>$ANDROID_PRODUCT_OUT + "/symbols" + library</code> a valid file.
     *
     * @param library the library in which to look for addresses.
     * @return a new Addr2Line object representing a started process, ready to
     *         be queried for addresses. If any error happened when launching a
     *         new process, <code>null</code> will be returned.
     */
    public static Addr2Line getProcess(final NativeLibraryMapInfo library) {
        String libName = library.getLibraryName();

        // synchronize around the hashmap object
        if (libName != null) {
            synchronized (sProcessCache) {
                // look for an existing process
                Addr2Line process = sProcessCache.get(libName);

                // if we don't find one, we create it
                if (process == null) {
                    process = new Addr2Line(library);

                    // then we start it
                    boolean status = process.start();

                    if (status) {
                        // if starting the process worked, then we add it to the
                        // list.
                        sProcessCache.put(libName, process);
                    } else {
                        // otherwise we just drop the object, to return null
                        process = null;
                    }
                }
                // return the process
                return process;
            }
        }
        return null;
    }

    /**
     * Construct the object with a library name. The library should be present
     * in the search path as provided by ANDROID_SYMBOLS, ANDROID_OUT/symbols, or in the user
     * provided search path.
     *
     * @param library the library in which to look for address.
     */
    private Addr2Line(final NativeLibraryMapInfo library) {
        mLibrary = library;
    }

    /**
     * Search for the library in the library search path and obtain the full path to where it
     * is found.
     * @return fully resolved path to the library if found in search path, null otherwise
     */
    private String getLibraryPath(String library) {
        // first check the symbols folder
        String path = DEFAULT_LIBRARY_SYMBOLS_FOLDER + library;
        if (new File(path).exists()) {
            return path;
        }

        for (String p : mLibrarySearchPaths) {
            // try appending the full path on device
            String fullPath = p + "/" + library;
            if (new File(fullPath).exists()) {
                return fullPath;
            }

            // try appending basename(library)
            fullPath = p + "/" + new File(library).getName();
            if (new File(fullPath).exists()) {
                return fullPath;
            }
        }

        return null;
    }

    /**
     * Starts the command line process.
     *
     * @return true if the process was started, false if it failed to start, or
     *         if there was any other errors.
     */
    private boolean start() {
        // because this is only called from getProcess() we know we don't need
        // to synchronize this code.

        String addr2Line = System.getenv("ANDROID_ADDR2LINE");
        if (addr2Line == null) {
            addr2Line = DdmUiPreferences.getAddr2Line();
        }

        // build the command line
        String[] command = new String[5];
        command[0] = addr2Line;
        command[1] = "-C";
        command[2] = "-f";
        command[3] = "-e";

        String fullPath = getLibraryPath(mLibrary.getLibraryName());
        if (fullPath == null) {
            String msg = String.format(LIBRARY_NOT_FOUND_MESSAGE_FORMAT, mLibrary.getLibraryName());
            Log.e("ddm-Addr2Line", msg);
            return false;
        }

        command[4] = fullPath;

        try {
            // attempt to start the process
            mProcess = Runtime.getRuntime().exec(command);

            if (mProcess != null) {
                // get the result reader
                InputStreamReader is = new InputStreamReader(mProcess
                        .getInputStream());
                mResultReader = new BufferedReader(is);

                // get the outstream to write the addresses
                mAddressWriter = new BufferedOutputStream(mProcess
                        .getOutputStream());

                // check our streams are here
                if (mResultReader == null || mAddressWriter == null) {
                    // not here? stop the process and return false;
                    mProcess.destroy();
                    mProcess = null;
                    return false;
                }

                // return a success
                return true;
            }

        } catch (IOException e) {
            // log the error
            String msg = String.format(
                    "Error while trying to start %1$s process for library %2$s",
                    DdmUiPreferences.getAddr2Line(), mLibrary);
            Log.e("ddm-Addr2Line", msg);

            // drop the process just in case
            if (mProcess != null) {
                mProcess.destroy();
                mProcess = null;
            }
        }

        // we can be here either cause the allocation of mProcess failed, or we
        // caught an exception
        return false;
    }

    /**
     * Stops the command line process.
     */
    public void stop() {
        synchronized (sProcessCache) {
            if (mProcess != null) {
                // remove the process from the list
                sProcessCache.remove(mLibrary);

                // then stops the process
                mProcess.destroy();

                // set the reference to null.
                // this allows to make sure another thread calling getAddress()
                // will not query a stopped thread
                mProcess = null;
            }
        }
    }

    /**
     * Stops all current running processes.
     */
    public static void stopAll() {
        // because of concurrent access (and our use of HashMap.values()), we
        // can't rely on the synchronized inside stop(). We need to put one
        // around the whole loop.
        synchronized (sProcessCache) {
            // just a basic loop on all the values in the hashmap and call to
            // stop();
            Collection<Addr2Line> col = sProcessCache.values();
            for (Addr2Line a2l : col) {
                a2l.stop();
            }
        }
    }

    /**
     * Looks up an address and returns method name, source file name, and line
     * number.
     *
     * @param addr the address to look up
     * @return a BacktraceInfo object containing the method/filename/linenumber
     *         or null if the process we stopped before the query could be
     *         processed, or if an IO exception happened.
     */
    public NativeStackCallInfo getAddress(long addr) {
        long offset = addr - mLibrary.getStartAddress();

        // even though we don't access the hashmap object, we need to
        // synchronized on it to prevent
        // another thread from stopping the process we're going to query.
        synchronized (sProcessCache) {
            // check the process is still alive/allocated
            if (mProcess != null) {
                // prepare to the write the address to the output buffer.

                // first, conversion to a string containing the hex value.
                String tmp = Long.toString(offset, 16);

                try {
                    // write the address to the buffer
                    mAddressWriter.write(tmp.getBytes());

                    // add CR-LF
                    mAddressWriter.write(sCrLf);

                    // flush it all.
                    mAddressWriter.flush();

                    // read the result. We need to read 2 lines
                    String method = mResultReader.readLine();
                    String source = mResultReader.readLine();

                    // make the backtrace object and return it
                    if (method != null && source != null) {
                        return new NativeStackCallInfo(addr, mLibrary.getLibraryName(), method, source);
                    }
                } catch (IOException e) {
                    // log the error
                    Log.e("ddms",
                            "Error while trying to get information for addr: "
                                    + tmp + " in library: " + mLibrary);
                    // we'll return null later
                }
            }
        }
        return null;
    }
}
