/*******************************************************************************
 * 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
 *    Ingenico  - Vincent Guignot <vincent.guignot@ingenico.com> - Add binutils strings
 *******************************************************************************/
package org.eclipse.linuxtools.internal.gcov.parser;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.eclipse.cdt.core.IBinaryParser.IBinaryObject;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
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.jface.dialogs.MessageDialog;
import org.eclipse.linuxtools.binutils.utils.STStrings;
import org.eclipse.linuxtools.binutils.utils.STSymbolManager;
import org.eclipse.linuxtools.internal.gcov.Constants;
import org.eclipse.linuxtools.internal.gcov.model.CovFileTreeElement;
import org.eclipse.linuxtools.internal.gcov.model.CovFolderTreeElement;
import org.eclipse.linuxtools.internal.gcov.model.CovFunctionTreeElement;
import org.eclipse.linuxtools.internal.gcov.model.CovRootTreeElement;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PlatformUI;
import org.osgi.framework.FrameworkUtil;

/**
 * @author Xavier Raynaud <xavier.raynaud@st.com>
 */
public class CovManager implements Serializable {

    /**
     *
     */
    private static final long serialVersionUID = 5582066617970911413L;

    private static String winOSType = ""; //$NON-NLS-1$

    // input
    private final String binaryPath;
    // results
    private final ArrayList<Folder> allFolders = new ArrayList<>();
    private final ArrayList<SourceFile> allSrcs = new ArrayList<>();
    private final ArrayList<GcnoFunction> allFnctns = new ArrayList<>();
    private final HashMap<String, SourceFile> sourceMap = new HashMap<>();
    private long nbrPgmRuns = 0;
    // for view
    private CovRootTreeElement rootNode;
	//FIXME EK-LINUXTOOLS: private final IProject project;
	//FIXME EK-LINUXTOOLS: Need project for finding local STRINGS etc. Can't be final
    private IProject project;

    /**
     * Constructor
     * @param binaryPath
     * @param project
     *            the project that will be used to get the path to run commands
     */
    public CovManager(String binaryPath, IProject project) {
        this.binaryPath = binaryPath;
        this.project = project;
    }

    /**
     * Constructor
     * @param binaryPath
     */
    public CovManager(String binaryPath)
    {
        this(binaryPath, null);
    	//FIXME EK-LINUXTOOLS: Need project for finding local STRINGS etc.
		this.project = STSymbolManager.sharedInstance.getProjectFromFile(new Path(binaryPath));
    }

    /**
     * parse coverage files, execute resolve graph algorithm, process counts for functions, lines and folders.
     * @param List of coverage files paths
     * @throws CoreException, IOException, InterruptedException
     */

