/*******************************************************************************
 * Copyright (c) 2008, 2018 Red Hat, Inc.
 * 
 * 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:
 *    Elliott Baron <ebaron@redhat.com> - initial API and implementation
 *******************************************************************************/
package org.eclipse.linuxtools.profiling.ui;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.IFunction;
import org.eclipse.cdt.core.index.IIndex;
import org.eclipse.cdt.core.index.IIndexFile;
import org.eclipse.cdt.core.index.IIndexManager;
import org.eclipse.cdt.core.index.IIndexName;
import org.eclipse.cdt.core.index.IndexFilter;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ICElementVisitor;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.debug.core.CDebugCorePlugin;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.debug.core.sourcelookup.containers.LocalFileStorage;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.debug.ui.IDebugModelPresentation;
import org.eclipse.debug.ui.sourcelookup.ISourceLookupResult;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.linuxtools.internal.profiling.ui.ProfileUIPlugin;
import org.eclipse.linuxtools.profiling.launch.IRemoteFileProxy;
import org.eclipse.linuxtools.profiling.launch.RemoteProxyManager;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;

public class ProfileUIUtils {

    /**
     * Opens the specified file in an editor (or selects an already open
     * editor) and highlights the specified line.
     * @param path - absolute path of file to open
     * @param line - line number to select, 0 to not select a line
     * @throws PartInitException - Failed to open editor
     * @throws BadLocationException - Line number not valid in file
     */
    public static void openEditorAndSelect(String path, int line) throws PartInitException, BadLocationException {
        Path p = new Path(path);

        if (p.toFile().exists()) {
            IWorkbenchPage activePage = ProfileUIPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow().getActivePage();
            IFileStore file = EFS.getLocalFileSystem().getStore(p);

            IEditorPart editor = IDE.openEditorOnFileStore(activePage, file);
            if (editor instanceof ITextEditor) {
                ITextEditor textEditor = (ITextEditor) editor;

                if (line > 0) {
                    IDocumentProvider provider = textEditor.getDocumentProvider();
                    IDocument document = provider.getDocument(textEditor.getEditorInput());

                    int start = document.getLineOffset(line - 1); //zero-indexed
                    textEditor.selectAndReveal(start, 0);
                }
            }
        }
    }

    /**
     * Open a file in the Editor at the specified offset, highlighting the given length
     *
     * @param path : Absolute path pointing to the file which will be opened.
     * @param line - line number to select, 0 to not select a line
     * @param project - current project object
     * @throws BadLocationException - Line number not valid in file
     * @throws PartInitException if the editor could not be initialized
     * @throws CoreException if the proxy cannot be initialized
     * @since 3.1
     */
    public static void openEditorAndSelect(String path, int line, IProject project) throws PartInitException, BadLocationException, CoreException {
        IWorkbenchPage activePage = ProfileUIPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow().getActivePage();
        IRemoteFileProxy proxy = null;
        proxy = RemoteProxyManager.getInstance().getFileProxy(project);
        IFileStore file = proxy.getResource(path);
        if (file.fetchInfo().exists()) {
            IEditorPart editor = IDE.openEditorOnFileStore(activePage, file);
            if (editor instanceof ITextEditor) {
                ITextEditor textEditor = (ITextEditor) editor;

                if (line > 0) {
                    IDocumentProvider provider = textEditor.getDocumentProvider();
                    IDocument document = provider.getDocument(textEditor.getEditorInput());

                    int start = document.getLineOffset(line - 1); //zero-indexed
                    textEditor.selectAndReveal(start, 0);
                }
            }
        }
    }


