/*
 * Copyright (c) 2002 - 2006 IBM Corporation.
 * 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:
 *     IBM Corporation - initial API and implementation
 */
package com.ibm.wala.ide.util;

import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.collections.HashSetFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.resources.IFile;
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.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageDeclaration;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeParameter;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jface.viewers.StructuredSelection;

/** Convenience methods to get information from JDT {@link IJavaElement} model. */
public class JdtUtil {

  public static String getFilePath(IJavaElement javaElt) {
    if (javaElt == null) {
      throw new IllegalArgumentException("javaElt is null");
    }
    try {
      IPath p = javaElt.getPath();
      if (p == null) {
        throw new IllegalArgumentException("javaElt has null path");
      }
      String filePath = p.toString();
      return filePath;
    } catch (Exception e) {
      throw new IllegalArgumentException("malformed javaElt: " + javaElt, e);
    }
  }

  public static String getPackageName(ICompilationUnit cu) {
    if (cu == null) {
      throw new IllegalArgumentException("cu is null");
    }
    try {
      IPackageDeclaration[] pkgDecl = cu.getPackageDeclarations();

      // TODO: handle default package?
      if (pkgDecl != null && pkgDecl.length > 0) {
        String packageName = pkgDecl[0].getElementName();
        return packageName;
      }
    } catch (JavaModelException e) {

    }
    return "";
  }

  public static String getFullyQualifiedClassName(IType type) {
    if (type == null) {
      throw new IllegalArgumentException("type is null");
    }
    ICompilationUnit cu = (ICompilationUnit) type.getParent();
    String packageName = getPackageName(cu);
    String className = type.getElementName();
    String fullyQName = packageName + '.' + className;
    return fullyQName;
  }

  public static String getClassName(IType type) {
    if (type == null) {
      throw new IllegalArgumentException("type is null");
    }
    String className = type.getElementName();
    return className;
  }

  /**
   * Return a unique string representing the specified Java element across projects in the
   * workspace. The returned string can be used as a handle to create JavaElement by
   * 'JavaCore.create(String)'
   *
   * <p>For example, suppose we have the method 'fooPackage.barPackage.FooClass.fooMethod(int)'
   * which is in the 'FooProject' and source folder 'src' the handle would be
   * '=FooProject/src&lt;fooPackage.barPackage{FooClass.java[FooClass~fooMethod~I'
   *
   * @throws IllegalArgumentException if javaElt is null
   */
  public static String getJdtHandleString(IJavaElement javaElt) {
    if (javaElt == null) {
      throw new IllegalArgumentException("javaElt is null");
    }
    return javaElt.getHandleIdentifier();
  }

  @Deprecated
  public static IJavaElement createJavaElementFromJdtHandle(String jdtHandle) {
    return JavaCore.create(jdtHandle);
  }

  public static IType[] getClasses(ICompilationUnit cu) {
    if (cu == null) {
      throw new IllegalArgumentException("cu is null");
    }
    try {
      return cu.getAllTypes();
    } catch (JavaModelException e) {
    }
    return null;
  }

  public static IJavaProject getProject(IJavaElement javaElt) {
    if (javaElt == null) {
      throw new IllegalArgumentException("javaElt is null");
    }
    IJavaProject javaProject = javaElt.getJavaProject();
    return javaProject;
  }

  public static String getProjectName(IJavaProject javaProject) {
    if (javaProject == null) {
      throw new IllegalArgumentException("javaProject is null");
    }
    return javaProject.getElementName();
  }

  /**
   * @param typeSignature Some of the type signatures examples are "QString;" (String) and "I" (int)
   *     The type signatures may be either unresolved (for source types) or resolved (for binary
   *     types), and either basic (for basic types) or rich (for parameterized types). See {@link
   *     Signature} for details.
   */
  public static String getHumanReadableType(String typeSignature) {
    String simpleName = Signature.getSignatureSimpleName(typeSignature);
    return simpleName;
  }

  public static IJavaProject getJavaProject(IFile appJar) {
    if (appJar == null) {
      throw new IllegalArgumentException("appJar is null");
    }
    String projectName = appJar.getProject().getName();
    return getJavaProject(projectName);
  }

  public static IJavaProject getJavaProject(String projectName) {
    if (projectName == null) {
      throw new IllegalArgumentException("null projectName");
    }
    IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
    IJavaModel javaModel = JavaCore.create(workspaceRoot);
    IJavaProject javaProject = javaModel.getJavaProject(projectName);
    return javaProject;
  }

