package jetbrains.buildServer.eclipse.agent.builder.ant;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import jetbrains.buildServer.eclipse.java.BuildDescriptor;
import jetbrains.buildServer.eclipse.java.IClasspathContainer;
import jetbrains.buildServer.eclipse.java.IClasspathEntry;
import jetbrains.buildServer.eclipse.java.JDTConstants;
import jetbrains.buildServer.eclipse.java.JDTUtil;
import jetbrains.buildServer.eclipse.java.PlatformDescriptor;

import org.jetbrains.annotations.NotNull;

public class EclipseClasspath {

  protected List<String> srcDirs = new ArrayList<String>();
  protected List<String> classDirs = new ArrayList<String>();
  protected List<List<String>> inclusionLists = new ArrayList<List<String>>();
  protected List<List<String>> exclusionLists = new ArrayList<List<String>>();

  protected Map<String, String> variable2valueMap = new LinkedHashMap<String, String>();
  protected List<String> rawClassPathEntries = new ArrayList<String>();
  protected List<String> rawClassPathEntriesAbsolute = new ArrayList<String>();

  private File/*IJavaProject*/project;
  @SuppressWarnings("unused")
  private PlatformDescriptor platform;
  private BuildDescriptor build;

  private static Map<String, IClasspathContainer> userLibraryCache = new HashMap<String, IClasspathContainer>();

  /**
   * Initialize object with classpath of given project.
   * 
   * @param platform
   * @param collection
   * @param unresolvedLibs
   */
  public EclipseClasspath(final @NotNull PlatformDescriptor platform, final @NotNull BuildDescriptor build, final @NotNull File project, final @NotNull Collection<String> unresolvedLibs, final @NotNull Collection<String> unresolvedVars) throws /*JavaModel*/Exception {
    //      this.project = project;
    this.platform = platform;
    this.build = build;
    this.project = project;
    handle(JDTUtil.getRawClasspath(project)/*.getRawClasspath()*/, unresolvedLibs, unresolvedVars);
  }

  //  /**
  //   * Initialize object with runtime classpath of given launch configuration.
  //   * @param project    project that contains given launch configuration conf
  //   * @param conf       launch configuration
  //   * @param bootstrap  if true only bootstrap entries are added, if false only
  //   *                   non-bootstrap entries are added  
  //   */
  //  public EclipseClasspath(File /*IJavaProject */project, /*ILaunchConfiguration conf,*/ boolean bootstrap)
  //      throws /*Core*/Exception
  //  {
  //      this.project = project;
  //      
  //      // convert IRuntimeClasspathEntry to IClasspathEntry
  //      IRuntimeClasspathEntry[] runtimeEntries;
  //      // see AbstractJavaLaunchConfigurationDelegate
  //      runtimeEntries = JavaRuntime.computeUnresolvedRuntimeClasspath(conf);
  //      List classpathEntries = new ArrayList(runtimeEntries.length);
  //      for (int i = 0; i < runtimeEntries.length; i++)
  //      {
  //          IRuntimeClasspathEntry entry = runtimeEntries[i];
  //          if (  bootstrap && (entry.getClasspathProperty()  == IRuntimeClasspathEntry.BOOTSTRAP_CLASSES) ||
  //              ! bootstrap && (entry.getClasspathProperty()  != IRuntimeClasspathEntry.BOOTSTRAP_CLASSES))
  //          {
  //              // NOTE: See AbstractJavaLaunchConfigurationDelegate.getBootpathExt()
  //              //       for an alternate bootclasspath detection
  //              if (entry.getClass().getName().equals("org.eclipse.jdt.internal.launching.VariableClasspathEntry")) //$NON-NLS-1$
  //              {
  //                  IClasspathEntry e = convertVariableClasspathEntry(entry);
  //                  if (e != null)
  //                  {
  //                      classpathEntries.add(e);
  //                  }
  //              }
  //              else if (entry.getClass().getName().equals("org.eclipse.jdt.internal.launching.DefaultProjectClasspathEntry")) //$NON-NLS-1$
  //              {
  //                  IClasspathEntry e = JavaCore.newProjectEntry(entry.getPath());
  //                  classpathEntries.add(e);
  //              }
  //              else if (entry.getClasspathEntry() != null)
  //              {
  //                  classpathEntries.add(entry.getClasspathEntry());
  //              }               
  //          }
  //          else if (bootstrap && entry.toString().startsWith(JavaRuntime.JRE_CONTAINER))
  //          {
  //              classpathEntries.add(entry.getClasspathEntry());
  //          }
  //          else if (bootstrap && entry.toString().startsWith(JavaCore.USER_LIBRARY_CONTAINER_ID))
  //          {
  //              classpathEntries.add(entry.getClasspathEntry());
  //          }
  //      }
  //      IClasspathEntry[] entries =
  //          (IClasspathEntry[]) classpathEntries.toArray(new IClasspathEntry[classpathEntries.size()]);
  //
  //      handle(entries); 
  //  }

