/**
 * Copyright (c) 2005-2009 IBM Corporation and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 * 
 * Contributors: 
 *   IBM - Initial API and implementation
 */
package org.eclipse.emf.codegen.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.runtime.NullProgressMonitor;

import org.eclipse.emf.common.EMFPlugin;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ImportDeclaration;

/**
 * A manager for import declarations in generated code.
 * 
 * <p>An instance of <code>ImportManager</code> should be created for each compilation unit being generated. It
 * maintains the list of imports to be added to the compilation unit and associates registered <b>short names</b> with
 * their corresponding <b>qualified names</b>.
 * 
 * <p>Common usage of <code>ImportManager</code> is very simple. This example assumes that a <code>StringBuilder</code>
 * is being used to accumulate the text for a generated source file:
 * 
 * <pre>
 * StringBuilder result = new StringBuilder();
 * ImportManager importManager = new ImportManager("com.example.target", "Target");
 * ...
 * importManager.markImportLocation(result);
 * ...
 * result.append(importManager.getImportedName("com.example.lib.Example", true));
 * ...
 * result.append(importManager.getImportedName("java.lang.String", true));
 * ...
 * result.append(importManager.getImportedName("java.util.Arrays", true));
 * ...
 * result.append(importManager.getImportedName("org.test.Example", true));
 * ...
 * result.append(importManager.getImportedName("org.test.Target", true));
 * ...
 * result.append(importManager.getImportedName("com.example.target.Helper", true));
 * ...
 * importManager.emitSortedImports();</pre>
 * 
 * <p>The constructor is passed the package name and short name for the compilation unit being generated. The point
 * where the imports should be inserted is marked by calling <code>markImportLocation</code>. Then,
 * <code>getImportedName()</code> is called each time a type name is needed, to determine the correct name to use.
 * Passing <code>true</code> as the second argument instructs the import manager to automatically import the specified
 * type, if possible.
 * 
 * <p>In this case, the following names would be returned:
 * 
 * <ul>
 * <li><code>Example</code>
 * <li><code>String</code>
 * <li><code>Arrays</code>
 * <li><code>org.test.Example</code>
 * <li><code>org.test.Target</code>
 * <li><code>Helper</code>
 * </ul>
 * 
 * Note that <code>org.test.Example</code> cannot be shortened because the short name <code>Example</code> is already
 * taken. Similarly, <code>Target</code> is already taken by the compilation unit, itself. <code>Helper</code> and
 * <code>String</code> are shortened, but they don't actually require imports.
 * 
 * <p>Finally, the needed import declarations are inserted by calling <code>emitSortedImports()</code>. In this case,
 * only two import declarations are produced:
 * 
 * <pre>
 * import com.example.lib.Example;
 *
 * import java.util.Arrays;</pre>
 * 
 * <p>In addition to auto-importing, <code>ImportManager</code> supports explicit pre-registration of individual and
 * wildcard imports via <code>addImport()</code>.
 * 
 * <p><a name="inner_types">
 * <code>ImportManager</code> provides special handling for inner types, which are specified using <code>$</code>
 * instead of dot in the qualified name. Note that this means that <code>ImportManager</code> does <em>not</em> support
 * the use of <code>$</code> as a character within a class name, although this is allowed by the Java language. For
 * example:
 * 
 * <pre>
 * result.append(importManager.getImportedName("com.example.lib.Outer$Inner", true));</pre>
 * 
 * <p>This imports <code>com.example.lib.Outer</code> and returns <code>Outer.Inner</code>. Multiple levels of nested
 * classes are supported, but <code>$</code> <em>must</em> be used as the separator between all of them. A qualified
 * name with any dots following the first <code>$</code> is not allowed.
 * 
 * <p>Note that it is also possible, instead, to use the simple dot notation for an Outer class, in which case the
 * containing class names will be treated as part of the package name and a more specific import will result. For
 * example:
 * 
 * <pre>
 * result.append(importManager.getImportedName("com.example.lib.Outer.Inner", true));</pre>
 * 
 * <p>This imports <code>com.example.lib.Outer.Inner</code> and returns <code>Inner</code>. Later, if the <code>$</code>
 * notation is used, it can match an import of this type:
 * 
 * <pre>
 * result.append(importManager.getImportedName("com.example.lib.Outer$Inner", false));</pre>
 * 
 * <p>This will use the existing import and return <code>Inner</code>. If, however, the second argument had been
 * <code>true</code> it would have imported <code>Outer</code> before getting the chance to consider <code>Inner</code>.
 *
 * <p><code>ImportManager</code> also handles array types, by always stripping off the indices (the square brackets)
 * when adding imports and preserving them when forming an imported name.
 * 
 * @since 2.1
 */
