package bndtools.launch;

import java.util.HashSet;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IParent;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import aQute.lib.strings.Strings;
import bndtools.launch.api.AbstractLaunchShortcut;

public class JUnitShortcut extends AbstractLaunchShortcut {

    public JUnitShortcut() {
        super(LaunchConstants.LAUNCH_ID_OSGI_JUNIT);
    }

    /*
    * This is called when the launch starts in the editor. If a method or type is selected
    * in a Java Editor, we will test that type/method.
    */
    @Override
    public void launch(IEditorPart editor, String mode) {
        try {

            //
            // Check if a method is selected. It is more precise then the 
            // older next method that adapts the editor to a JavaElement
            // this in general returns a Compilation Unit
            //

            if (editor instanceof JavaEditor) {
                IJavaElement element = getSelectedJavaElement((JavaEditor) editor);
                if (element == null) {
                    IEditorInput input = editor.getEditorInput();
                    element = (IJavaElement) input.getAdapter(IJavaElement.class);
                }

                if (element != null) {
                    launchJavaElement(element, mode);
                    return;
                }
            }

        } catch (CoreException e) {
            e.printStackTrace();
        }
        //
        // Did not succeed to get a JavaElement, we try the original way
        //
        super.launch(editor, mode);
    }

    @Override
    protected void launchJavaElement(IJavaElement element, String mode) throws CoreException {
        IPath projectPath = element.getJavaProject().getProject().getFullPath().makeRelative();

        ILaunchConfiguration config = findLaunchConfig(projectPath);
        if (config == null) {
            ILaunchConfigurationWorkingCopy wc = createConfiguration(projectPath);
            wc.doSave();

            customise(element, wc);
            config = wc;
        } else {
            ILaunchConfigurationWorkingCopy wc = config.getWorkingCopy();
            customise(element, wc);
            config = wc;
        }
        DebugUITools.launch(config, mode);
    }

    /*
     * From the Java element, we traverse over all the children of the given element 
     * until we hit a Type. Types are then added if they are either a JUnit 3 
     * (implements junit.framework.Test) or JUnit 4 (one of the methods has an org.junit 
     * annotation)
     * 
     */
    private static void customise(IJavaElement element, ILaunchConfigurationWorkingCopy config) throws JavaModelException {

        assert element != null;

        Set<String> testNames = new HashSet<String>();
        gatherTests(testNames, element, config);
        if (!testNames.isEmpty()) {
            config.setAttribute(OSGiJUnitLaunchDelegate.ORG_BNDTOOLS_TESTNAMES, Strings.join(" ", testNames));
        }
    }

    /*
     * Recursive method that traverses the children of a Java Element and gathers test case types.
     */
    private static void gatherTests(Set<String> testNames, IJavaElement element, ILaunchConfigurationWorkingCopy config) throws JavaModelException {

        assert element != null;

        if (element.isReadOnly()) // not interested in in non-source
            return;

        switch (element.getElementType()) {

        /*
         * For a type, we only have to check if it is testable.
         */
        case IJavaElement.TYPE : {
            IType type = (IType) element;
            if (isTestable(type)) {
                testNames.add((type).getFullyQualifiedName());
            }
        }
            break;

        /*
         * Normally we do not traverse to methods but we can call the customise with a method 
         */
        case IJavaElement.METHOD : {
            IMethod method = (IMethod) element;
            String typeName = ((IType) method.getParent()).getFullyQualifiedName();
            String methodName = method.getElementName();
            testNames.add(typeName + ":" + methodName);
        }
            break;

        /*
         * Default we go deeper if the element is a parent
         */
        default : {
            if (element instanceof IParent)
                for (IJavaElement type : ((IParent) element).getChildren()) {
                    gatherTests(testNames, type, config);
                }
        }
            break;
        }
    }

    /*
     * Check if this element can be tested
     */
    private static boolean isTestable(IType type) throws JavaModelException {

        assert type != null;

        int flags = type.getFlags();

        //
        // Must be a class, not abstract, and public
        //

        if (!type.isClass() || Flags.isAbstract(flags) || !Flags.isPublic(flags))
            return false;

        //
        // One of the super classes/interfaces must be a Junit class/interface
        //
        ITypeHierarchy hierarchy = type.newSupertypeHierarchy(null);
        for (IType z : hierarchy.getAllSupertypes(type)) {
            if (z.getFullyQualifiedName().startsWith("junit."))
                return true;
        }

        if (isJUnit4(type))
            return true;

        for (IType z : hierarchy.getAllSuperclasses(type)) {
            if (isJUnit4(z))
                return true;
        }

        return false;
    }

    /*
    *
    * Check if any of the methods has an annotation
    * from the org.junit package
    *
    */

    private static boolean isJUnit4(IType z) throws JavaModelException {
        for (IMethod m : z.getMethods()) {
            if (Flags.isPublic(m.getFlags())) {
                for (IAnnotation annotation : m.getAnnotations()) {
                    String[][] names = m.getDeclaringType().resolveType(annotation.getElementName());
                    for (String[] pair : names) {
                        if (pair[0].contains("org.junit"))
                            return true;
                    }
                }
            }
        }

        return false;
    }

    /*
     * Helper method to find out the selected Java Element from a Java Editor
     */
    private IJavaElement getSelectedJavaElement(JavaEditor editor) throws JavaModelException {
        IJavaElement elem = JavaUI.getEditorInputJavaElement(editor.getEditorInput());
        if (elem instanceof ICompilationUnit) {
            ITextSelection selection = (ITextSelection) editor.getSelectionProvider().getSelection();
            if (selection != null)
                return ((ICompilationUnit) elem).getElementAt(selection.getOffset());
        }
        return null;
    }
}
@@ -2594,42 +2594,8 @@
 h);%0A
-        if (config == null) %7B%0A    
     
@@ -2639,189 +2639,124 @@
 c = 
-createConfiguration(projectPath);%0A            wc.doSave();%0A%0A            customise(element, wc);%0A            config = wc;%0A        %7D else %7B%0A            ILaunchConfigurationWorkingCopy
+null;%0A%0A        if (config == null) %7B%0A            wc = createConfiguration(projectPath);%0A        %7D else %7B%0A           
  wc 
@@ -2778,24 +2778,61 @@
 kingCopy();%0A
+        %7D%0A%0A        if (wc != null) %7B%0A
             
@@ -2878,28 +2878,31 @@
 fig = wc
+.doSave()
 ;%0A    
-    %7D%0A
         
@@ -2928,32 +2928,42 @@
 (config, mode);%0A
+        %7D%0A
     %7D%0A%0A    /*%0A  

Fix bnd unit tests launch shortcut to add launch into Eclipse run history