  private void handle(IClasspathEntry[] entries, Collection<String> unresolvedLibs, Collection<String> unresolvedVars) throws /*JavaModel*/Exception {
    for (int i = 0; i < entries.length; i++) {
      handleSources(entries[i]);
      unresolvedVars.addAll(handleVariables(entries[i]));
      handleJars(entries[i]);
      unresolvedLibs.addAll(handleLibraries(entries[i]));
      handleProjects(entries[i]);
    }
  }

  private void handleSources(IClasspathEntry entry) throws /*JavaModel*/Exception {
    String projectRoot = JDTUtil.getProjectRoot(project);
    final File outputLocation = JDTUtil.getOutputLocation(project);
    String defaultClassDir = outputLocation.getPath();// project.getOutputLocation().toString();
    String defaultClassDirAbsolute = JDTUtil.resolve(JDTUtil.getOutputLocation(project)/*project.getOutputLocation()*/);

    if (entry.getContentKind() == JDTConstants.IPackageFragmentRoot_K_SOURCE && entry.getEntryKind() == JDTConstants.IClasspathEntry_CPE_SOURCE) {
      // found source path
      File/*IPath*/srcDirPath = entry.getPath();
      File/*IPath*/classDirPath = entry.getOutputLocation();
      String srcDir = handleLinkedResource(srcDirPath);
      JDTUtil.removeProjectRoot((srcDirPath != null) ? srcDirPath.toString() : projectRoot, project/*.getProject()*/);
      String classDir = JDTUtil.removeProjectRoot((classDirPath != null) ? classDirPath.toString() : defaultClassDir, project/*.getProject()*/);
      srcDirs.add(srcDir);
      classDirs.add(classDir);
      String classDirAbsolute = (classDirPath != null) ? JDTUtil.resolve(classDirPath) : defaultClassDirAbsolute;
      rawClassPathEntries.add(classDir);
      rawClassPathEntriesAbsolute.add(classDirAbsolute);
      String/*IPath*/[] inclusions = entry.getInclusionPatterns();
      List<String> inclusionList = new ArrayList<String>();
      for (int j = 0; j < inclusions.length; j++) {
        if (inclusions[j] != null) {
          inclusionList.add(JDTUtil.removeProjectRoot(inclusions[j].toString(), project/*.getProject()*/));
        }
      }
      inclusionLists.add(inclusionList);
      String/*IPath*/[] exclusions = entry.getExclusionPatterns();
      List<String> exclusionList = new ArrayList<String>();
      for (int j = 0; j < exclusions.length; j++) {
        if (exclusions[j] != null) {
          exclusionList.add(JDTUtil.removeProjectRoot(exclusions[j].toString(), project/*.getProject()*/));
        }
      }
      exclusionLists.add(exclusionList);
    }
  }