public class ImportManager
{
  /**
   * The set of imports to be added to the compilation unit.
   */
  protected SortedSet<String> imports = new TreeSet<String>();

  /**
   * The mapping from short names to qualified names for explicit and implicit imports.
   */
  protected HashMap<String, String> shortNameToImportMap = new HashMap<String, String>();

  /**
   * The set of packages that have been imported with wildcards.
   * This can also include containing classes, when inner classes are registered using dot separators.
   */
  protected HashSet<String> importedPackages;

  /*
   * The line delimiter string to use, or {@link System#getProperty(String) System.getProperty}<code>("line.separator")</code>
   * if null.
   * @since 2.5
   */
  private String lineDelimiter;

  /*
   * The string builder into which imports should be inserted.
   * @since 2.5
   */
  private StringBuilder importStringBuilder;

  /*
   * The string buffer into which imports should be inserted.
   * @since 2.5
   */
  private StringBuffer importStringBuffer;

  /*
   * The point in the string builder or buffer at which imports should be inserted.
   * @since 2.5
   */
  private int importInsertionPoint;

  /**
   * The set of short names from <code>java.lang</code> for which explicit import declarations are desired.
   */
  protected HashSet<String> javaLangImports = null;

  /**
   * Creates an import manager for the given compilation unit package and short name.
   * This is the preferred constructor form, as it automatically adds the {@link #addMasterImport(String, String) master import}
   * for the compilation unit.
   * @since 2.5
   * @see ImportManager#addMasterImport(String, String)
   */
  public ImportManager(String compilationUnitPackage, String compilationUnitShortName)
  {
    this(compilationUnitPackage);
    addMasterImport(compilationUnitPackage, compilationUnitShortName);
  }

  /**
   * Creates an import manager for a compilation unit in the given package.
   * Note that the {@link #ImportManager(String, String) two-argument form} is preferred.
   * @see #ImportManager(String, String)
   */
  public ImportManager(String compilationUnitPackage)
  {
    importedPackages = new HashSet<String>();
    importedPackages.add(collapse(compilationUnitPackage));
  }

  /*
   * Removes the whitespace from the given string if there is any.
   * The string is returned unchanged otherwise.
   */
  private String collapse(String s)
  {
    char[] src = s.toCharArray();
    char[] result = null;
    int srcLength = src.length;
    int resultLenth = -1;

    for (int i = 0; i < srcLength; i++)
    {
      if (src[i] <= ' ')
      {
        if (result == null)
        {
          result = new char[srcLength];
          System.arraycopy(src, 0, result, 0, i);
          resultLenth = i;
        }
      }
      else if (result != null)
      {
        result[resultLenth++] = src[i];
      }
    }
    return result != null ? new String(result, 0, resultLenth) : s;
  }

  /*
   * Normalizes the given qualified name, package name, or short name.
   * This must be done, at some point, on any name that is recorded.
   */
  private String normalize(String name)
  {
    int j = name.indexOf('[');  
    return collapse(j == -1 ? name : name.substring(0, j));
  }

  /*
   * Computes the normalized import name (package name + short name) from the given qualified name.
   */
  private String getImportName(String qualifiedName)
  {
    int j = qualifiedName.indexOf('$');
    if (j == -1)
    {
      j = qualifiedName.indexOf('[');
    }
    return collapse(j == -1 ? qualifiedName : qualifiedName.substring(0, j));
  }

  /*
   * Computes the normalized package name from the given qualified or import name.
   */
  private String getPackageName(String qualifiedName)
  {
    int j = qualifiedName.lastIndexOf('.');
    return j == -1 ? "" : collapse(qualifiedName.substring(0, j));
  }

