/*******************************************************************************
 * Copyright (c) 2009, 2018 STMicroelectronics and others.
 * 
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Xavier Raynaud <xavier.raynaud@st.com> - initial API and implementation
 *    Red Hat Inc. - ongoing maintenance
 *    Ingenico - Vincent Guignot <vincent.guignot@ingenico.com> - Add binutils strings
 *******************************************************************************/
package org.eclipse.linuxtools.binutils.utils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.IAddress;
import org.eclipse.cdt.core.IBinaryParser;
import org.eclipse.cdt.core.IBinaryParser.IBinaryFile;
import org.eclipse.cdt.core.IBinaryParser.IBinaryObject;
import org.eclipse.cdt.core.IBinaryParser.ISymbol;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.CoreModelUtil;
import org.eclipse.cdt.core.model.IBinary;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
import org.eclipse.cdt.utils.Addr2line;
import org.eclipse.cdt.utils.CPPFilt;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.linuxtools.internal.Activator;

/**
 * This class Is a utility on top of c++filt and addr2line. It allows an easy conversion between address and source
 * location, and between mangled and demangled symbols.
 */
public class STSymbolManager {

    /**
     * Auto dispose timeout: If some tools has been unused since more that this time (in ms), they are disposed.
     */
    private final static long AUTO_DISPOSE_TIMEOUT = 30000;

    /**
     * Singleton instance
     */
    public final static STSymbolManager sharedInstance = new STSymbolManager();

    private final static class AutoDisposeAddr2line {
        private Addr2line addr2line;
        private long startTime;
    }

    private final static class AutoDisposeCPPFilt {
        private CPPFilt cppfilt;
        private long startTime;
    }

    /** Map of all living instance of addr2line */
    private final HashMap<IBinaryObject, AutoDisposeAddr2line> addr2lines = new HashMap<>();
    /** Map of all living instance of cppfilt */
    private final HashMap<String, AutoDisposeCPPFilt> cppfilts = new HashMap<>();

    /**
     * Constructor
     */
    private STSymbolManager() {
        Runnable worker = () -> {
		    try {
		        do {
		            try {
		                Thread.sleep(AUTO_DISPOSE_TIMEOUT);
		            } catch (InterruptedException e1) {
		                break;
		            }
		            cleanup();
		        } while (true);
		    } catch (Exception e2) {
		        Status s = new Status(IStatus.ERROR, Activator.PLUGIN_ID, e2.getMessage(), e2);
		        Activator.getDefault().getLog().log(s);
		    }
		};
        new Thread(worker, "ST System Analysis Symbol Manager").start(); //$NON-NLS-1$
        // TODO: perhaps this thread has to be lazy-created ?
        // and perhaps this thread has to destroy itself when it is no longer
        // used ?
    }

    /**
     * @since 4.1
     */
    public synchronized void reset() {
        Iterator<Entry<IBinaryObject, AutoDisposeAddr2line>> iter = addr2lines.entrySet().iterator();
        while (iter.hasNext()) {
            Entry<IBinaryObject, AutoDisposeAddr2line> entry = iter.next();
            AutoDisposeAddr2line ada2l = entry.getValue();
            ada2l.addr2line.dispose();
            ada2l.addr2line = null;
            iter.remove();
        }

        Iterator<Entry<String, AutoDisposeCPPFilt>> iter2 = cppfilts.entrySet().iterator();
        while (iter2.hasNext()) {
            Entry<String, AutoDisposeCPPFilt> entry = iter2.next();
            AutoDisposeCPPFilt adcppf = entry.getValue();
            adcppf.cppfilt.dispose();
            adcppf.cppfilt = null;
        }
    }