    /**
     * Opens an editor on the given file and selects the line.
     * @param file The file to open.
     * @param line The line to select.
     * @throws PartInitException If opening editor failed.
     * @throws BadLocationException If line number is invalid.
     *
     * @since 2.0
     */
    public static void openEditorAndSelect(IFile file, int line) throws PartInitException, BadLocationException {
        if (file.exists()) {
            IWorkbenchPage activePage = ProfileUIPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow().getActivePage();

            IEditorPart editor = IDE.openEditor(activePage, file);
            if (editor instanceof ITextEditor) {
                ITextEditor textEditor = (ITextEditor) editor;

                if (line > 0) {
                    IDocumentProvider provider = textEditor.getDocumentProvider();
                    IDocument document = provider.getDocument(textEditor.getEditorInput());

                    int start = document.getLineOffset(line - 1); //zero-indexed
                    textEditor.selectAndReveal(start, 0);
                }
            }
        }
    }

    /**
     * Opens the specified file in an editor (or selects an already open
     * editor) and highlights the specified line.
     * @param result - result of performing source lookup with a org.eclipse.debug.core.model.ISourceLocator
     * @param line - line number to select, 0 to not select a line
     * @throws PartInitException - Failed to open editor
     * @throws BadLocationException - Line number not valid in file
     * @see DebugUITools#lookupSource(Object, org.eclipse.debug.core.model.ISourceLocator)
     */
    public static void openEditorAndSelect(ISourceLookupResult result, int line) throws PartInitException, BadLocationException {
        IEditorInput input = result.getEditorInput();
        String editorID = result.getEditorId();

        if (input == null || editorID == null) {
            // Consult the CDT DebugModelPresentation
            Object sourceElement = result.getSourceElement();
            if (sourceElement != null) {
                // Resolve IResource in case we get a LocalFileStorage object
                if (sourceElement instanceof LocalFileStorage) {
                    IPath filePath = ((LocalFileStorage) sourceElement).getFullPath();
                    URI fileURI = URIUtil.toURI(filePath);
                    IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
                    IFile[] files = root.findFilesForLocationURI(fileURI);
                    if (files.length > 0) {
                        // Take the first match
                        sourceElement = files[0];
                    }
                }

                IDebugModelPresentation pres = DebugUITools.newDebugModelPresentation(CDebugCorePlugin.getUniqueIdentifier());
                input = pres.getEditorInput(sourceElement);
                editorID = pres.getEditorId(input, sourceElement);
                pres.dispose();
            }
        }
        if (input != null && editorID != null) {
            // Open the editor
            IWorkbenchPage activePage = ProfileUIPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow().getActivePage();

            IEditorPart editor = IDE.openEditor(activePage, input, editorID);
            // Select the line
            if (editor instanceof ITextEditor) {
                ITextEditor textEditor = (ITextEditor) editor;

                if (line > 0) {
                    IDocumentProvider provider = textEditor.getDocumentProvider();
                    IDocument document = provider.getDocument(textEditor.getEditorInput());

                    IRegion lineRegion = document.getLineInformation(line - 1); //zero-indexed
                    textEditor.selectAndReveal(lineRegion.getOffset(), lineRegion.getLength());
                }
            }
        }
    }

    /**
     * Open a file in the Editor at the specified offset, highlighting the given length
     *
     * @param path : Absolute path pointing to the file which will be opened.
     * @param offset : Offset of the function to be highlighted.
     * @param length : Length of the function to be highlighted.
     * @throws PartInitException if the editor could not be initialized
     */
    public static void openEditorAndSelect(String path, int offset, int length) throws PartInitException {
        Path p = new Path (path);

        if (p.toFile().exists()) {
            IWorkbenchPage activePage = ProfileUIPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow().getActivePage();
            IFileStore fileStore = EFS.getLocalFileSystem().getStore(p);

            IEditorPart editor = IDE.openEditorOnFileStore(activePage, fileStore);
            if (editor instanceof ITextEditor) {
                ITextEditor text = (ITextEditor) editor;
                text.selectAndReveal(offset, length);
            }
        }
    }