  /*
   * Computes the normalized short name from the given qualified or import name.
   */
  private String getShortName(String qualifiedName)
  {
    int i = qualifiedName.lastIndexOf('.') + 1;
    int j = qualifiedName.indexOf('$', i);
    if (j == -1)
    {
      j = qualifiedName.indexOf('[', i);
    }
    if (j == -1)
    {
      j = qualifiedName.length();
    }
    return collapse(qualifiedName.substring(i, j));
  }

  /*
   * Computes the raw name, which is suitable for use in code, from the given qualified name.
   * This is exactly the same name, but with <code>$</code> replaced by dot.
   */
  private String getRawName(String qualifiedName)
  {
    return qualifiedName.replace('$', '.');
  }

  /*
   * Computes the base name, which is suitable for use in code, from the given qualified name.
   * The base name is not normalized.
   */
  private String getBaseName(String qualifiedName)
  {
    int i = qualifiedName.lastIndexOf('.') + 1;
    return qualifiedName.substring(i).replace('$', '.');
  }

  /**
   * Returns the equivalent imported short name for the given qualified name, if there is one, or the qualified name
   * itself otherwise. Optionally, the qualified name can be automatically imported if possible.
   * In fact, a parameterized type expression is also allowed, in which case the expression will be parsed to obtain the
   * individual imported names within it. Then the full expression, with the appropriate substitutions, is returned.
   * @param qualifiedName the qualified name or parameterized type expression.
   * @param autoImport whether to try to automatically import types as needed.
   * @return the equivalent type or type expression, using short names wherever possible.
   * @since 2.5
   */
  public String getImportedName(String qualifiedName, boolean autoImport)
  {
    int i = qualifiedName.indexOf('<');
    if (i == -1)
    {
      return basicGetImportedName(qualifiedName, autoImport);
    }

    StringBuilder result = new StringBuilder();
    for (int start = 0,  end = qualifiedName.length(); i < end; i++)
    {
      char c = qualifiedName.charAt(i);
      switch (c)
      {
        case ',':
        case '<':
        case '>':
        case '&':
        case '[':
        case ']':
        {
          if (start != i)
          {
            result.append(basicGetImportedName(qualifiedName.substring(start, i), autoImport));
          }
          result.append(c);
          start = i + 1;
          break;
        }
        case '?':
        {
          int j = i + 1;
          while (j < end && Character.isWhitespace(qualifiedName.charAt(j)))
          {
            j++;
          }
          if (j + 6 < end && "extends".equals(qualifiedName.substring(j, j + 7)))
          {
            result.append(qualifiedName.substring(i, j + 7));
            i = j + 6;
          }
          else if (j + 4 < end && "super".equals(qualifiedName.substring(j, j + 5)))
          {
            result.append(qualifiedName.substring(i, j + 5));
            i = j + 4;
          }
          else
          {
            result.append(c);
          }
          start = i + 1;
          break;
        }
        default:
        {
          if (Character.isWhitespace(c) && start == i)
          {
            result.append(c);
            start++;
          }
          break;
        }
      }
    }
    return result.toString();
  }

  /**
   * Returns the equivalent imported short name for the given qualified name, if there is one, or the qualified name
   * itself otherwise. Optionally, the qualified name can be automatically imported if possible.
   * In fact, a parameterized type expression is also allowed, in which case the expression will be parsed to obtain the
   * individual imported names within it. Then the full expression, with the appropriate substitutions, is returned.
   * @param qualifiedName the qualified name or parameterized type expression.
   * @return the equivalent type or type expression, using short names wherever possible.
   */
  public String getImportedName(String qualifiedName)
  {
    return getImportedName(qualifiedName, false);
  }