    /**
     * each {@link #AUTO_DISPOSE_TIMEOUT} ms, the unused addr2line and c++filt programs are disposed.
     */
    private synchronized void cleanup() {
        long currentTime = System.currentTimeMillis();
        Iterator<Entry<IBinaryObject, AutoDisposeAddr2line>> iter = addr2lines.entrySet().iterator();
        while (iter.hasNext()) {
            Entry<IBinaryObject, AutoDisposeAddr2line> entry = iter.next();
            AutoDisposeAddr2line ada2l = entry.getValue();
            long diff = currentTime - ada2l.startTime;
            if (diff > AUTO_DISPOSE_TIMEOUT) {
                if (ada2l.addr2line != null) {
                    ada2l.addr2line.dispose();
                    ada2l.addr2line = null;
                }
                iter.remove();
            }
        }

        Iterator<Entry<String, AutoDisposeCPPFilt>> iter2 = cppfilts.entrySet().iterator();
        while (iter2.hasNext()) {
            Entry<String, AutoDisposeCPPFilt> entry = iter2.next();
            AutoDisposeCPPFilt adcppf = entry.getValue();
            long diff = currentTime - adcppf.startTime;
            if (diff > AUTO_DISPOSE_TIMEOUT) {
                if (adcppf.cppfilt != null) {
                    adcppf.cppfilt.dispose();
                    adcppf.cppfilt = null;
                }
                iter2.remove();
            }
        }
    }

    /**
     * Demangle the given symbol
     * @param symbol
     * @param project The project to be
     * @return The demangled symbol.
     */
    public synchronized String demangle(ISymbol symbol, IProject project) {
        String cpu = symbol.getBinaryObject().getCPU();
        String symbolName = symbol.getName();
        return demangleImpl(symbolName, cpu, project);
    }

    /**
     * Demangle the given symbol
     * @param program
     * @param symbolName
     * @param project
     * @return The demangled symbol.
     */
    public synchronized String demangle(IBinaryObject program, String symbolName, IProject project) {
        String cpu = program.getCPU();
        return demangleImpl(symbolName, cpu, project);
    }

    /**
     * Demangle the given symbol
     * @param symbolName
     * @param cpu
     * @param project
     * @param symbol
     * @return
     */
    private synchronized String demangleImpl(String symbolName, String cpu, IProject project) {
        CPPFilt cppfilt = getCppFilt(cpu, project);
        if (cppfilt != null && (symbolName.startsWith("_Z") || symbolName.startsWith("_G"))) { //$NON-NLS-1$ //$NON-NLS-2$
            try {
                symbolName = cppfilt.getFunction(symbolName);
            } catch (IOException e) {
                // TODO: log the error ?
            }
        }
        return symbolName;
    }

    /**
     * @param program
     * @param address
     * @param project
     * @return the line number of the given address
     */
    public synchronized int getLineNumber(IBinaryObject program, IAddress address, IProject project) {
        Addr2line addr2line = getAddr2line(program, project);
        if (addr2line == null) {
            return -1;
        }
        try {
            return addr2line.getLineNumber(address);
        } catch (IOException e) {
            // TODO: log the error ?;
            // Perhaps log the error only once, because
            // this method is called many many times...
            return -1;
        }
    }

    /**
     * @param symbol
     * @param project
     * @return the line number of the given symbol
     */
    public int getLineNumber(ISymbol symbol, IProject project) {
        IBinaryObject obj = symbol.getBinaryObject();
        IAddress address = symbol.getAddress();
        return getLineNumber(obj, address, project);
    }

    /**
     * @param program
     * @param address
     * @param project
     * @return the file name of the given address
     */
    public synchronized String getFileName(IBinaryObject program, IAddress address, IProject project) {
        Addr2line addr2line = getAddr2line(program, project);
        if (addr2line == null) {
            return null;
        }
        try {
            return addr2line.getFileName(address);
        } catch (IOException e) {
            // TODO: log the error ?;
            // Perhaps log the error only once, because
            // this method is called many many times...
            return null;
        }
    }

    /**
     * @param symbol
     * @param project
     * @return the filename of the given symbol
     */
    public String getFilename(ISymbol symbol, IProject project) {
        IBinaryObject obj = symbol.getBinaryObject();
        IAddress address = symbol.getAddress();
        return getFileName(obj, address, project);
    }

