/*******************************************************************************
 * Copyright (c) 2017 Microsoft Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Microsoft Corporation - initial API and implementation
*******************************************************************************/

package com.microsoft.java.debug.plugin.internal;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
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.IPath;
import org.eclipse.debug.core.sourcelookup.ISourceContainer;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IModuleDescription;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.launching.IRuntimeClasspathEntry;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jdt.launching.sourcelookup.containers.JavaProjectSourceContainer;
import org.eclipse.jdt.launching.sourcelookup.containers.PackageFragmentRootSourceContainer;

import com.microsoft.java.debug.core.DebugException;
import com.microsoft.java.debug.core.StackFrameUtility;
import com.sun.jdi.AbsentInformationException;
import com.sun.jdi.ArrayType;
import com.sun.jdi.ClassNotLoadedException;
import com.sun.jdi.Location;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.StackFrame;
import com.sun.jdi.Type;

public class JdtUtils {
    private static final String TEST_SCOPE = "test";
    private static final String MAVEN_SCOPE_ATTRIBUTE = "maven.scope";
    private static final String GRADLE_SCOPE_ATTRIBUTE = "gradle_scope";

    /**
     * Returns the module this project represents or null if the Java project doesn't represent any named module.
     */
    public static String getModuleName(IJavaProject project) {
        if (project == null || !JavaRuntime.isModularProject(project)) {
            return null;
        }
        IModuleDescription module;
        try {
            module = project.getModuleDescription();
        } catch (CoreException e) {
            return null;
        }
        return module == null ? null : module.getElementName();
    }

    /**
     * Check if the project is a java project or not.
     */
    public static boolean isJavaProject(IProject project) {
        if (project == null || !project.exists()) {
            return false;
        }
        try {
            if (!project.isNatureEnabled(JavaCore.NATURE_ID)) {
                return false;
            }
        } catch (CoreException e) {
            return false;
        }
        return true;
    }

    /**
     * If the project represents a java project, then convert it to a java project.
     * Otherwise, return null.
     */
    public static IJavaProject getJavaProject(IProject project) {
        if (isJavaProject(project)) {
            return JavaCore.create(project);
        }
        return null;
    }

    /**
     * Given the project name, return the corresponding java project model.
     * If the project doesn't exist or not a java project, return null.
     */
    public static IJavaProject getJavaProject(String projectName) {
        if (StringUtils.isBlank(projectName)) {
            return null;
        }
        IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
        IProject project = root.getProject(projectName);
        return getJavaProject(project);
    }

    /**
     * List all available Java projects of the specified workspace.
     */
    public static List<IJavaProject> listJavaProjects(IWorkspaceRoot workspace) {
        List<IJavaProject> results = new ArrayList<>();
        for (IProject project : workspace.getProjects()) {
            if (isJavaProject(project)) {
                results.add(JavaCore.create(project));
            }
        }
        return results;
    }

    /**
     * Given the project name, return the corresponding project object.
     * If the project doesn't exist, return null.
     */
    public static IProject getProject(String projectName) {
        if (StringUtils.isBlank(projectName)) {
            return null;
        }
        IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
        return root.getProject(projectName);
    }

    /**
     * Compute the fragment roots for each test source folders.
     *
     * @param project the java project.
     * @return the fragment roots for each test source folders.
     */
    public static IPackageFragmentRoot[] getTestPackageFragmentRoots(IJavaProject project) {
        try {
            IPackageFragmentRoot[] packageFragmentRoot = project.getPackageFragmentRoots();
            List<IPackageFragmentRoot> sources = new ArrayList<>();
            for (int i = 0; i < packageFragmentRoot.length; i++) {
                if (packageFragmentRoot[i].getElementType() == IJavaElement.PACKAGE_FRAGMENT_ROOT
                        && packageFragmentRoot[i].getKind() == IPackageFragmentRoot.K_SOURCE) {
                    IClasspathEntry cpe = packageFragmentRoot[i].getResolvedClasspathEntry();

                    if (isTest(cpe)) {
                        sources.add(packageFragmentRoot[i]);
                    }
                }
            }
            return sources.toArray(new IPackageFragmentRoot[0]);
        } catch (JavaModelException e) {
            // ignore
            return new IPackageFragmentRoot[0];
        }
    }