    public void processCovFiles(List<String> covFilesPaths, String initialGcda) throws CoreException, IOException {
        GcdaRecordsParser daRcrd = null;
        DataInput traceFile;

        Map<File, File> sourcePath = new HashMap<>();

        if (initialGcda != null) {
            File initialGcdaFile = new File(initialGcda).getAbsoluteFile();
            for (String s : covFilesPaths) {
                File gcda = new File(s).getAbsoluteFile();
                if (gcda.getName().equals(initialGcdaFile.getName()) && !gcda.equals(initialGcdaFile)) {
                    if (!sourcePath.isEmpty()) {
                        // hum... another file has the same name...
                        // sorry, we have to clean sourcePath
                        sourcePath.clear();
                        break;
                    } else {
                        addSourceLookup(sourcePath, initialGcdaFile, gcda);
                    }
                }
            }
        }

        for (String gcdaPath : covFilesPaths) {
            String gcnoPath = gcdaPath.replace(".gcda", ".gcno"); //$NON-NLS-1$ //$NON-NLS-2$
            // parse GCNO file
            traceFile = openTraceFileStream(gcnoPath, ".gcno", sourcePath); //$NON-NLS-1$
            if (traceFile == null) {
                return;
            }
            GcnoRecordsParser noRcrd = new GcnoRecordsParser(sourceMap, allSrcs);
            noRcrd.parseData(traceFile);

            // add new functions parsed to AllSrcs array
            for (GcnoFunction f : noRcrd.getFnctns()) {
                allFnctns.add(f);
            }

            // close the input stream
            if (traceFile.getClass() == DataInputStream.class) {
                ((DataInputStream) traceFile).close();
            }

            // parse GCDA file
            traceFile = openTraceFileStream(gcdaPath, ".gcda", sourcePath); //$NON-NLS-1$
            if (traceFile == null) {
                return;
            }
            if (noRcrd.getFnctns().isEmpty()) {
                String message = NLS.bind(Messages.CovManager_No_Funcs_Error, gcnoPath);
				Status status = new Status(IStatus.ERROR, Constants.PLUGIN_ID, message);
                throw new CoreException(status);
            }
            daRcrd = new GcdaRecordsParser(noRcrd.getFnctns());
            daRcrd.parseGcdaRecord(traceFile);

            // close the input stream
            if (traceFile.getClass() == DataInputStream.class) {
                ((DataInputStream) traceFile).close();
            }
        }

        // to fill the view title
        if (daRcrd != null) {
            nbrPgmRuns = daRcrd.getPgmSmryNbrPgmRuns();
        }

        /* process counts from data parsed */

        // solve graph for each function
        for (GcnoFunction gf : allFnctns) {
            gf.solveGraphFnctn();
        }

        // allocate lines
        for (SourceFile sourceFile : allSrcs) {
            sourceFile.createLines();
        }

        // add line counts
        for (GcnoFunction gf : allFnctns) {
            gf.addLineCounts(allSrcs);
        }

        // accumulate lines
        for (SourceFile sf : allSrcs) {
            sf.accumulateLineCounts();
        }

        /* compute counts by folder */

        // make the folders list
        for (SourceFile sf : allSrcs) {
            File srcFile = new File(sf.getName());
            String folderName = srcFile.getParent();
            if (folderName == null) {
                folderName = "?"; //$NON-NLS-1$
            }
            Folder folder = null;
            for (Folder f : allFolders) {
                if (f.getPath().equals(folderName)) {
                    folder = f;
                }
            }
            if (folder == null) {
                folder = new Folder(folderName);
                allFolders.add(folder);
            }
            folder.addSrcFiles(sf);
        }

        // assign sourcesList for each folder
        for (Folder f : allFolders) {
            f.accumulateSourcesCounts();
        }
    }

    /**
     * fill the model by count results
     * @throws CoreException, IOException, InterruptedException
     */

    public void fillGcovView() {
        // process counts for summary level
        int summaryTotal = 0, summaryInstrumented = 0, summaryExecuted = 0;
        for (Folder f : allFolders) {
            summaryTotal += f.getNumLines();
            summaryInstrumented += f.getLinesInstrumented();
            summaryExecuted += f.getLinesExecuted();
        }

        // fill rootNode model: the entry of the contentProvider
        rootNode = new CovRootTreeElement(Messages.CovManager_Summary, summaryTotal, summaryExecuted,
                summaryInstrumented);
        IBinaryObject binaryObject = STSymbolManager.sharedInstance.getBinaryObject(new Path(binaryPath));

        for (Folder fldr : allFolders) {
            String folderLocation = fldr.getPath();
            CovFolderTreeElement fldrTreeElem = new CovFolderTreeElement(rootNode, folderLocation, fldr.getNumLines(),
                    fldr.getLinesExecuted(), fldr.getLinesInstrumented());
            rootNode.addChild(fldrTreeElem);

            for (SourceFile src : fldr.getSrcFiles()) {
                CovFileTreeElement srcTreeElem = new CovFileTreeElement(fldrTreeElem, src.getName(), src.getNumLines(),
                        src.getLinesExecuted(), src.getLinesInstrumented());
                fldrTreeElem.addChild(srcTreeElem);

                for (GcnoFunction fnctn : src.getFnctns()) {
                    String name = fnctn.getName();
                    name = STSymbolManager.sharedInstance.demangle(binaryObject, name, project);
                    srcTreeElem.addChild(new CovFunctionTreeElement(srcTreeElem, name, fnctn.getSrcFile(), fnctn
                            .getFirstLineNmbr(), fnctn.getCvrge().getLinesExecuted(), fnctn.getCvrge()
                            .getLinesInstrumented()));
                }
            }
        }
    }