  /** compute the java projects in the active workspace */
  public static Collection<IJavaProject> getWorkspaceJavaProjects() {
    Collection<IJavaProject> result = HashSetFactory.make();
    IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();

    for (IProject p : workspaceRoot.getProjects()) {
      try {
        if (p.hasNature(JavaCore.NATURE_ID)) {
          IJavaProject jp = JavaCore.create(p);
          if (jp != null) {
            result.add(jp);
          }
        }
      } catch (CoreException e) {
        // do nothing
      }
    }
    return result;
  }

  /**
   * Find the {@link IType} in the workspace corresponding to a class name.
   *
   * @return null if not found
   * @throws IllegalArgumentException if projects == null
   */
  public static IType findJavaClassInProjects(
      String fullyQualifiedName, Collection<IJavaProject> projects)
      throws IllegalArgumentException {
    if (projects == null) {
      throw new IllegalArgumentException("projects == null");
    }
    for (IJavaProject project : projects) {
      try {
        IType t = project.findType(fullyQualifiedName);
        if (t != null) {
          return t;
        }
      } catch (JavaModelException e) {
        e.printStackTrace();
      }
    }
    System.err.println("failed to find " + fullyQualifiedName);
    return null;
    // IJavaElement[] arr = new IJavaElement[projects.size()];
    // projects.toArray(arr);
    // IJavaSearchScope scope = SearchEngine.createJavaSearchScope(arr, false);
    //
    // return searchForJavaClass(className, scope);
  }

  public static IType findJavaClassInResources(String className, Collection<IResource> resources) {
    if (resources == null) {
      throw new IllegalArgumentException("null resources");
    }
    Collection<IJavaProject> projects = HashSetFactory.make();
    for (IResource r : resources) {
      projects.add(JavaCore.create(r).getJavaProject());
    }
    return findJavaClassInProjects(className, projects);
  }

  // private static IType searchForJavaClass(String className, IJavaSearchScope scope) {
  // SearchPattern p = SearchPattern.createPattern(className,
  // IJavaSearchConstants.CLASS_AND_INTERFACE,
  // IJavaSearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH);
  // SearchEngine engine = new SearchEngine();
  // final Collection<IJavaElement> kludge = HashSetFactory.make();
  // SearchRequestor requestor = new SearchRequestor() {
  //
  // @Override
  // public void acceptSearchMatch(SearchMatch match) throws CoreException {
  // kludge.add((IJavaElement) match.getElement());
  // }
  //
  // };
  // try {
  // engine.search(p, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }, scope,
  // requestor, null);
  // } catch (CoreException e) {
  // e.printStackTrace();
  // }
  // if (kludge.size() == 1) {
  // System.err.println("Found " + className);
  // return (IType) kludge.iterator().next();
  // } else {
  // System.err.println("Failed to find " + className + " " + kludge.size());
  // return null;
  // }
  // }

  /**
   * Find the IMethod in the workspace corresponding to a method selector.
   *
   * <p>TODO: this is way too slow. figure out something better.
   *
   * @return null if not found
   */
  public static IMethod findJavaMethodInProjects(
      String klass, String selector, Collection<IJavaProject> projects) {

    if (klass == null) {
      throw new IllegalArgumentException("null klass");
    }
    if (projects == null) {
      throw new IllegalArgumentException("null projects");
    }
    IType type = null;
    try {
      type = findJavaClassInProjects(klass, projects);
    } catch (Throwable t) {
      return null;
    }
    if (type == null) {
      return null;
    }
    String name = parseForName(selector, type);
    String[] paramTypes = parseForParameterTypes(selector);
    IMethod m = type.getMethod(name, paramTypes);
    IMethod[] methods = type.findMethods(m);
    if (methods != null && methods.length == 1) {
      return methods[0];
    } else {
      // methods is null. probably got screwed by generics.
      // i spent 5 hours trying to figure out how to fix this correctly
      // and failed. implementing a miserable hack instead.
      // Need to consult a guru to figure out how to do this.
      try {
        List<IMethod> matches = new ArrayList<>();
        Collection<String> typeParameterNames = getTypeParameterNames(type);
        METHODS:
        for (IMethod x : type.getMethods()) {
          if (x.getElementName().equals(name)) {
            if (x.getParameterTypes().length == paramTypes.length) {
              for (int i = 0; i < x.getParameterTypes().length; i++) {
                String s1 =
                    Signature.getTypeErasure(
                        Signature.getSignatureSimpleName(x.getParameterTypes()[i]));
                String s2 =
                    Signature.getTypeErasure(Signature.getSignatureSimpleName(paramTypes[i]));
                if (typeParameterNames.contains(s1)) {
                  // s1 is a type parameter to the class. optimistically assume
                  // the types match.
                } else {
                  if (!s1.equals(s2)) {
                    // no match
                    continue METHODS;
                  }
                }
              }
              matches.add(x);
            }
          }
        }
        if (matches.size() == 1) {
          return matches.get(0);
        } else {
          System.err.println("findJavaMethodInWorkspace FAILED TO MATCH " + m);
          return null;
        }
      } catch (JavaModelException e) {
        e.printStackTrace();
        return null;
      }
    }
  }