    /**
     * There is an issue on IClasspathEntry#isTest: it will return true if the scope is runtime, so we will this method for testing whether
     * the classpath entry is for test only.
     *
     * @param classpathEntry classpath entry
     * @return whether this classpath entry is only used in test
     */
    public static boolean isTest(final IClasspathEntry classpathEntry) {
        for (IClasspathAttribute attribute : classpathEntry.getExtraAttributes()) {
            if (GRADLE_SCOPE_ATTRIBUTE.equals(attribute.getName()) || MAVEN_SCOPE_ATTRIBUTE.equals(attribute.getName())) {
                return TEST_SCOPE.equals(attribute.getValue());
            }
        }
        return classpathEntry.isTest();
    }

    /**
     * Compute the possible source containers that the specified project could be associated with.
     * <p>
     * If the project name is specified, it will put the source containers parsed from the specified project's
     * classpath entries in the front of the result, then the other projects at the same workspace.
     * </p>
     * <p>
     * Otherwise, just loop every projects at the current workspace and combine the parsed source containers directly.
     * </p>
     * @param projectName
     *                  the project name.
     * @return the possible source container list.
     */
    public static ISourceContainer[] getSourceContainers(String projectName) {
        Set<ISourceContainer> containers = new LinkedHashSet<>();
        List<IProject> projects = new ArrayList<>();

        // If the project name is specified, firstly compute the source containers from the specified project's
        // classpath entries so that they can be placed in the front of the result.
        IProject targetProject = JdtUtils.getProject(projectName);
        if (targetProject != null) {
            projects.add(targetProject);
        }

        List<IProject> workspaceProjects = Arrays.asList(ResourcesPlugin.getWorkspace().getRoot().getProjects());
        projects.addAll(workspaceProjects);

        Set<IRuntimeClasspathEntry> calculated = new LinkedHashSet<>();

        projects.stream().distinct().map(project -> JdtUtils.getJavaProject(project))
            .filter(javaProject -> javaProject != null && javaProject.exists())
            .forEach(javaProject -> {
                // Add source containers associated with the project's runtime classpath entries.
                containers.addAll(Arrays.asList(getSourceContainers(javaProject, calculated)));
                // Add source containers associated with the project's source folders.
                containers.add(new JavaProjectSourceContainer(javaProject));
            });

        return containers.toArray(new ISourceContainer[0]);
    }

    private static ISourceContainer[] getSourceContainers(IJavaProject project, Set<IRuntimeClasspathEntry> calculated) {
        if (project == null || !project.exists()) {
            return new ISourceContainer[0];
        }

        try {
            IRuntimeClasspathEntry[] unresolved = JavaRuntime.computeUnresolvedRuntimeClasspath(project);
            List<IRuntimeClasspathEntry> resolved = new ArrayList<>();
            for (IRuntimeClasspathEntry entry : unresolved) {
                for (IRuntimeClasspathEntry resolvedEntry : JavaRuntime.resolveRuntimeClasspathEntry(entry, project)) {
                    if (!calculated.contains(resolvedEntry)) {
                        calculated.add(resolvedEntry);
                        resolved.add(resolvedEntry);
                    }
                }
            }
            Set<ISourceContainer> containers = new LinkedHashSet<>();
            containers.addAll(Arrays.asList(
                    JavaRuntime.getSourceContainers(resolved.toArray(new IRuntimeClasspathEntry[0]))));

            // Due to a known jdt java 9 support bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=525840,
            // it would miss some JRE libraries source containers when the debugger is running on JDK9.
            // As a workaround, recompute the possible source containers for JDK9 jrt-fs.jar libraries.
            IRuntimeClasspathEntry jrtFs = resolved.stream().filter(entry -> {
                return entry.getType() == IRuntimeClasspathEntry.ARCHIVE && entry.getPath().lastSegment().equals("jrt-fs.jar");
            }).findFirst().orElse(null);
            if (jrtFs != null && project.isOpen()) {
                IPackageFragmentRoot[] allRoots = project.getPackageFragmentRoots();
                for (IPackageFragmentRoot root : allRoots) {
                    if (root.getPath().equals(jrtFs.getPath()) && isSourceAttachmentEqual(root, jrtFs)) {
                        containers.add(new PackageFragmentRootSourceContainer(root));
                    }
                }
            }

            return containers.toArray(new ISourceContainer[0]);
        } catch (CoreException ex) {
         // do nothing.
        }

        return new ISourceContainer[0];
    }