    // Get the Windows OS Type.  We might have to change a path over to Windows format
    // and this is different on Cygwin vs MingW.
    private String getWinOSType() {
        if (winOSType.equals("")) { //$NON-NLS-1$
            try {
                Process process = Runtime.getRuntime().exec(new String[] {"sh", "-c", "echo $OSTYPE"}); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

                String firstLine = null;
                try (BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                    firstLine = stdout.readLine();
                }
                if (firstLine != null) {
                    winOSType = firstLine.trim();
                }
            } catch (IOException e) {
                // ignore
            }
        }
        return winOSType;
    }

    // Get the OS path string.  For Cygwin, translate We add a Win check to handle MingW.
    // For MingW, we would rather represent C:\a\b as /C/a/b which
    // doesn't cause Makefile to choke. For Cygwin we use /cygdrive/C/a/b
    private String getTransformedPathString(IPath path) {
        String s = path.toOSString();
        if (Platform.getOS().equals(Platform.OS_WIN32)) {
            if (getWinOSType().equals("cygwin")) { //$NON-NLS-1$
                s = s.replaceAll("^\\\\cygdrive\\\\([a-zA-Z])", "$1:"); //$NON-NLS-1$ //$NON-NLS-2$
            } else {
                s = s.replaceAll("^\\\\([a-zA-Z])", "$1:"); //$NON-NLS-1$ //$NON-NLS-2$
            }
        }
        return s;
    }

	private class StringHolder {
		private String s;

		public String getString() {
			return s;
		}

		public void setString(String s) {
			this.s = s;
		}
	}