  /**
   * Gets the imported name for a single qualified name, optionally automatically importing if possible.
   */
  protected String basicGetImportedName(String qualifiedName, boolean autoImport)
  {
    int i = qualifiedName.lastIndexOf('.');
    if (i == -1)
    {
      if ("this".equals(qualifiedName) || "super".equals(qualifiedName))
      {
        String qualifiedThisOrSuper = shortNameToImportMap.get(qualifiedName);
        if (qualifiedThisOrSuper!= null)
        {
          return qualifiedThisOrSuper;
        }
      }
      return qualifiedName;
    }

    if (autoImport)
    {
      addImport(qualifiedName);
    }

    // For inner classes, we should try for an import at any level of nested class.
    //
    String rawName = getRawName(qualifiedName);
    char[] qualifiedNameChars = null;
    while (i != -1)
    {
      String baseName = getBaseName(qualifiedName);
      String importName = getImportName(qualifiedName);
      String shortName = getShortName(importName);

      String registeredName = shortNameToImportMap.get(shortName);
      
      if (registeredName == null)
      {
        // If no match, check for implicit java.lang import, but only on the first try (at the compilation unit level).
        // Failing that, check for matching package.
        //
        if (qualifiedNameChars == null && importName.equals("java.lang." + shortName))
        {
          if (javaLangImports != null && javaLangImports.contains(shortName))
          {
            imports.add(importName);
          }
          return baseName;
        }
        else if (importedPackages.contains(getPackageName(importName)) &&  (javaLangImports == null || !javaLangImports.contains(baseName)))
        {
          return baseName;
        }
      }
      else if (importName.equals(registeredName))
      {
        return baseName;
      }

      // Convert the next $ to a dot.
      //
      i = qualifiedName.indexOf('$', i);
      if (i != -1)
      {
        if (qualifiedNameChars == null)
        {
          qualifiedNameChars = qualifiedName.toCharArray();
        }
        qualifiedNameChars[i] = '.';
        qualifiedName = new String(qualifiedNameChars);
      }
    }
    return rawName;
  }

  /**
   * Registers an import for the given package name and short name.
   * Note that the <code>$</code> notation for inner classes is <em>not</em> supported by this method, since the short
   * name is explicitly specified.
   * @param packageName the package name of the type to import
   * @param shortName the short name of the type to import, or <code>"*"</code> for a wildcard import.
   */
  public void addImport(String packageName, String shortName)
  {
    basicAddImport(normalize(packageName), normalize(shortName), null);
  }

  /**
   * Registers an import for the given qualified name.
   * @param qualifiedName the qualified name of the type to import, which may end with <code>".*"</code> for a wildcard import.
   */
  public void addImport(String qualifiedName)
  {
    String importName = getImportName(qualifiedName);
    basicAddImport(getPackageName(importName), getShortName(importName), importName);
  }

  /*
   * Adds an import for the given package name, short name, and import name, which must all have been normalized already.
   * The import name is actually optional: if null, it will be computed from the package name and short name. 
   */
  private void basicAddImport(String packageName, String shortName, String importName)
  {
    if (importName == null)
    {
      importName = new StringBuilder(packageName).append('.').append(shortName).toString();
    }

    if (shortName.equals("*"))
    {
      importedPackages.add(packageName);
      imports.add(importName);
    }
    else if (!shortNameToImportMap.containsKey(shortName) && shouldImport(packageName, shortName, importName))
    {
      shortNameToImportMap.put(shortName, importName);

      if (!importedPackages.contains(packageName))
      {
        imports.add(importName);
      }
    }
  }

  /**
   * Determines whether the given non-wildcard import should be added.
   * By default, this returns false if the short name is a built-in Java language type name.
   * @since 2.8
   */
  protected boolean shouldImport(String packageName, String shortName, String importName)
  {
    return !CodeGenUtil.isJavaDefaultType(shortName);
  }

  /**
   * Registers a pseudo-import for the given qualified name.
   * A psuedo-import reserves the mapping from short name to qualified name, just like an ordinary import, but does not
   * actually include the import declarations among those returned by {@link #computeSortedImports()} or emitted by
   * {@link #emitSortedImports()}.
   * Note that all pseudo-imports must be added before any other ordinary imports.
   * @param qualifiedName the qualified name of the type to import, which may end with <code>".*"</code> for a wildcard import.
   * @see #computeSortedImports()
   * @see #emitSortedImports()
   */
  public void addPseudoImport(String qualifiedName)
  {
    String importName = getImportName(qualifiedName);
    String shortName = getShortName(importName);
    
    if (shortName.equals("*"))
    {
      String packageName = getPackageName(importName);
      importedPackages.add(packageName);
    }
    else
    {
      String oldImportName = shortNameToImportMap.put(shortName, importName);
      if (oldImportName != null)
      {
        imports.remove(oldImportName);
      }
    }
  }

