package org.testng.eclipse.util;


import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageDeclaration;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IParent;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jface.operation.IRunnableContext;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.ui.PlatformUI;
import org.testng.eclipse.TestNGPlugin;
import org.testng.eclipse.launch.TestFinder;
import org.testng.eclipse.launch.components.Filters;
import org.testng.eclipse.launch.components.ITestContent;
import org.testng.eclipse.ui.util.TypeParser;

import com.google.common.collect.Sets;

/**
 * Search engine for TestNG related elements.
 * <P/>
 * Original idea from org.eclipse.jdt.internal.junit.util.TestSearchEngine
 * 
 * @author <a href='mailto:the_mindstorm@evolva.ro'>Alexandru Popescu</a>
 * @author cedric
 */
public class TestSearchEngine {

  public static IType[] findTestNGTests(IRunnableContext context,
      final IJavaElement element) throws InvocationTargetException, InterruptedException {
    final Set<IType> result = Sets.newHashSet();
    
    IRunnableWithProgress runnable = new IRunnableWithProgress() {
      public void run(IProgressMonitor pm) throws InterruptedException, InvocationTargetException {
        try {
          new TestFinder().findTestsInContainer(element, result, pm);
        } catch (CoreException e) {
          throw new InvocationTargetException(e);
        }
      }
    };
    context.run(true, true, runnable);
    
    return result.toArray(new IType[result.size()]);
  }

  /**
   * Searches for TestNG test types.
   *
   * @param context
   * @param javaProject
   * @return
   * @throws InvocationTargetException
   * @throws InterruptedException
   */
  public static IType[] findTests(IRunnableContext context,
                                  final Object[] elements,
                                  final Filters.ITypeFilter filter) throws InvocationTargetException, InterruptedException {
    final Set<IType> result = Sets.newHashSet();

    if(elements.length != 0) {
      IRunnableWithProgress runnable = new IRunnableWithProgress() {
          public void run(IProgressMonitor pm) throws InterruptedException {
            doFindTests(elements, result, pm, filter);
          }
        };
      context.run(true, true, runnable);
    }

    return result.toArray(new IType[result.size()]);
  }
  
  public static String[] findPackages(IRunnableContext context,
			final Object[] elements)
			throws InvocationTargetException, InterruptedException {
    final Set<String> result = Sets.newHashSet();

		if (elements.length != 0) {
			IRunnableWithProgress runnable = new IRunnableWithProgress() {
				public void run(IProgressMonitor pm)
						throws InterruptedException {
					doFindPackages(elements, result, pm);
				}
			};
			context.run(true, true, runnable);
		}

		return result.toArray(new String[result.size()]);
	}
  
  public static String[] findMethods(IRunnableContext context,
			final Object[] elements, final String className)
			throws InvocationTargetException, InterruptedException {
		final Set<String> result = new HashSet<>();

		if (elements.length != 0) {
			IRunnableWithProgress runnable = new IRunnableWithProgress() {
				public void run(IProgressMonitor pm)
						throws InterruptedException {
					doFindMethods(elements, result, pm, className);
				}
			};
			context.run(true, true, runnable);
		}

		return result.toArray(new String[result.size()]);
	}

  public static IFile[] findSuites(IRunnableContext context,
                                  final Object[] elements) throws InvocationTargetException, InterruptedException {
    final Set<IFile> result = new HashSet<>();

    if(elements.length != 0) {
      IRunnableWithProgress runnable = new IRunnableWithProgress() {
          public void run(IProgressMonitor pm) throws InterruptedException {
            doFindSuites(elements, result, pm);
          }
        };
      context.run(true, true, runnable);
    }

    return result.toArray(new IFile[result.size()]);
  }