    private DataInput openTraceFileStream(String filePath, String extension, Map<File, File> sourcePath)
            throws FileNotFoundException {
        Path p = new Path(filePath);
        // get the file path transformed to work on local OS (e.g. Windows)
        filePath = getTransformedPathString(p);
        File f = new File(filePath).getAbsoluteFile();
        String filename = f.getName();
        if (f.isFile() && f.canRead()) {
            FileInputStream fis = new FileInputStream(f);
            InputStream inputStream = new BufferedInputStream(fis);
            return new DataInputStream(inputStream);
        } else {
            String postfix = ""; //$NON-NLS-1$
            File dir = null;
            do {
                if (postfix.isEmpty()) {
                    postfix = f.getName();
                } else {
                    postfix = f.getName() + File.separator + postfix;
                }
                f = f.getParentFile();
                if (f != null) {
                    dir = sourcePath.get(f);
                } else {
                    break;
                }
            } while (dir == null);

            if (dir != null) {
                f = new File(dir, postfix);
                if (f.isFile() && f.canRead()) {
                    return openTraceFileStream(f.getAbsolutePath(), extension, sourcePath);
                }
            }

			// We need to ask user for location. Open up a dialog.
			final StringHolder holder = new StringHolder();
			final String filePathToUse = filePath;
			Display.getDefault().syncExec(() -> {
				Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell();
				FileDialog fg = new FileDialog(shell, SWT.OPEN);
				fg.setFilterExtensions(new String[] { "*" + extension, "*.*", "*" }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
				fg.setFileName(filename);
				fg.setText(NLS.bind(Messages.CovManager_No_FilePath_Error, new Object[] { filePathToUse, filename }));
				String s = fg.open();
				holder.setString(s);
			});
			// transform String path to stream
			String s = holder.getString();
            if (s == null) {
				return null;
            } else {
				f = new File(s).getAbsoluteFile();
				addSourceLookup(sourcePath, f, new File(filePath).getAbsoluteFile());
				if (f.isFile() && f.canRead()) {
					FileInputStream fis = new FileInputStream(f);
					InputStream inputStream = new BufferedInputStream(fis);
					return new DataInputStream(inputStream);
				}
            }
        }
        return null;
    }

    public ArrayList<SourceFile> getAllSrcs() {
        return allSrcs;
    }

    public ArrayList<GcnoFunction> getAllFnctns() {
        return allFnctns;
    }

    public CovRootTreeElement getRootNode() {
        return rootNode;
    }

    public String getBinaryPath() {
        return binaryPath;
    }

    public SourceFile getSourceFile(String sourcePath) {
        return sourceMap.get(sourcePath);
    }

    public long getNbrPgmRuns() {
        return nbrPgmRuns;
    }

    /**
     * Retrieve a list containing gcda paths from a binary file
     * @return
     * @throws InterruptedException
     */
    public List<String> getGCDALocations() throws InterruptedException {
        IBinaryObject binaryObject = STSymbolManager.sharedInstance.getBinaryObject(new Path(binaryPath));
        String binaryPath = binaryObject.getPath().toOSString();
        STStrings strings = STSymbolManager.sharedInstance.getStrings(binaryObject, project);
        List<String> l = new LinkedList<>();
        Process p = getStringsProcess(strings.getName(), strings.getArgs(), binaryPath);
        if (p == null) {
			if (Platform.getOS().equals(Platform.OS_WIN32)) {
				Display.getDefault().syncExec(() -> {
					MessageDialog.openError(new Shell(), Messages.CovManager_Retrieval_Error_title,
							Messages.CovManager_No_Strings_Windows_Error);
				});
			} else {
				Status status = new Status(IStatus.ERROR, Constants.PLUGIN_ID, IStatus.ERROR,
						Messages.CovManager_Retrieval_Error, new IOException());
				Platform.getLog(FrameworkUtil.getBundle(CovManager.class)).log(status);
			}
            return l;
        }
        ThreadConsumer t = new ThreadConsumer(p, l);
        t.start();
        p.waitFor();
        t.join();
        return l;
    }

    private Process getStringsProcess(String stringsTool, String[] stringsArgs, String binaryPath) {
        String[] runtimeStr = new String[stringsArgs.length + 2];
        System.arraycopy(stringsArgs, 0, runtimeStr, 1, stringsArgs.length);
        runtimeStr[0] = stringsTool;
        runtimeStr[runtimeStr.length-1] = binaryPath;
        try {
            return Runtime.getRuntime().exec(runtimeStr);
        } catch (IOException e) {
            return null;
        }
    }

    private static final class ThreadConsumer extends Thread {
        private final Process p;
        private final List<String> list;

        ThreadConsumer(Process p, List<String> files) {
            super();
            this.p = p;
            this.list = files;
        }

        @Override
        public void run() {
            try {
                populateGCDAFiles(p.getInputStream());
            } catch (IOException e) {
            }
        }

        private void populateGCDAFiles(InputStream s) throws IOException {
            InputStreamReader isr = new InputStreamReader(s);
            LineNumberReader lnr = new LineNumberReader(isr);
            String line = null;
            while ((line = lnr.readLine()) != null) {
                if (line.endsWith(".gcda")) //$NON-NLS-1$
                {
                    // absolute .gcda filepaths retrieved using the "strings" tool may
                    // be prefixed by random printable characters so strip leading
                    // characters until the filepath starts with "X:/", "X:\", "/"  or "\"
                    // FIXME: need a more robust mechanism to locate .gcda files [Bugzilla 329710]
                    while ((line.length() > 6) && !line.matches("^([A-Za-z]:)?[/\\\\].*")) { //$NON-NLS-1$
                        line = line.substring(1);
                    }
                    IPath p = new Path(line);
                    String filename = p.toString();

                    if (!list.contains(filename)) {
                        list.add(filename);
                    }
                }
            }
        }
    }

    /**
     * @return the sourceMap
     */
    public HashMap<String, SourceFile> getSourceMap() {
        return sourceMap;
    }

    private void addSourceLookup(Map<File, File> map, File hostPath, File compilerPath) {
        while (hostPath.getName().equals(compilerPath.getName())) {
            hostPath = hostPath.getParentFile();
            compilerPath = compilerPath.getParentFile();
        }
        map.put(compilerPath, hostPath);
    }

}