  /**
   * Check if given source path is a linked resource. Add values to
   * {@link #variable2valueMap} accordingly.
   * 
   * @param srcDirPath
   *          source dir as IPath
   * @return source directory with reference, e.g. ${MYPATH}/src, if it is no
   *         link, orginal source dir is returned
   */
  private String handleLinkedResource(final File/*IPath*/srcDirPath) {
    String projectRoot = JDTUtil.getProjectRoot(project);
    String srcDir = JDTUtil.removeProjectRoot((srcDirPath != null) ? srcDirPath.toString() : projectRoot, project/*.getProject()*/);
    if (srcDirPath == null) {
      return srcDir;
    }
    //      IFile file;
    //      try
    //      {
    //          file = ResourcesPlugin.getWorkspace().getRoot().getFile(srcDirPath);
    //      }
    //      catch (IllegalArgumentException e)
    //      {
    //          return srcDir;
    //      }
    //TODO: perhaps should not support Linked Resources?      
    //      if (file.isLinked())
    //      {
    //          String pathVariable = file.getRawLocation().segment(0).toString();
    //          IPath pathVariableValue = file.getWorkspace().getPathVariableManager().getValue(pathVariable);
    //          if (pathVariableValue != null)
    //          {
    //              // path variable was used
    //              String pathVariableExtension = file.getRawLocation().removeFirstSegments(1).toString(); // Bug 192726
    //              String relativePath = ExportUtil.getRelativePath(pathVariableValue.toString(),
    //                      projectRoot);
    //              variable2valueMap.put(pathVariable + ".pathvariable", relativePath); //$NON-NLS-1$
    //              variable2valueMap.put(srcDir + ".link", //$NON-NLS-1$
    //                      "${" + pathVariable + ".pathvariable}/" + pathVariableExtension); //$NON-NLS-1$  //$NON-NLS-2$
    //          }
    //          else
    //          {
    //              String relativePath = ExportUtil.getRelativePath(file.getLocation() + "", //$NON-NLS-1$
    //                      projectRoot);
    //              variable2valueMap.put(srcDir + ".link", relativePath); //$NON-NLS-1$
    //          }
    //          srcDir = "${" + srcDir + ".link}"; //$NON-NLS-1$ //$NON-NLS-2$
    //      }
    return srcDir;
  }

  private void handleJars(IClasspathEntry entry) {
    if (entry.getContentKind() == JDTConstants.IPackageFragmentRoot_K_BINARY && entry.getEntryKind() == JDTConstants.IClasspathEntry_CPE_LIBRARY) {
      String jarFile = entry.getPath().toString();
      StringBuffer jarFileBuffer = new StringBuffer();
      StringBuffer jarFileAbsoluteBuffer = new StringBuffer();
      String jarFileAbsolute = JDTUtil.resolve(entry.getPath());
      if (jarFileAbsolute == null) {
        jarFileAbsolute = jarFile; // jarFile was already absolute
        if (handleSubProjectClassesDirectory(jarFile, jarFileBuffer, jarFileAbsoluteBuffer)) {
          jarFile = jarFileBuffer.toString();
          jarFileAbsolute = jarFileAbsoluteBuffer.toString();
        }
      }
      String jarFileOld = jarFile;
      jarFile = JDTUtil.removeProjectRoot(jarFile, project/*.getProject()*/);
      if (jarFile.equals(jarFileOld)) {
        if (handleSubProjectClassesDirectory(jarFile, jarFileBuffer, jarFileAbsoluteBuffer)) {
          jarFile = jarFileBuffer.toString();
          jarFileAbsolute = jarFileAbsoluteBuffer.toString();
        }
      }
      rawClassPathEntries.add(jarFile);
      rawClassPathEntriesAbsolute.add(jarFileAbsolute);
    }
  }