  public static IFile[] findSuites(final Object[] elements) throws InterruptedException,
                                                                   InvocationTargetException {
    final Set<IFile> result = new HashSet<>();

    if(elements.length > 0) {
      IRunnableWithProgress runnable = new IRunnableWithProgress() {
          public void run(IProgressMonitor pm) throws InterruptedException {
            doFindSuites(elements, result, pm);
          }
        };
      PlatformUI.getWorkbench().getProgressService().busyCursorWhile(runnable);
    }

    return result.toArray(new IFile[result.size()]);
  }

  public static IType[] findTests(final Object[] elements, final Filters.ITypeFilter filter)
  throws InvocationTargetException, InterruptedException {
    final Set<IType> result = new HashSet<>();

    if(elements.length > 0) {
      IRunnableWithProgress runnable = new IRunnableWithProgress() {
          public void run(IProgressMonitor pm) throws InterruptedException {
            doFindTests(elements, result, pm, filter);
          }
        };
      PlatformUI.getWorkbench().getProgressService().busyCursorWhile(runnable);
    }

    return result.toArray(new IType[result.size()]);
  }
  
  public static IType[] findPackages(final Object[] elements)
  throws InvocationTargetException, InterruptedException {
    final Set<IType> result = new HashSet<>();

    if(elements.length > 0) {
      IRunnableWithProgress runnable = new IRunnableWithProgress() {
          public void run(IProgressMonitor pm) throws InterruptedException {
            doFindPackages(elements, result, pm);
          }
        };
      PlatformUI.getWorkbench().getProgressService().busyCursorWhile(runnable);
    }

    return result.toArray(new IType[result.size()]);
  }
  

  private static Map/*<IJavaElement,Boolean>*/ s_isTestCache= new HashMap();
  
  /**
   * Returns true in case the IJavaElement is one of COMPILATION_UNIT, TYPE or METHOD 
   * representing a valid TestNG test element.
   */
  public static boolean isTest(IJavaElement ije) {
    Boolean cachedResult= (Boolean) s_isTestCache.get(ije);
    if(null != cachedResult && cachedResult.booleanValue()) {
      return true;
    }
    
    boolean result;
    
    try {
      if(IJavaElement.METHOD == ije.getElementType()) {
        result = doIsTest((IMethod) ije);
      }
      else if(IJavaElement.COMPILATION_UNIT == ije.getElementType()) {
        result = doIsTest((ICompilationUnit) ije);
      } 
      else if(IJavaElement.TYPE == ije.getElementType()) {
        result = doIsTest((IType) ije);
      }
      else {
        result = false;
      }
    }
    catch(JavaModelException jme) {
      TestNGPlugin.log(jme);
      return false;
    }
    
    if(result) {
      s_isTestCache.put(ije, Boolean.TRUE);
    }

    return result;
  }
  
  private static boolean doIsTest(IMethod iMethod) {
    ITestContent content = TypeParser.parseType(iMethod.getDeclaringType());
    if(content.hasTestMethods()) {
      return content.isTestMethod(iMethod);
    }
    return false;
  }
  
  private static boolean doIsTest(ICompilationUnit iCompilationUnit) throws JavaModelException {
    IType[] types = iCompilationUnit.getAllTypes();
    for (IType type : types) {
      if (doIsTest(type)) {
        return true;
      }
    }
    return false;
  }
  
  private static boolean doIsTest(IType iType) throws JavaModelException {
    if (Flags.isAbstract(iType.getFlags())) {
      return false;
    }
    ITestContent testContent = TypeParser.parseType(iType);
    if (testContent.hasTestMethods()) {
      return true;
    }

    // Check if super-type contains tests.
    IType[] superclasses;
    try {
      superclasses = iType.newSupertypeHierarchy(null).getAllSuperclasses(iType);
    } catch (JavaModelException e) {
      TestNGPlugin.log("Could not resolve supertype of "+iType.getFullyQualifiedName());
      return false;
    }

    for (IType superclass : superclasses) {
      ITestContent superContent = TypeParser.parseType(superclass);
      if (superContent.hasTestMethods()) {
        return true;
      }
    }
    return false;
  }
  