  public static Collection<String> getTypeParameterNames(IType type)
      throws IllegalArgumentException, JavaModelException {
    if (type == null) {
      throw new IllegalArgumentException("type == null");
    }
    ITypeParameter[] tp = type.getTypeParameters();
    Collection<String> typeParameterNames = HashSetFactory.make(tp.length);
    for (ITypeParameter p : tp) {
      typeParameterNames.add(p.getElementName());
    }
    return typeParameterNames;
  }

  public static String parseForName(String selector, IType type) {
    if (selector == null) {
      throw new IllegalArgumentException("selector is null");
    }
    try {
      String result = selector.substring(0, selector.indexOf('('));
      if (result.equals("<init>")) {
        return type.getElementName();
      } else {
        return result;
      }
    } catch (StringIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid selector: " + selector, e);
    }
  }

  public static final String[] parseForParameterTypes(String selector)
      throws IllegalArgumentException {

    try {
      if (selector == null) {
        throw new IllegalArgumentException("selector is null");
      }
      String d = selector.substring(selector.indexOf('('));
      if (d.length() <= 2) {
        throw new IllegalArgumentException("invalid descriptor: " + d);
      }
      if (d.charAt(0) != '(') {
        throw new IllegalArgumentException("invalid descriptor: " + d);
      }

      ArrayList<String> sigs = new ArrayList<>(10);

      int i = 1;
      while (true) {
        switch (d.charAt(i++)) {
          case TypeReference.VoidTypeCode:
            sigs.add(TypeReference.VoidName.toString());
            continue;
          case TypeReference.BooleanTypeCode:
            sigs.add(TypeReference.BooleanName.toString());
            continue;
          case TypeReference.ByteTypeCode:
            sigs.add(TypeReference.ByteName.toString());
            continue;
          case TypeReference.ShortTypeCode:
            sigs.add(TypeReference.ShortName.toString());
            continue;
          case TypeReference.IntTypeCode:
            sigs.add(TypeReference.IntName.toString());
            continue;
          case TypeReference.LongTypeCode:
            sigs.add(TypeReference.LongName.toString());
            continue;
          case TypeReference.FloatTypeCode:
            sigs.add(TypeReference.FloatName.toString());
            continue;
          case TypeReference.DoubleTypeCode:
            sigs.add(TypeReference.DoubleName.toString());
            continue;
          case TypeReference.CharTypeCode:
            sigs.add(TypeReference.CharName.toString());
            continue;
          case TypeReference.ArrayTypeCode:
            {
              int off = i - 1;
              while (d.charAt(i) == TypeReference.ArrayTypeCode) {
                ++i;
              }
              if (d.charAt(i++) == TypeReference.ClassTypeCode) {
                while (d.charAt(i++) != ';') ;
                sigs.add(d.substring(off, i).replaceAll("/", "."));
              } else {
                sigs.add(d.substring(off, i));
              }
              continue;
            }
          case (byte) ')': // end of parameter list
            return toArray(sigs);
          default:
            {
              // a class
              int off = i - 1;
              char c;
              do {
                c = d.charAt(i++);
              } while (c != ',' && c != ')');
              sigs.add('L' + d.substring(off, i - 1) + ';');

              if (c == ')') {
                return toArray(sigs);
              }

              continue;
            }
        }
      }
    } catch (StringIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("error parsing selector " + selector, e);
    }
  }

  private static String[] toArray(ArrayList<String> sigs) {
    int size = sigs.size();
    if (size == 0) {
      return new String[0];
    }
    Iterator<String> it = sigs.iterator();
    String[] result = new String[size];
    for (int j = 0; j < size; j++) {
      result[j] = it.next();
    }
    return result;
  }