  /**
   * Checks if file is a class directory of a subproject and fills string
   * buffers with resolved values.
   * 
   * @param file
   *          file to check
   * @param jarFile
   *          filled with file location with variable reference
   *          ${project.location}, which is also added to variable2valueMap
   * @param jarFileAbsolute
   *          filled with absolute file location
   * @return true if file is a classes directory
   */
  private boolean handleSubProjectClassesDirectory(String file, StringBuffer jarFile, StringBuffer jarFileAbsolute) {
    // class directory of a subproject?
    if (file != null && file.indexOf('/') == 0) {
      int i = file.indexOf('/', 1);
      i = (i != -1) ? i : file.length();
      String subproject = file.substring(1, i);
      File/*IJavaProject*/javaproject = JDTUtil.getJavaProjectByName(subproject);
      if (javaproject != null) {
        jarFile.setLength(0);
        jarFileAbsolute.setLength(0);
        String location = JDTUtil.getProjectName(javaproject)/*javaproject.getProject().getName()*/+ ".location"; //$NON-NLS-1$
        jarFileAbsolute.append(JDTUtil.replaceProjectRoot(file, javaproject/*.getProject()*/, JDTUtil.getProjectRoot(javaproject)));
        jarFile.append(JDTUtil.replaceProjectRoot(file, javaproject/*.getProject()*/, "${" + location + "}")); //$NON-NLS-1$ //$NON-NLS-2$
        String projectRoot = JDTUtil.getProjectRoot(project);
        String relativePath = JDTUtil.getRelativePath(JDTUtil.getProjectRoot(javaproject), projectRoot);
        variable2valueMap.put(location, relativePath);
        return true;
      }
    }
    return false;
  }

  private Collection<String> handleVariables(IClasspathEntry entry) {
    final ArrayList<String> unresolved = new ArrayList<String>();
    if (entry.getContentKind() == JDTConstants.IPackageFragmentRoot_K_SOURCE && entry.getEntryKind() == JDTConstants.IClasspathEntry_CPE_VARIABLE) {
      // found variable
      String e = entry.getPath().toString();
      int index = e.indexOf('/');
      if (index == -1) {
        index = e.indexOf('\\');
      }
      String variable = e;
      String path = ""; //$NON-NLS-1$
      if (index != -1) {
        variable = e.substring(0, index);
        path = e.substring(index);
      }
      File/*IPath*/value = JDTUtil./*JavaCore.*/getClasspathVariable(variable);
      if (value != null) {
        String projectRoot = JDTUtil.getProjectRoot(project);
        String relativePath = JDTUtil.getRelativePath(value.toString(), projectRoot);
        variable2valueMap.put(variable, relativePath);
      } else if (variable2valueMap.get(variable) == null) {
        // only add empty value, if variable is new 
        variable2valueMap.put(variable, ""); //$NON-NLS-1$
        unresolved.add(variable);
      }
      rawClassPathEntriesAbsolute.add(value + path);
      rawClassPathEntries.add("${" + variable + "}" + path); //$NON-NLS-1$ //$NON-NLS-2$
    }
    return unresolved;
  }

  private Collection<String> handleLibraries(IClasspathEntry entry) throws /*JavaModel*/Exception {
    final ArrayList<String> unresolved = new ArrayList<String>();
    if (entry.getContentKind() == JDTConstants.IPackageFragmentRoot_K_SOURCE && entry.getEntryKind() == JDTConstants.IClasspathEntry_CPE_CONTAINER) {
      // found library
      IClasspathContainer container = JDTUtil/*JavaCore*/.getClasspathContainer(entry.getPath(), platform, build, project);
      if (container == null) {
        // jar missing (project not compile clean)
        unresolved.add(entry.getPath().toString());
        return unresolved;
      }
      String jar = entry.getPath().toString();
      String refName;
      if (jar.startsWith(JDTConstants.JavaRuntime_JRE_CONTAINER)) {
        // JRE System Library
        refName = "${jre.container}"; //$NON-NLS-1$
      } else if (jar.startsWith(JDTConstants.JavaCore_USER_LIBRARY_CONTAINER)) {
        // User Library
        String libraryName = container.getDescription();
        refName = "${" + libraryName + ".userclasspath}"; //$NON-NLS-1$ //$NON-NLS-2$
        if (container.getKind() == IClasspathContainer.K_SYSTEM) {
          refName = "${" + libraryName + ".bootclasspath}"; //$NON-NLS-1$ //$NON-NLS-2$
        }
      } else {
        // Library dependencies: e.g. Plug-in Dependencies
        String libraryName = container.getDescription();
        refName = "${" + libraryName + ".libraryclasspath}"; //$NON-NLS-1$ //$NON-NLS-2$
      }
      userLibraryCache.put(refName, container);
      srcDirs.add(refName);
      classDirs.add(refName);
      rawClassPathEntries.add(refName);
      rawClassPathEntriesAbsolute.add(refName);
      inclusionLists.add(new ArrayList<String>());
      exclusionLists.add(new ArrayList<String>());
    }
    return unresolved;
  }