  private static void doFindTests(Object[] elements,
                                  Set result,
                                  IProgressMonitor pm,
                                  Filters.ITypeFilter filter) throws InterruptedException {
    int nElements = elements.length;
    pm.beginTask(ResourceUtil.getString("TestSearchEngine.message.searching"), nElements); //$NON-NLS-1$
    try {
      for(int i = 0; i < nElements; i++) {
        try {
          collectTypes(elements[i], new SubProgressMonitor(pm, 1), result, filter);
        }
        catch(CoreException e) {
          TestNGPlugin.log(e.getStatus());
        }
        if(pm.isCanceled()) {
          throw new InterruptedException();
        }
      }
    }
    finally {
//      pm.done();
    }
  }
  
  private static void doFindPackages(Object[] elements, Set result,
			IProgressMonitor pm)
			throws InterruptedException {
		int nElements = elements.length;
		pm.beginTask(ResourceUtil
				.getString("TestSearchEngine.message.searching"), nElements); //$NON-NLS-1$
		try {
			for (int i = 0; i < nElements; i++) {

				if (elements[i] instanceof IJavaElement) {
					findPackages(((IJavaElement) elements[i]).getJavaProject(),
							result);
				}

				if (pm.isCanceled()) {
					throw new InterruptedException();
				}
			}
		} finally {
			pm.done();
		}
	}
  private static void doFindMethods(Object[] elements, Set result,
			IProgressMonitor pm, String className)
			throws InterruptedException {
		int nElements = elements.length;
		pm.beginTask(ResourceUtil
				.getString("TestSearchEngine.message.searching"), nElements); //$NON-NLS-1$
		try {
			for (int i = 0; i < nElements; i++) {
                
				if (elements[i] instanceof IJavaElement) {
					findMethods(((IJavaElement) elements[i]).getJavaProject(),
							result, className);
				}
               
				if (pm.isCanceled()) {
					throw new InterruptedException();
				}
			}
		} finally {
			pm.done();
		}
	}
  private static boolean isTestNgXmlFile(IFile f) {
    String fileExtension = f.getFileExtension();
    if(!"xml".equals(fileExtension) && !"yaml".equals(fileExtension)) {
      return false;
    }

    try {
      return SuiteFileValidator.isSuiteDefinition(f);
    }
    catch(CoreException ce) {
      TestNGPlugin.log(ce);
    }

    return false;
  }

  private static void doFindSuites(Object[] elements, Set<IFile> result,
                                   IProgressMonitor pm) 
  throws InterruptedException 
{
    int nElements = elements.length;
    pm.beginTask(ResourceUtil.getString("TestSearchEngine.message.searching"), nElements); //$NON-NLS-1$
    
    try {
      for(int i = 0; i < nElements; i++) {
        if(elements[i] instanceof IJavaProject) {
          findSuites(((IJavaProject) elements[i]).getProject(), result);
        }
        if(pm.isCanceled()) {
          throw new InterruptedException();
        }
      }
    }
    finally {
      pm.done();
    }
    
  }

  private static void findSuites(IContainer ires, Set<IFile> results) {
    if(null == ires) {
      return;
    }

    try {
      IResource[] children = ires.members();
      for(IResource res : children) {
        if(res instanceof IFile) {
          if(isTestNgXmlFile((IFile) res)) {
            results.add((IFile) res);
          }
        }
        else {
          findSuites((IContainer) res, results);
        }
      }
    }
    catch(CoreException ce) {
      TestNGPlugin.log(ce);
    }
  }