    private static boolean isSourceAttachmentEqual(IPackageFragmentRoot root, IRuntimeClasspathEntry entry) throws JavaModelException {
        IPath entryPath = entry.getSourceAttachmentPath();
        if (entryPath == null) {
            return true;
        }
        IPath rootPath = root.getSourceAttachmentPath();
        if (rootPath == null) {
            // entry has a source attachment that the package root does not
            return false;
        }
        return rootPath.equals(entryPath);

    }

    /**
     * Given a source name info, search the associated source file or class file from the source container list.
     *
     * @param sourcePath
     *                  the target source name (e.g. com\microsoft\java\debug\xxx.java).
     * @param containers
     *                  the source container list.
     * @return the associated source file or class file.
     */
    public static Object findSourceElement(String sourcePath, ISourceContainer[] containers) {
        if (containers == null) {
            return null;
        }
        for (ISourceContainer container : containers) {
            try {
                Object[] objects = container.findSourceElements(sourcePath);
                if (objects.length > 0 && (objects[0] instanceof IResource || objects[0] instanceof IClassFile)) {
                    return objects[0];
                }
            } catch (CoreException e) {
                // do nothing.
            }
        }
        return null;
    }

    /**
     * Given a stack frame, find the target java project that the associated source file belongs to.
     *
     * @param stackFrame
     *                  the stack frame.
     * @param containers
     *                  the source container list.
     * @return the java project.
     */
    public static IJavaProject findProject(StackFrame stackFrame, ISourceContainer[] containers) {
        Location location = stackFrame.location();
        try {
            Object sourceElement = findSourceElement(location.sourcePath(), containers);
            if (sourceElement instanceof IResource) {
                return JavaCore.create(((IResource) sourceElement).getProject());
            } else if (sourceElement instanceof IClassFile) {
                IJavaProject javaProject = ((IClassFile) sourceElement).getJavaProject();
                if (javaProject != null) {
                    return javaProject;
                }
            }
        } catch (AbsentInformationException e) {
            // When the compiled .class file doesn't contain debug source information, return null.
        }
        return null;
    }

    /**
     * Given a stack frame, get the fully qualified type name that associated with the frame.
     * @param frame the stack frame
     * @return the fully qualified type name
     * @throws DebugException debug exception
     */
    public static String getDeclaringTypeName(StackFrame frame) throws DebugException {
        return getGenericName(StackFrameUtility.getDeclaringType(frame));
    }

    private static String getGenericName(ReferenceType type) throws DebugException {
        if (type instanceof ArrayType) {
            try {
                Type componentType;
                componentType = ((ArrayType) type).componentType();
                if (componentType instanceof ReferenceType) {
                    return getGenericName((ReferenceType) componentType) + "[]"; //$NON-NLS-1$
                }
                return type.name();
            } catch (ClassNotLoadedException e) {
                // we cannot create the generic name using the component type,
                // just try to create one with the information
            }
        }
        String signature = type.signature();
        StringBuffer res = new StringBuffer(getTypeName(signature));
        String genericSignature = type.genericSignature();
        if (genericSignature != null) {
            String[] typeParameters = Signature.getTypeParameters(genericSignature);
            if (typeParameters.length > 0) {
                res.append('<').append(Signature.getTypeVariable(typeParameters[0]));
                for (int i = 1; i < typeParameters.length; i++) {
                    res.append(',').append(Signature.getTypeVariable(typeParameters[i]));
                }
                res.append('>');
            }
        }
        return res.toString();
    }

    private static String getTypeName(String genericTypeSignature) {
        int arrayDimension = 0;
        while (genericTypeSignature.charAt(arrayDimension) == '[') {
            arrayDimension++;
        }
        int parameterStart = genericTypeSignature.indexOf('<');
        StringBuffer name = new StringBuffer();
        if (parameterStart < 0) {
            name.append(genericTypeSignature.substring(arrayDimension + 1, genericTypeSignature.length() - 1)
                    .replace('/', '.'));
        } else {
            if (parameterStart != 0) {
                name.append(genericTypeSignature.substring(arrayDimension + 1, parameterStart).replace('/', '.'));
            }
            try {
                String sig = Signature.toString(genericTypeSignature)
                        .substring(Math.max(parameterStart - 1, 0) - arrayDimension);
                name.append(sig.replace('/', '.'));
            } catch (IllegalArgumentException iae) {
                // do nothing
                name.append(genericTypeSignature);
            }
        }
        for (int i = 0; i < arrayDimension; i++) {
            name.append("[]"); //$NON-NLS-1$
        }
        return name.toString();
    }
}