  /**
   * Reserves the import mapping for the given package and short name of the compilation unit.
   * The <code>$</code> notation for inner classes is <em>not</em> supported by this method, since the short name is
   * explicitly specified.
   * Note that a master import must be added before any pseudo- or ordinary imports. However, it need not be done
   * explicitly if the preferred, {@link #ImportManager(String, String) two-argument constructor} form is used.
   * @see #ImportManager(String, String)
   */
  public void addMasterImport(String packageName, String shortName)
  {
    packageName = normalize(packageName);
    shortName = normalize(shortName);
    shortNameToImportMap.put(shortName, new StringBuilder(packageName).append('.').append(shortName).toString());
    shortNameToImportMap.put("this", shortName + ".this");
    shortNameToImportMap.put("super", shortName + ".super");
  }

  /**
   * Returns whether a mapping for the given short name has been reserved.
   */
  public boolean hasImport(String shortName)
  {
    return shortNameToImportMap.containsKey(normalize(shortName));
  }

  /**
   * Returns the list of qualified names for which imports are to be added to the compilation unit. 
   */
  public Collection<String> getImports()
  {
    compactImports();
    return imports;
  }

  /*
   * Removes any explicit imports that are already covered by wildcards.
   */
  private void compactImports()
  {
    for (Iterator<String> i = imports.iterator(); i.hasNext(); )
    {
      String importName = i.next();
      if (!getShortName(importName).equals("*") && importedPackages.contains(getPackageName(importName)))
      {
        i.remove();
      }
    }
  }

  /**
   * Returns the line delimiter to be used in {@link #computeSortedImports()}.
   * By default, this is {@link System#getProperty(String) System.getProperty}<code>("line.separator")</code>.
   * @see #computeSortedImports()
   * @see #setLineDelimiter(String)
   * @since 2.5
   */
  public String getLineDelimiter()
  {
    return lineDelimiter == null ? System.getProperty("line.separator") : lineDelimiter;
  }

  /**
   * Sets the line delimiter to be used in {@link #computeSortedImports()}.
   * @see #computeSortedImports()
   * @see #getLineDelimiter()
   * @since 2.5
   */
  public void setLineDelimiter(String lineDelimiter)
  {
    this.lineDelimiter = lineDelimiter;
  }

  /**
   * Returns the sorted, formatted import declarations that should be added to the compilation unit.
   * Each statement appears on its own line, with an additional {@link #getLineDelimiter() line delimiter} before the
   * first import and between imports from different packages.
   * @see #getLineDelimiter()
   */
  public String computeSortedImports()
  {
    String NL = getLineDelimiter();
    String previousPackageName = null;
    StringBuilder  result = new StringBuilder();

    for (String importName : getImports())
    {
      String packageName = getPackageName(importName);
      if (previousPackageName != null && !previousPackageName.equals(packageName))
      {
        result.append(NL);
      }
      previousPackageName = packageName;
      result.append(NL + "import " + importName + ";");
    }
    return result.toString();
  }

  /**
   * Registers {@link #addPseudoImport(String) pseudo-imports} for all of the import declarations in the specified
   * compilation unit contents.
   * This uses JDT's Java AST API to parse the code when Eclipse is running, and a simpler, less accurate approach
   * based on regular expressions otherwise. 
   * Note that this must be invoked before any ordinary imports are added.
   * @param compilationUnitContents the contents of a Java source file.
   * @see #addPseudoImport(String)
   */
  public void addCompilationUnitImports(String compilationUnitContents)
  {
    List<String> imports = EMFPlugin.IS_ECLIPSE_RUNNING ?
      EclipseHelper.getCompilationUnitImports(compilationUnitContents) :
      getCompilationUnitImports(compilationUnitContents);

    for (String qualifiedName : imports)
    {
      addPseudoImport(qualifiedName);
    }
  }