  /**
   * Find all the resources named fileName.
   */
  public static void findFile(IContainer container, String fileName, Set<String> outResult) {
    IResource[] resources;
    try {
      resources = container.members();
      for (IResource r : resources) {
        if (r.getFullPath().toOSString().endsWith(fileName)) {
          outResult.add(r.getFullPath().toOSString());
        } else if (r instanceof IContainer) {
          findFile((IContainer) r, fileName, outResult);
        }
      }
    } catch (CoreException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }

  /**
   * Collect all the types under the parameter element, which is expected to be either
   * an IJavaProject, an IPackageFragmentRoot or a IPackageFragment.
   */
  public static void collectTypes(Object element,
      IProgressMonitor pm,
      Set<IType> result,
      Filters.ITypeFilter filter) throws CoreException {
    collectTypes(element, pm, result, filter, null);
  }

  public static void collectTypes(Object element,
      IProgressMonitor pm,
      Set<IType> result,
      Filters.ITypeFilter filter,
      String message) throws CoreException {
    element = computeScope(element);
    try {
      if (message == null) {
        message = ResourceUtil.getString("TestSearchEngine.message.searching");  //$NON-NLS-1$
      }

      while((element instanceof ISourceReference) && !(element instanceof ICompilationUnit)) {
        if(element instanceof IType) {
          if(filter.accept((IType) element)) {
            result.add((IType) element);

            return;
          }
        }
        element = ((IJavaElement) element).getParent();
      }

      if(element instanceof ICompilationUnit) {
        ICompilationUnit cu = (ICompilationUnit) element;

        IType[] types = cu.getAllTypes();
        for(int i = 0; i < types.length; i++) {
          pm.worked(1);
          if(filter.accept(types[i])) {
            result.add(types[i]);
          }
        }
      }
      else if (element instanceof IPackageFragmentRoot) {
        // Do this test before instanceof IJavaElement (which is more general)
        IPackageFragmentRoot pfr = (IPackageFragmentRoot) element;
        for (Object javaElement : pfr.getChildren()) {
          collectTypes(javaElement, pm, result, filter);
        }
      }
      else if (element instanceof IPackageFragment) {
        // Do this test before instanceof IJavaElement (which is more general)
        IPackageFragment pfr = (IPackageFragment) element;
        for (Object javaElement : pfr.getChildren()) {
          collectTypes(javaElement, pm, result, filter);
        }
      }
      else if (element instanceof IJavaElement) {
        findTestTypes(pm, ((IJavaElement) element).getJavaProject(), result, filter);
      }
    }
    finally {
//      pm.done();
    }
  }

  public static void findTestTypes(IProgressMonitor pm,
      IJavaElement ije, Set<IType> result, Filters.ITypeFilter filter) {

    if(IJavaElement.PACKAGE_FRAGMENT > ije.getElementType()) {
      try {
        IJavaElement[] children = ((IParent) ije).getChildren();

//        SubMonitor sm = SubMonitor.convert(pm, "Package fragment " + children.length, children.length);
        for(int i = 0; i < children.length; i++) {
          pm.worked(1);
          findTestTypes(pm, children[i], result, filter);
        }
      }
      catch(JavaModelException jme) {
        TestNGPlugin.log(jme);
      }
    }

    if(IJavaElement.PACKAGE_FRAGMENT == ije.getElementType()) {
      try {
        ICompilationUnit[] compilationUnits = ((IPackageFragment) ije).getCompilationUnits();
        for(int i = 0; i < compilationUnits.length; i++) {
          pm.worked(1);
          findTestTypes(pm, compilationUnits[i], result, filter);
        }
      }
      catch(JavaModelException jme) {
        TestNGPlugin.log(jme);
      }

    }

    if(IJavaElement.COMPILATION_UNIT == ije.getElementType()) {
      try {
        IType[] types = ((ICompilationUnit) ije).getAllTypes();

//        SubMonitor sm = SubMonitor.convert(pm, "Compilation unit " + types.length, types.length);
        for(int i = 0; i < types.length; i++) {
          if(filter.accept(types[i])) {
            pm.worked(1);
//            pm.subTask("Type:" + types[i]);
            result.add(types[i]);
          }
        }
      }
      catch(JavaModelException jme) {
        TestNGPlugin.log(jme);
      }
    }
  }
  
  private static void findPackages(IJavaElement ije, Set result) {
		if (IJavaElement.PACKAGE_FRAGMENT > ije.getElementType()) {
			try {
				IJavaElement[] children = ((IParent) ije).getChildren();

				for (int i = 0; i < children.length; i++) {
					findPackages(children[i], result);
				}
			} catch (JavaModelException jme) {
				TestNGPlugin.log(jme);
			}
		}

		if (IJavaElement.PACKAGE_FRAGMENT == ije.getElementType()) {
			try {
				ICompilationUnit[] compilationUnits = ((IPackageFragment) ije)
						.getCompilationUnits();

				for (int i = 0; i < compilationUnits.length; i++) {
					findPackages(compilationUnits[i], result);
				}
			} catch (JavaModelException jme) {
				TestNGPlugin.log(jme);
			}

		}

	    if(IJavaElement.COMPILATION_UNIT == ije.getElementType()) {
	        try {
	          IType[] types = ((ICompilationUnit) ije).getAllTypes();

	          for(int i = 0; i < types.length; i++) {
	            if(Filters.SINGLE_TEST.accept(types[i])) {
	            	IPackageDeclaration[] pkg = ((ICompilationUnit)ije).getPackageDeclarations();
	            	if (pkg.length > 0) {
	            	  result.add(pkg[0].getElementName());
	            	}
	            }
	          }
	        }
	        catch(JavaModelException jme) {
	          TestNGPlugin.log(jme);
	        }
	      }
	}
  

  private static void findMethods(IJavaElement ije, Set result, String className) {
		if (IJavaElement.PACKAGE_FRAGMENT > ije.getElementType()) {
			try {
				IJavaElement[] children = ((IParent) ije).getChildren();
				if (children.length == 0) {return;}
				for (int i = 0; i < children.length; i++) {
					findMethods(children[i], result, className);
				}
			} catch (JavaModelException jme) {
				TestNGPlugin.log(jme);
			}
		}

		if (IJavaElement.PACKAGE_FRAGMENT == ije.getElementType()) {
			try {
				ICompilationUnit[] compilationUnits = ((IPackageFragment) ije)
						.getCompilationUnits();

				if (compilationUnits.length == 0) {return;}
				for (int i = 0; i < compilationUnits.length; i++) {
					findMethods(compilationUnits[i], result, className);
				}
				
			} catch (JavaModelException jme) {
				TestNGPlugin.log(jme);
			}
		}

		if (IJavaElement.COMPILATION_UNIT == ije.getElementType()) {
			try {
				IType[] types = ((ICompilationUnit) ije).getAllTypes();

				for (int i = 0; i < types.length; i++) {
					IType classType;
					if (Filters.SINGLE_TEST.accept(types[i])) {
						if (IJavaElement.TYPE == types[i].getElementType()) {
							classType = types[i];
						}
						else if (IJavaElement.CLASS_FILE == types[i].getElementType()) {
							classType = ((IClassFile)types[i]).findPrimaryType();
						}
						else {
							classType = null;
						}
						
						if (classType != null) {
							if (className.equals("") || classType.getFullyQualifiedName().equals(className)) {
								IMethod[] methods = classType.getMethods();
								for (int j = 0; j < methods.length; j++) {
									if (TypeParser.parseType(classType).isTestMethod(methods[j])) {
										result.add(methods[j].getDeclaringType().getFullyQualifiedName() + "." + methods[j].getElementName());									
									}
								}
							}
						}					
					}
				}
			} catch (JavaModelException jme) {
				TestNGPlugin.log(jme);
			}
		}								
	}
  

  private static Object computeScope(Object element) throws JavaModelException {
    if(element instanceof IResource) {
      element = JavaCore.create((IResource) element);
    }
    if(element instanceof IClassFile) {
      IClassFile cf = (IClassFile) element;
      element = cf.getType();
    }

    return element;
  }

  public static void ppp(String s) {
    System.out.println("[TestSearchEngine] " + s);
  }
}