  /**
   * Find the IMethod in the workspace corresponding to a method signature.
   *
   * <p>This doesn't work for elements declared in inner classes. It's possible this is a 3.2 bug
   * fixed in 3.3
   *
   * @return null if not found
   */
  @Deprecated
  public static IMethod findJavaMethodInWorkspaceBrokenForInnerClasses(String methodSig) {
    // dammit ... this doesn't work for inner classes.

    System.err.println("Search for " + methodSig);
    SearchPattern p =
        SearchPattern.createPattern(
            methodSig,
            IJavaSearchConstants.METHOD,
            IJavaSearchConstants.DECLARATIONS,
            SearchPattern.R_EXACT_MATCH);
    IJavaSearchScope scope = SearchEngine.createWorkspaceScope();
    SearchEngine engine = new SearchEngine();
    final Collection<IJavaElement> kludge = HashSetFactory.make();
    SearchRequestor requestor =
        new SearchRequestor() {

          @Override
          public void acceptSearchMatch(SearchMatch match) throws CoreException {
            kludge.add((IJavaElement) match.getElement());
          }
        };
    try {
      engine.search(
          p,
          new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()},
          scope,
          requestor,
          null);
    } catch (CoreException e) {
      e.printStackTrace();
    }
    if (kludge.size() == 1) {
      return (IMethod) kludge.iterator().next();
    } else {
      System.err.println("RETURNED " + kludge.size() + ' ' + kludge);
      return null;
    }
  }

  /** Use the search engine to find all methods in a java element */
  public static Collection<IMethod> findMethods(IJavaElement elt) {

    if (elt instanceof ICompilationUnit) {
      Collection<IMethod> result = HashSetFactory.make();
      for (IType type : getClasses((ICompilationUnit) elt)) {
        try {
          result.addAll(Arrays.asList(type.getMethods()));
        } catch (JavaModelException e) {
          e.printStackTrace();
        }
      }
      return result;
    } else {
      final Collection<IMethod> result = HashSetFactory.make();
      SearchPattern p =
          SearchPattern.createPattern(
              "*",
              IJavaSearchConstants.METHOD,
              IJavaSearchConstants.DECLARATIONS,
              SearchPattern.R_PATTERN_MATCH);
      SearchPattern p2 =
          SearchPattern.createPattern(
              "*",
              IJavaSearchConstants.CONSTRUCTOR,
              IJavaSearchConstants.DECLARATIONS,
              SearchPattern.R_PATTERN_MATCH);
      IJavaSearchScope scope =
          SearchEngine.createJavaSearchScope(new IJavaElement[] {elt}, IJavaSearchScope.SOURCES);
      SearchEngine engine = new SearchEngine();
      SearchRequestor requestor =
          new SearchRequestor() {
            @Override
            public void acceptSearchMatch(SearchMatch match) throws CoreException {
              if (match.getElement() instanceof IMethod) {
                result.add((IMethod) match.getElement());
              }
            }
          };
      try {
        engine.search(
            p,
            new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()},
            scope,
            requestor,
            null);
        engine.search(
            p2,
            new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()},
            scope,
            requestor,
            null);
      } catch (CoreException e) {
        e.printStackTrace();
      }

      return result;
    }
  }

  /** get a {@link StructuredSelection} corresponding to the named projects */
  public static StructuredSelection getStructuredSelectionForProjectNames(
      Collection<String> projectNames) {
    if (projectNames == null) {
      throw new IllegalArgumentException("null projectNames");
    }
    Object[] projects = new Object[projectNames.size()];
    int i = 0;
    for (String projectName : projectNames) {
      projects[i++] = getJavaProject(projectName);
    }
    StructuredSelection selection = new StructuredSelection(projects);
    return selection;
  }

  public static IJavaProject getNamedProject(String projectName) {
    IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
    IJavaModel javaModel = JavaCore.create(workspaceRoot);
    IJavaProject helloWorldProject = javaModel.getJavaProject(projectName);
    return helloWorldProject;
  }

  public static ASTNode getAST(IFile javaSourceFile) {
    ASTParser parser = ASTParser.newParser(AST.JLS3);
    parser.setSource(JavaCore.createCompilationUnitFrom(javaSourceFile));
    parser.setProject(JavaCore.create(javaSourceFile.getProject()));
    parser.setResolveBindings(true);
    return parser.createAST(new NullProgressMonitor());
  }

  public static ASTNode getOriginalNode(JdtPosition pos) {
    ASTNode root = getAST(pos.getEclipseFile());
    return getOriginalNode(root, pos);
  }

  public static ASTNode getOriginalNode(ASTNode root, JdtPosition pos) {
    return NodeFinder.perform(
        root, pos.getFirstOffset(), pos.getLastOffset() - pos.getFirstOffset());
  }
}
