package bndtools.launch;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IProject;
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 = input.getAdapter(IJavaElement.class);
                }

                if (element != null) {
                    launchJavaElements(Collections.singletonList(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 launchJavaElements(List<IJavaElement> elements, String mode) throws CoreException {
        assert elements != null && elements.size() > 0;

        IProject targetProject = elements.get(0)
            .getJavaProject()
            .getProject();
        IPath projectPath = targetProject.getFullPath()
            .makeRelative();

        ILaunchConfiguration config = findLaunchConfig(projectPath);
        ILaunchConfigurationWorkingCopy wc = null;

        if (config == null) {
            wc = createConfiguration(projectPath, targetProject);
        } else {
            wc = config.getWorkingCopy();
        }

        if (wc != null) {
            customise(elements, wc);
            config = wc.doSave();
            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(List<IJavaElement> elements, ILaunchConfigurationWorkingCopy config) throws JavaModelException {

        assert elements != null;

        Set<String> testNames = new HashSet<String>();
        for (IJavaElement element : elements) {
            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;
    }
}