  private void handleProjects(IClasspathEntry entry) {
    if (entry.getContentKind() == JDTConstants.IPackageFragmentRoot_K_SOURCE && entry.getEntryKind() == JDTConstants.IClasspathEntry_CPE_PROJECT) {
      // found required project on build path
      String subProjectRoot = entry.getPath().toString();
      File/*IJavaProject*/subProject = JDTUtil.getJavaProject(subProjectRoot);
      if (subProject == null) {
        // project was not loaded in workspace
        System.err.println(/*);AntUIPlugin.log(*/"project is not loaded in workspace: " + subProjectRoot/*, null*/); //$NON-NLS-1$
        return;
      }
      // only add an indicator that this is a project reference
      String classpathRef = "${" + JDTUtil.getProjectName(subProject)/*subProject.getProject().getName()*/+ ".classpath}"; //$NON-NLS-1$ //$NON-NLS-2$
      srcDirs.add(classpathRef);
      classDirs.add(classpathRef);
      rawClassPathEntries.add(classpathRef);
      rawClassPathEntriesAbsolute.add(classpathRef);
      inclusionLists.add(new ArrayList<String>());
      exclusionLists.add(new ArrayList<String>());
    }
  }

  //  /**
  //   * Get runtime classpath items for given project separated with path separator.
  //   */
  //  public static String getClasspath(IJavaProject project) throws /*Core*/Exception
  //  {
  //      List items = getClasspathList(project);
  //      return ExportUtil.toString(items, File.pathSeparator);
  //  }
  //
  //  /**
  //   * Get runtime classpath items for given project.
  //   */
  //  public static List getClasspathList(IJavaProject project) throws /*Core*/Exception
  //  {
  //      String[] classpath = JavaRuntime.computeDefaultRuntimeClassPath(project);
  //      return Arrays.asList(classpath);
  //  }

  /**
   * Check if given string is a reference.
   */
  public static boolean isReference(String s) {
    return isProjectReference(s) || isUserLibraryReference(s) || isUserSystemLibraryReference(s) || isLibraryReference(s) || isJreReference(s);
    // NOTE: A linked resource is no reference
  }

  /**
   * Check if given string is a project reference.
   */
  public static boolean isProjectReference(String s) {
    return s.startsWith("${") && s.endsWith(".classpath}"); //$NON-NLS-1$ //$NON-NLS-2$ 
  }

  /**
   * Resolves given project reference to a project.
   * 
   * @return <code>null</code> if project is not resolvable
   */
  public static File/*IJavaProject*/resolveProjectReference(String s) {
    String name = JDTUtil.removePrefixAndSuffix(s, "${", ".classpath}"); //$NON-NLS-1$ //$NON-NLS-2$
    return JDTUtil.getJavaProjectByName(name);
  }

  /**
   * Check if given string is a user library reference.
   */
  public static boolean isUserLibraryReference(String s) {
    return s.startsWith("${") && s.endsWith(".userclasspath}"); //$NON-NLS-1$ //$NON-NLS-2$ 
  }