  /*
   * Uses a regular expression to try to parse the import statements from the given compilation unit contents.
   */
  private List<String> getCompilationUnitImports(String compilationUnitContents)
  {
    List<String> result = new ArrayList<String>();
    Pattern importPattern = Pattern.compile("import\\s+([^\\s;]*);\\s*", Pattern.MULTILINE | Pattern.DOTALL);
    Matcher matcher = importPattern.matcher(compilationUnitContents);
    while (matcher.find())
    {
      result.add(matcher.group(1));
    }
    return result;
  }

  /*
   * Uses JDT's AST API to parse the import statements from the given compilation unit contents.
   */
  private static class EclipseHelper
  {
    private static final AtomicReference<ASTParser> cachedASTParser = new AtomicReference<ASTParser>(null);

    public static List<String> getCompilationUnitImports(String compilationUnitContents)
    {
      List<String> result = new ArrayList<String>();
      ASTParser parser = cachedASTParser.getAndSet(null);
      if (parser == null)
      {
        parser = CodeGenUtil.EclipseUtil.newASTParser(true);
      }
      parser.setSource(compilationUnitContents.toCharArray());
      CompilationUnit compilationUnit = (CompilationUnit)parser.createAST(new NullProgressMonitor());
      for (Iterator<?> i = compilationUnit.imports().iterator(); i.hasNext();)
      {
        ImportDeclaration importDeclaration = (ImportDeclaration)i.next();
        result.add(importDeclaration.getName().getFullyQualifiedName());
      }
      cachedASTParser.set(parser);
      return result;
    }
  }


  /**
   * Records the given <code>StringBuilder</code> and its current length, so that computed imports can later be
   * {@link #emitSortedImports() emitted}, and {@link #addCompilationUnitImports(String) adds} any import declarations
   * that the builder already contains.
   * @since 2.5
   * @see #emitSortedImports()
   * @see #addCompilationUnitImports(String)
   */
  public void markImportLocation(StringBuilder stringBuilder)
  {
    importStringBuffer = null;
    importStringBuilder = stringBuilder;
    importInsertionPoint = stringBuilder.length();
    addCompilationUnitImports(stringBuilder.toString());
  }

  /**
   * Records the given <code>StringBuffer</code> and its current length, so that computed imports can later be
   * {@link #emitSortedImports() emitted}, and {@link #addCompilationUnitImports(String) adds} any import declarations
   * that the buffer already contains.
   * @since 2.5
   * @see #emitSortedImports()
   * @see #addCompilationUnitImports(String)
   */
  public void markImportLocation(StringBuffer stringBuffer)
  {
    importStringBuilder = null;
    importStringBuffer = stringBuffer;
    importInsertionPoint = stringBuffer.length();
    addCompilationUnitImports(stringBuffer.toString());
  }

  /**
   * Inserts all the {@link #computeSortedImports() computed imports} for the compilation unit into the recorded
   * {@link #markImportLocation(StringBuilder) StringBuilder} or {@link #markImportLocation(StringBuffer) StringBuffer}. 
   * @since 2.5
   * @see #computeSortedImports()
   * @see #markImportLocation(StringBuilder)
   * @see #markImportLocation(StringBuffer)
   */
  public void emitSortedImports()
  {
    if (importStringBuilder != null)
    {
      importStringBuilder.insert(importInsertionPoint, computeSortedImports());
    }
    else if (importStringBuffer != null)
    {
      importStringBuffer.insert(importInsertionPoint, computeSortedImports());
    }
  }

  /**
   * Ensures that explicit import declarations will be added for classes from <code>java.lang</code> with the 
   * specified short names.
   * By default, all the short names of classes in this package are reserved so that the implicit imports are used.
   * By specifying particular classes with this method, those imports, if actually used, will be made explicit.
   * @param javaLangClassNames
   */
  public void addJavaLangImports(List<String> javaLangClassNames)
  {
    if (!javaLangClassNames.isEmpty())
    {
      javaLangImports = new HashSet<String>();
      for (String shortName : javaLangClassNames)
      {
        javaLangImports.add(normalize(shortName));
      }
    }
  }
}