    /**
     * Find an ICProject that contains the specified absolute path.
     *
     * @param absPath An absolute path (usually to some file/folder in a project)
     * @return an ICProject corresponding to the project that contains the absolute path
     * @throws CoreException can be thrown if visiting (accepting) a project walking the ICElement tree fails.
     */
    public static ICProject findCProjectWithAbsolutePath(final String absPath) throws CoreException{
        final String workspaceLoc = ResourcesPlugin.getWorkspace().getRoot().getLocation().toOSString();
        final ArrayList<ICProject> ret = new ArrayList<>();

        // visitor object to check for the matching path string
        ICElementVisitor vis = element -> {
		    if (element.getElementType() == ICElement.C_CCONTAINER
		            || element.getElementType() == ICElement.C_PROJECT){
		        return true;
		    }else if (absPath.equals(workspaceLoc+element.getPath().toFile().getAbsolutePath())){
		        ret.add(element.getCProject());
		    }
		    return false;
      };

        ICProject[] cProjects = CCorePlugin.getDefault().getCoreModel().getCModel().getCProjects();
        for (ICProject proj : cProjects){
                // visit every project
                proj.accept(vis);
        }

        // is it possible to find more than one matching project ?
        return ret.isEmpty() ? null : ret.get(0);
    }

    /**
     * Get a mapping between a file name, and the data relevant to locating
     * the corresponding function name for a given project.
     *
     * @param project : C Project Type
     * @param functionName : Name of a function
     * @param numArgs : The number of arguments this function is expected to have.
     * A value of -1 will ignore the number of arguments when searching.
     * @param fileHint : The name of the file where we expect to find functionName.
     * It is null if we do not want to use this option.
     * @return Absolute paths of files and the function's corresponding node-offset and length.
     * @since 3.0
     */
    public static Map<String,int[]> findFunctionsInProject(ICProject project, String functionName,
            int numArgs, String fileHint)  {
          HashMap<String,int[]> files = new HashMap<>() ;

          IIndexManager manager = CCorePlugin.getIndexManager();
          IIndex index = null;
            try {
                index = manager.getIndex(project);
                index.acquireReadLock();
                IBinding[] bindings = index.findBindings(functionName.toCharArray(), IndexFilter.ALL, null);
                for (IBinding bind : bindings) {
                    if (bind instanceof IFunction
                            && (numArgs == -1 || ((IFunction)bind).getParameters().length == numArgs)) {
                        IFunction ifunction = (IFunction) bind;
                        IIndexName[] names = index.findNames(ifunction, IIndex.FIND_DEFINITIONS);
                        for (IIndexName iname : names) {
                            IIndexFile file = iname.getFile();
                            if (file != null) {
                                String loc = file.getLocation().getURI().getPath();
                                if (fileHint != null){
                                    if (loc.equals(new File(fileHint).getCanonicalPath())){
                                        //TODO: Consider changing data structure so that we can
                                        // store multiple same-named functions (different args)
                                        // from the same file.
                                        files.put(loc, new int [] {iname.getNodeOffset(), iname.getNodeLength()});
                                    }
                                }else{
                                    files.put(loc, new int [] {iname.getNodeOffset(), iname.getNodeLength()});
                                }
                            }
                        }
                    }
                }

            } catch (CoreException | InterruptedException | IOException e) {
                e.printStackTrace();
            } finally{
                index.releaseReadLock();
            }
           return files;
    }

    /**
     * Helper function for findFunctionsInProject
     * @param project   C Project Type
     * @param functionName  Name of a function
     * @param numArgs  The number of arguments this function is expected to have
     * A value of -1 will ignore the number of arguments when searching
     * @param fileHint  The name of the file where we expect to find functionName
     * It is null if we do not want to use this option
     * @param needResult true if result is needed
     * @return Absolute paths of files and the function's corresponding node-offset and length
     * @since 3.0
     */
    public static Map<String,int[]> findFunctionsInProject(ICProject project, String functionName,
            int numArgs, String fileHint, boolean needResult){
        Map<String, int []> map = findFunctionsInProject(project, functionName, numArgs, fileHint);
        if (needResult && map.isEmpty()){
            map = findFunctionsInProject(project, functionName, -1, fileHint);
            if (map.isEmpty()){
                return findFunctionsInProject(project, functionName, -1, null);
            }
        }
        return map;
    }

}