  /**
   * Check if given string is a user system library reference. This library is
   * added to the compiler boot classpath.
   */
  public static boolean isUserSystemLibraryReference(String s) {
    return s.startsWith("${") && s.endsWith(".bootclasspath}"); //$NON-NLS-1$ //$NON-NLS-2$ 
  }

  /**
   * Check if given string is a library reference. e.g. Plug-in dependencies are
   * library references.
   * 
   */
  public static boolean isLibraryReference(String s) {
    return s.startsWith("${") && s.endsWith(".libraryclasspath}"); //$NON-NLS-1$ //$NON-NLS-2$ 
  }

  /**
   * Check if given string is a JRE reference.
   */
  public static boolean isJreReference(String s) {
    return s.equals("${jre.container}"); //$NON-NLS-1$
  }

  /**
   * Resolves given user (system) library or plugin reference to its container.
   * 
   * <p>
   * NOTE: The library can only be resolved if an EclipseClasspath object was
   * created which had a reference to this library. The class holds an internal
   * cache to circumvent that UserLibraryManager is an internal class.
   * 
   * @return null if library is not resolvable
   */
  public static IClasspathContainer resolveUserLibraryReference(String s) {
    return /*(IClasspathContainer) */userLibraryCache.get(s);
  }

  /**
   * Check if given string is a linked resource.
   * 
   */
  public static boolean isLinkedResource(String s) {
    return s.startsWith("${") && s.endsWith(".link}"); //$NON-NLS-1$ //$NON-NLS-2$ 
  }

  /**
   * Get source folder name of a linked resource.
   * 
   * @see #isLinkedResource(String)
   */
  public static String getLinkedResourceName(String s) {
    return JDTUtil.removePrefixAndSuffix(s, "${", ".link}"); //$NON-NLS-1$ //$NON-NLS-2$ 
  }

  /**
   * Resolves given linked resource to an absolute file location.
   */
  public String resolveLinkedResource(String s) {
    String name = JDTUtil.removePrefixAndSuffix(s, "${", "}"); //$NON-NLS-1$ //$NON-NLS-2$
    String value = (String) variable2valueMap.get(name);
    String suffix = ".pathvariable}"; //$NON-NLS-1$
    int i = value.indexOf(suffix);
    if (i != -1) {
      // path variable
      String pathVariable = value.substring(0, i + suffix.length() - 1);
      pathVariable = JDTUtil.removePrefix(pathVariable, "${"); //$NON-NLS-1$
      return (String) variable2valueMap.get(pathVariable) + value.substring(i + suffix.length());
    }
    return value;
  }

  /**
   * Convert a VariableClasspathEntry to a IClasspathEntry.
   * 
   * <p>
   * This is a workaround as entry.getClasspathEntry() returns null.
   */
  //  private IClasspathEntry convertVariableClasspathEntry(IRuntimeClasspathEntry entry)
  //  {
  //      try
  //      {
  //          Document doc = ExportUtil.parseXmlString(entry.getMemento());
  //          Element element = (Element) doc.getElementsByTagName("memento").item(0); //$NON-NLS-1$
  //          String variableString = element.getAttribute("variableString"); //$NON-NLS-1$
  //          ExportUtil.addVariable(variable2valueMap, variableString, ExportUtil.getProjectRoot(project));
  //          // remove ${...} from string to be conform for handleVariables()
  //          variableString = ExportUtil.removePrefix(variableString, "${");//$NON-NLS-1$
  //          int i = variableString.indexOf('}');
  //          if (i != -1)
  //          {
  //              variableString = variableString.substring(0, i)
  //                  + variableString.substring(i + 1);                
  //          }
  //          IPath path = new Path(variableString);
  //          return JavaCore.newVariableEntry(path, null, null);
  //      }
  //      catch (Exception e)
  //      {
  //          AntUIPlugin.log(e);
  //          return null;
  //      }
  //
  //  }

}