    /**
     * Gets the c++filt support for the given program Note that the instance if kept in a local hashmap, and discarded
     * after 30 seconds of inactivity.
     * @param cpu
     * @param project
     * @param program
     * @return an instance of CPPFilt suitable for the given program
     */
    private synchronized CPPFilt getCppFilt(String cpu, IProject project) {
        AutoDisposeCPPFilt adCppfilt = cppfilts.get(cpu);
        if (adCppfilt == null) {
            adCppfilt = new AutoDisposeCPPFilt();
            cppfilts.put(cpu, adCppfilt);
        }
        if (adCppfilt.cppfilt == null) {
            try {
                adCppfilt.cppfilt = STBinutilsFactoryManager.getCPPFilt(cpu, project);
            } catch (IOException e) {
                // TODO: log the error ?;
                // Perhaps log the error only once, because
                // this method is called many many times...
                return null;
            }
        }
        adCppfilt.startTime = System.currentTimeMillis();
        return adCppfilt.cppfilt;
    }

    /**
     * Gets the addr2line support for the given program Note that the instance if kept in a local hashmap, and discarded
     * after 30 seconds of inactivity.
     * @param program
     * @param project
     * @return an instance of Addr2line suitable for the given program
     */
    private synchronized Addr2line getAddr2line(IBinaryObject program, IProject project) {
        AutoDisposeAddr2line adAddr2line = addr2lines.get(program);
        if (adAddr2line == null) {
            adAddr2line = new AutoDisposeAddr2line();
            addr2lines.put(program, adAddr2line);
        }
        if (adAddr2line.addr2line == null) {
            try {
                adAddr2line.addr2line = STBinutilsFactoryManager.getAddr2line(program.getCPU(), program.getPath()
                        .toOSString(), project);
            } catch (IOException e) {
                // TODO: log the error ?;
                // Perhaps log the error only once, because
                // this method is called many many times...
                return null;
            }
        }
        adAddr2line.startTime = System.currentTimeMillis();
        return adAddr2line.addr2line;
    }

    /**
     * Gets the strings support for the given program.
     * @param program
     * @param project
     * @return an instance of Strings suitable for the given program
     * @since 6.0
     */
    public synchronized STStrings getStrings(IBinaryObject program, IProject project) {
        STStrings strings = null;
        try {
            strings = STBinutilsFactoryManager.getStrings(program.getCPU(), project);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
        return strings;
    }

	/**
     * Gets the IBinaryObject corresponding to the given path (absolute path in filesystem). If a IBinaryObject
     * corresponding to the given path has been already built by eclipse, return it. Otherwise build a new
     * IBinaryObject, according to project preferences. Note that it may return null if the path is invalid, or is not a
     * valid binary file.
     * @param loc
     * @return a IBinaryObject
     */
    public IBinaryObject getBinaryObject(String loc) {
        IPath path = new Path(loc);
        return getBinaryObject(path);
    }

    /**
     * Gets the IBinaryObject corresponding to the given path (absolute path in filesystem). If a IBinaryObject
     * corresponding to the given path has been already built by eclipse, return it. Otherwise build a new
     * IBinaryObject, according to project preferences. Note that it may return null if the path is invalid, or is not a
     * valid binary file.
     * @param path
     * @return a IBinaryObject
     */
    public IBinaryObject getBinaryObject(IPath path) {

        return getBinaryObject(path, null);
    }

    /**
     * Gets the IBinaryObject corresponding to the given path (absolute path in filesystem). If a IBinaryObject
     * corresponding to the given path has been already built by eclipse, return it. Otherwise build a new
     * IBinaryObject, according to project preferences. Note that it may return null if the path is invalid, or is not a
     * valid binary file.
     * @param path
     * @param defaultparser
     * @return a IBinaryObject
     */
    private IBinaryObject getBinaryObject(IPath path, IBinaryParser defaultparser) {
        //FIXME EK-LINUXTOOLS: IFile c = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(path);
        IFile c = findFileFromPath(path);
        List<IBinaryParser> parsers;
        if (c != null) {
            IBinaryObject object = getAlreadyExistingBinaryObject(c);
            if (object != null) {
                return object;
            }
            parsers = getBinaryParser(c.getProject());
        } else {
            parsers = new LinkedList<>();
        }

        if (defaultparser == null) {
            try {
                defaultparser = CCorePlugin.getDefault().getDefaultBinaryParser();
            } catch (CoreException e) {
                Activator.getDefault().getLog().log(e.getStatus());
            }
        }
        if (defaultparser != null) {
            parsers.add(defaultparser);
        }
        IBinaryObject ret = buildBinaryObject(path, parsers);
        if (ret == null) { // trying all BinaryParsers...
            parsers.clear();
            IExtensionPoint extensionPoint = Platform.getExtensionRegistry().getExtensionPoint(CCorePlugin.PLUGIN_ID,
                    CCorePlugin.BINARY_PARSER_SIMPLE_ID);
            for (IExtension extension : extensionPoint.getExtensions()) {
                if (extension != null) {
                    IConfigurationElement element[] = extension.getConfigurationElements();
                    for (IConfigurationElement element2 : element) {
                        if (element2.getName().equalsIgnoreCase("cextension")) { //$NON-NLS-1$
                            IBinaryParser parser;
                            try {
                                parser = (IBinaryParser) element2.createExecutableExtension("run"); //$NON-NLS-1$
                                parsers.add(parser);
                            } catch (CoreException e) {
                            }
                        }
                    }
                }
            }
            ret = buildBinaryObject(path, parsers);
        }
        return ret;
    }

    //FIXME EK-LINUXTOOLS: Search workspace for one binary
    /**
     * Searches workspace and tries to find one-and-only binary which could have
     * created GCOV files.
     * 	1. Look in the project where .gcda or .gcna file are registered
     * 	2. Search workspace and list all binaries
     * 	3. If there is only one binary found then return it
     *
     * @param file
     * @return Binary path or null
     * @since 6.0
     */
    public IResource findOneAndOnlyBinary(IPath file)
    {
        IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
        IResource project;
        IResource binary = null;

        project = STSymbolManager.sharedInstance.getProjectFromFile(file);
        if (project != null && project.exists())
        {
            ICProject cproject = (ICProject)CoreModel.getDefault().create(project);
            if (cproject != null)
            {
                try
                {
                    IBinary[] bs = cproject.getBinaryContainer().getBinaries();
                    if (bs.length == 1)
                    {
                        binary = bs[0].getResource();
                    }
                }
                catch (CModelException e)
                {
                    String message = 
                       "Error getting binaries for [" + cproject.toString() + "]"; //$NON-NLS-1$ //$NON-NLS-2$
                    CCorePlugin.log(message, e);
                }
            }
        }

        if (binary == null)
        {
            /*
             * Apparently we did not find one binary in the project
             * where GCOV files are located.
             * Search workspace in hopes that there will be only one binary there
             */
            ArrayList<IResource> binaries = new ArrayList<>();
            IProject projects[] = root.getProjects();
            for (IProject prj : projects)
            {
                ICProject cproject = CoreModel.getDefault().create(prj);
                if (cproject != null)
                {
                    try
                    {
                        IBinary[] bs = cproject.getBinaryContainer().getBinaries();
                        if (bs.length == 1)
                        {
                            /*
                             * Validate binary. Not tested
                             */
                            IBinaryObject binObj =
                                    STSymbolManager.sharedInstance.getBinaryObject(bs[0].getPath());
                            if (binObj != null)
                            {
                                binaries.add(bs[0].getResource());
                            }
                        }
                    }
                    catch (CModelException e)
                    {
                        String message = 
                           "Error getting binaries for [" + cproject.toString() + "]"; //$NON-NLS-1$ //$NON-NLS-2$
                        CCorePlugin.log(message, e);
                    }
                }

            }

            if (binaries.size() == 1)
            {
                binary = binaries.get(0);
            }
        }

        return (binary);
    }

    //FIXME EK-LINUXTOOLS: New method to get location for linked binary resource
    /**
	 * @param file 
     * @return 
     * @since 6.0
	 */
    public IFile findFileFromPath(IPath file)
    {
    	// can't use a null or empty path below
    	if (file == null || file.isEmpty()) {
    		return null;
    	}
    	
        IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
        /*
         * Find resource to which this GCOV file maps to in workspace
         */
        IFile files[] = root.findFilesForLocationURI(URIUtil.toURI(file));
        if ((files.length == 0) || (files.length > 1))
        {
            /*
             * Nothing found or more than one GCOV output file
             * Let user decide what to use as binary
             */
            return (null);
        }
        return files[0];   	
    }
    
    //FIXME EK-LINUXTOOLS: IProject file resolution for linked resources
    /**
	 * @param file 
     * @return 
     * @since 6.0
	 */
    public IProject getProjectFromFile(IPath file)
    {
        IFile ifile = findFileFromPath(file);
        if (ifile == null)
        {
            /*
             * Nothing found or more than one GCOV output file
             * Let upstream processing handle this exception
             */
            return (null);
        }
        /*
         * Find project this resource belongs to
         */
        return (ifile.getProject());   	
    }

    /**
     * Validate the binary file. In particular, verify that this binary file can be decoded.
     * @param o
     * @return the binary object, or null.
     */
    private IBinaryObject validateBinary(IBinaryFile o) {
        if (o instanceof IBinaryObject) {
            IBinaryObject object = (IBinaryObject) o;
            String s = object.getCPU(); //
            if (s != null && !s.isEmpty()) {
                return object;
            }
        }
        return null;
    }

    private IBinaryObject buildBinaryObject(IPath path, List<IBinaryParser> parsers) {
        for (IBinaryParser iBinaryParser : parsers) {
            IBinaryObject o = buildBinaryObject(path, iBinaryParser);
            if (o != null) {
                return o;
            }
        }
        return null;
    }

    /**
     * Build a binary object with the given file and parser. Also verify that the builded binary object is valid (@see
     * #validateBinary)
     * @param path The path to the binary object.
     * @param parser Parser to use to parse the object.
     * @return The newly parsed object.
     */
    private IBinaryObject buildBinaryObject(IPath path, IBinaryParser parser) {
        if (parser == null) {
            return null;
        }
        IBinaryFile bf = null;
        try {
            bf = parser.getBinary(path);
        } catch (IOException e) {
            // do nothing ?
        }
        return validateBinary(bf);
    }

    /**
     * Ask the workbench to find if a binary object already exist for the given file
     * @param c The file to look binary object for.
     * @return The binary object if found, null otherwise.
     */
    private IBinaryObject getAlreadyExistingBinaryObject(IFile c) {
        IProject project = c.getProject();
        if (project != null && project.exists()) {
            ICProject cproject = CoreModel.getDefault().create(project);
            if (cproject != null) {
                try {
                    IBinary[] b = cproject.getBinaryContainer().getBinaries();
                    for (IBinary binary : b) {
                        IResource r = binary.getResource();
                        if (r.equals(c)) {
                            IBinaryObject binaryObject = binary.getAdapter(IBinaryObject.class);
                            return validateBinary(binaryObject);
                        }
                    }
                } catch (CModelException e) {
                }
            }
        }
        return null;
    }

    /**
     * Retrieve the list of binary parsers defined for the given project.
     * @param project The project.
     * @return The binary parsers for this project.
     */
    private List<IBinaryParser> getBinaryParser(IProject project) {
        List<IBinaryParser> parsers = new LinkedList<>();

        ICProjectDescription projDesc = CCorePlugin.getDefault().getProjectDescription(project);
        if (projDesc == null) {
            return parsers;
        }
        ICConfigurationDescription[] cfgs = projDesc.getConfigurations();
        String[] binaryParserIds = CoreModelUtil.getBinaryParserIds(cfgs);

        IExtensionPoint extensionPoint = Platform.getExtensionRegistry().getExtensionPoint(CCorePlugin.PLUGIN_ID,
                CCorePlugin.BINARY_PARSER_SIMPLE_ID);
        for (String id : binaryParserIds) {
            IExtension extension = extensionPoint.getExtension(id);
            if (extension != null) {
                IConfigurationElement element[] = extension.getConfigurationElements();
                for (IConfigurationElement element2 : element) {
                    if (element2.getName().equalsIgnoreCase("cextension")) { //$NON-NLS-1$
                        try {
                            IBinaryParser parser = (IBinaryParser) element2.createExecutableExtension("run"); //$NON-NLS-1$
                            if (parser != null) {
                                parsers.add(parser);
                            }
                        } catch (CoreException e) {
                            // TODO: handle exception ?
                        }
                    }
                }
            }
        }
        return parsers;
    }

}
