/*
 * Copyright 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dev.javac;

import com.google.gwt.core.ext.CachedGeneratorResult;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.IncrementalGenerator;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.RebindResult;
import com.google.gwt.core.ext.RebindRuleResolver;
import com.google.gwt.core.ext.SubsetFilteringPropertyOracle;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.impl.ResourceLocatorImpl;
import com.google.gwt.core.ext.linker.Artifact;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility;
import com.google.gwt.core.ext.linker.GeneratedResource;
import com.google.gwt.core.ext.linker.impl.StandardGeneratedResource;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.CompilerContext;
import com.google.gwt.dev.cfg.RuleGenerateWith;
import com.google.gwt.dev.resource.Resource;
import com.google.gwt.dev.resource.ResourceOracle;
import com.google.gwt.dev.resource.impl.AbstractResourceOracle;
import com.google.gwt.dev.util.DiskCache;
import com.google.gwt.dev.util.collect.HashSet;
import com.google.gwt.dev.util.collect.IdentityHashMap;
import com.google.gwt.dev.util.log.perf.AbstractJfrEvent;
import com.google.gwt.thirdparty.guava.common.hash.Hashing;
import com.google.gwt.thirdparty.guava.common.io.Files;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;

import jdk.jfr.Label;
import jdk.jfr.Name;

/**
 * Manages generators and generated units during a single compilation.
 */
public class StandardGeneratorContext implements GeneratorContext {

  /**
   * Wraps a ResourceOracle to collect the paths of Resources read by Generators.
   */
  private class RecordingResourceOracle extends AbstractResourceOracle {

    private final ResourceOracle wrappedResourceOracle;

    public RecordingResourceOracle(ResourceOracle wrappedResourceOracle) {
      this.wrappedResourceOracle = wrappedResourceOracle;
    }

    @Override
    public void clear() {
      wrappedResourceOracle.clear();
    }

    @Override
    public Set<String> getPathNames() {
      return wrappedResourceOracle.getPathNames();
    }

    @Override
    public Resource getResource(String pathName) {
      pathName = Files.simplifyPath(pathName);
      compilerContext.getMinimalRebuildCache().associateReboundTypeWithInputResource(
          currentRebindBinaryTypeName, pathName);
      return wrappedResourceOracle.getResource(pathName);
    }

    @Override
    public Set<Resource> getResources() {
      return wrappedResourceOracle.getResources();
    }
  }

  /**
   * Extras added to {@link GeneratedUnit}.
   */
  private static interface Generated extends GeneratedUnit {
    void abort();

    void commit(TreeLogger logger);
  }

  /**
   * This generated unit acts as a normal generated unit as well as a buffer
   * into which generators can write their source. A controller should ensure
   * that source isn't requested until the generator has finished writing it.
   * This version is backed by {@link StandardGeneratorContext#diskCache}.
   */
  public static class GeneratedUnitImpl implements Generated {

    /**
     * A token to retrieve this object's bytes from the disk cache.
     */
    protected long sourceToken = -1;

    private long creationTime;

    private String strongHash; // cache so that refreshes work correctly

    private StringWriter sw;

    private final String typeName;

    public GeneratedUnitImpl(StringWriter sw, String typeName) {
      this.typeName = typeName;
      this.sw = sw;
    }

    @Override
    public void abort() {
      sw = null;
    }

    /**
     * Finalizes the source and adds this generated unit to the host.
     */
    @Override
    public void commit(TreeLogger logger) {
      String source = sw.toString();
      byte[] sourceBytes = source.getBytes(StandardCharsets.UTF_8);
      strongHash = Hashing.murmur3_128().hashBytes(sourceBytes).toString().toUpperCase(Locale.ROOT);
      sourceToken = diskCache.writeByteArray(sourceBytes);
      sw = null;
      creationTime = System.currentTimeMillis();
    }

    @Override
    public long creationTime() {
      return creationTime;
    }

    @Override
    public String getSource() {
      if (sw != null) {
        throw new IllegalStateException("source not committed");
      }
      return diskCache.readString(sourceToken);
    }

    @Override
    public String getSourceMapPath() {
      return "gen/" + getTypeName().replace('.', '/') + ".java";
    }

    @Override
    public long getSourceToken() {
      if (sw != null) {
        throw new IllegalStateException("source not committed");
      }
      return sourceToken;
    }

    @Override
    public String getStrongHash() {
      return strongHash;
    }

    @Override
    public String getTypeName() {
      return typeName;
    }

    @Override
    public String optionalFileLocation() {
      return null;
    }
  }

  /**
   * This generated unit acts as a normal generated unit as well as a buffer
   * into which generators can write their source. A controller should ensure
   * that source isn't requested until the generator has finished writing it.
   * This version is backed by an explicit generated file.
   */
  private static class GeneratedUnitWithFile extends GeneratedUnitImpl {
    private final File file;

    public GeneratedUnitWithFile(File file, StringWriter pw, String typeName) {
      super(pw, typeName);
      this.file = file;
    }

    @Override
    public void commit(TreeLogger logger) {
      super.commit(logger);
      try (FileOutputStream fos = new FileOutputStream(file)) {
        diskCache.transferToStream(sourceToken, fos);
      } catch (IOException e) {
        logger.log(TreeLogger.WARN, "Error writing out generated unit at '"
            + file.getAbsolutePath() + "': " + e);
      }
    }

    @Override
    public String optionalFileLocation() {
      return file.exists()
          ? file.getAbsolutePath().replaceAll("^file:jar:[^!]+!/", "")
          : null;
    }
  }

  /**
   * Manages a resource that is in the process of being created by a generator.
   */
  private static class PendingResource extends OutputStream {

    private ByteArrayOutputStream baos = new ByteArrayOutputStream();
    private final String partialPath;

    public PendingResource(String partialPath) {
      this.partialPath = partialPath;
    }

    public void abort() {
      baos = null;
    }

    public String getPartialPath() {
      return partialPath;
    }

    public byte[] takeBytes() {
      byte[] result = baos.toByteArray();
      baos = null;
      return result;
    }

    @Override
    public void write(byte[] b) throws IOException {
      if (baos == null) {
        throw new IOException("stream closed");
      }
      baos.write(b);
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
      if (baos == null) {
        throw new IOException("stream closed");
      }
      baos.write(b, off, len);
    }

    @Override
    public void write(int b) throws IOException {
      if (baos == null) {
        throw new IOException("stream closed");
      }
      baos.write(b);
    }
  }

  private static final String GENERATOR_VERSION_ID_KEY = "generator-version-id";

  private static DiskCache diskCache = DiskCache.INSTANCE;

  @Name("gwt.ext.Generator")
  @Label("Generator Event")
  static class GeneratorEvent extends AbstractJfrEvent {
    @Label("Generator Class")
    String generatorClass;
    @Label("Type Name")
    String typeName;
  }

  private final ArtifactSet allGeneratedArtifacts;

  private final Map<String, GeneratedUnit> committedGeneratedCups =
      new HashMap<String, GeneratedUnit>();

  private CompilationState compilationState;

  private Class<? extends Generator> currentGenerator;

  private final File genDir;

  private final Map<Class<? extends Generator>, Generator> generators =
      new IdentityHashMap<Class<? extends Generator>, Generator>();

  private ArtifactSet newlyGeneratedArtifacts = new ArtifactSet();

  private final Set<String> newlyGeneratedTypeNames = new HashSet<String>();

  private final Map<String, PendingResource> pendingResources =
      new HashMap<String, PendingResource>();

  private transient PropertyOracle propertyOracle;

  private RebindRuleResolver rebindRuleResolver;

  private final Map<PrintWriter, Generated> uncommittedGeneratedCupsByPrintWriter =
      new IdentityHashMap<PrintWriter, Generated>();

  private CachedGeneratorResultImpl cachedRebindResult = null;

  private boolean generatorResultCachingEnabled = false;

  private List<String> cachedTypeNamesToReuse = null;

  private boolean isProdMode;

  private CompilerContext compilerContext;

  private String currentRebindBinaryTypeName;

  private final ResourceOracle buildResourceOracle;

  /**
   * Normally, the compiler host would be aware of the same types that are
   * available in the supplied type oracle although it isn't strictly required.
   */
  public StandardGeneratorContext(CompilerContext compilerContext,
      CompilationState compilationState, ArtifactSet allGeneratedArtifacts, boolean isProdMode) {
    this.compilerContext = compilerContext;
    this.compilationState = compilationState;
    this.genDir = compilerContext.getOptions().getGenDir();
    this.allGeneratedArtifacts = allGeneratedArtifacts;
    this.isProdMode = isProdMode;

    this.buildResourceOracle =
        new RecordingResourceOracle(compilerContext.getBuildResourceOracle());

    ResourceLocatorImpl.resetClassLoaderLoadWarningCount();
  }

  /**
   * Adds a generated unit to the context if not already present, but will not
   * overwrite an existing unit.
   */
  public void addGeneratedUnit(GeneratedUnit gu) {
    if (!committedGeneratedCups.containsKey(gu.getTypeName())) {
      committedGeneratedCups.put(gu.getTypeName(), gu);
    }
  }

  /**
   * Adds generated units to the context, but will not overwrite any existing
   * units that might already be present.
   */
  public void addGeneratedUnits(Collection<GeneratedUnit> generatedUnits) {
    for (GeneratedUnit gu : generatedUnits) {
      addGeneratedUnit(gu);
    }
  }

  /**
   * Adds all available cached generated units to the context. Existing units
   * for a given type will not be overwritten.
   */
  public void addGeneratedUnitsFromCache() {
    if (cachedRebindResult != null && cachedRebindResult.getGeneratedUnits() != null) {
      addGeneratedUnits(cachedRebindResult.getGeneratedUnits());
    }
  }

  /**
   * Adds cached generated units to the context that have been marked for reuse.
   * Existing units for a given type will not be overwritten.
   */
  public void addGeneratedUnitsMarkedForReuseFromCache() {
    if (cachedTypeNamesToReuse != null && cachedRebindResult != null) {
      for (String typeName : cachedTypeNamesToReuse) {
        GeneratedUnit gu = cachedRebindResult.getGeneratedUnit(typeName);
        if (gu != null) {
          addGeneratedUnit(gu);
        }
      }
    }
  }

  /**
   * Checks whether a rebind rule is available for a given sourceTypeName.
   */
  @Override
  public boolean checkRebindRuleAvailable(String sourceTypeName) {
    if (rebindRuleResolver != null) {
      return rebindRuleResolver.checkRebindRuleResolvable(sourceTypeName);
    } else {
      return false;
    }
  }

  /**
   * Frees memory used up by compilation state.
   */
  public void clear() {
    compilationState = null;
    generators.clear();
  }

  /**
   * Commits a pending generated type.
   */
  @Override
  public final void commit(TreeLogger logger, PrintWriter pw) {
    Generated gcup = uncommittedGeneratedCupsByPrintWriter.get(pw);
    if (gcup == null) {
      logger.log(TreeLogger.WARN, "Generator attempted to commit an unknown PrintWriter", null);
      return;
    }
    gcup.commit(logger);
    uncommittedGeneratedCupsByPrintWriter.remove(pw);
    committedGeneratedCups.put(gcup.getTypeName(), gcup);

    // Write as a source artifact so that a debugger can use it.
    // TODO: if we're not generating sourcemaps then we should probably skip this entirely
    // since the data will be written to the shard's jar file and never read.
    // (But how do we check that?)

    if (currentGenerator == null) {
      return; // probably a test.
    }

    GeneratedResource debuggerSource =
        new StandardGeneratedResource(gcup.getSourceMapPath(), gcup.getSourceToken());
    debuggerSource.setVisibility(Visibility.Source);
    commitArtifact(logger, debuggerSource);
  }

  /**
   * Adds an Artifact to the context's ArtifactSets. This will replace a
   * pre-existing entry in allGeneratedArtifacts, but will not overwrite an
   * entry in the newlyGeneratedArtifacts (since it is assumed by convention
   * that only new entries will ever be inserted here for a given generator
   * run).
   */
  @Override
  public void commitArtifact(TreeLogger logger, Artifact<?> artifact) {
    allGeneratedArtifacts.replace(artifact);
    newlyGeneratedArtifacts.add(artifact);
  }

  /**
   * Commits all available cached Artifacts to the context.
   */
  public void commitArtifactsFromCache(TreeLogger logger) {
    if (cachedRebindResult != null && cachedRebindResult.getArtifacts() != null) {
      for (Artifact<?> art : cachedRebindResult.getArtifacts()) {
        commitArtifact(logger, art);
      }
    }
  }

  @Override
  public GeneratedResource commitResource(TreeLogger logger, OutputStream os)
      throws UnableToCompleteException {

    PendingResource pendingResource = null;
    String partialPath = null;
    if (os instanceof PendingResource) {
      pendingResource = (PendingResource) os;
      partialPath = pendingResource.getPartialPath();
      // Make sure it's ours by looking it up in the map.
      if (pendingResource != pendingResources.get(partialPath)) {
        pendingResource = null;
      }
    }
    if (pendingResource == null) {
      logger.log(TreeLogger.WARN, "Generator attempted to commit an unknown OutputStream", null);
      throw new UnableToCompleteException();
    }

    // Add the GeneratedResource to the ArtifactSet
    GeneratedResource toReturn =
        new StandardGeneratedResource(partialPath, pendingResource.takeBytes());
    commitArtifact(logger, toReturn);
    pendingResources.remove(pendingResource.getPartialPath());
    return toReturn;
  }

  /**
   * Call this whenever generators are known to not be running to clear out
   * uncommitted compilation units and to force committed compilation units to
   * be parsed and added to the type oracle.
   *
   * @return any newly generated artifacts since the last call
   *
   * @throw UnableToCompleteException if the compiler aborted (not
   * a normal compile error).</p>
   */
  public ArtifactSet finish(TreeLogger logger) throws UnableToCompleteException {
    abortUncommittedResources(logger);

    try {
      TreeLogger branch;
      if (isDirty()) {
        // Assimilate the new types into the type oracle.
        //
        String msg = "Assimilating generated source";
        branch = logger.branch(TreeLogger.DEBUG, msg, null);

        TreeLogger subBranch = null;
        if (branch.isLoggable(TreeLogger.DEBUG)) {
          subBranch = branch.branch(TreeLogger.DEBUG, "Generated source files...", null);
        }

        for (GeneratedUnit gcup : committedGeneratedCups.values()) {
          String qualifiedTypeName = gcup.getTypeName();
          if (subBranch != null) {
            subBranch.log(TreeLogger.DEBUG, qualifiedTypeName, null);
          }
        }

        compilationState.addGeneratedCompilationUnits(logger, committedGeneratedCups.values());
      }
      return newlyGeneratedArtifacts;
    } finally {

      // Remind the user if there uncommitted cups.
      if (!uncommittedGeneratedCupsByPrintWriter.isEmpty()) {
        String msg =
            "For the following type(s), generated source was never committed (did you forget to call commit()?)";
        logger = logger.branch(TreeLogger.WARN, msg, null);

        for (Generated unit : uncommittedGeneratedCupsByPrintWriter.values()) {
          logger.log(TreeLogger.WARN, unit.getTypeName(), null);
        }
      }

      reset();
    }
  }

  public boolean isDirty() {
    return !committedGeneratedCups.isEmpty();
  }

  /**
   * Clears all accumulated artifacts and state so that the context can be used
   * as if from scratch. Is useful for clearing out undesired changes after
   * having used the context to explore some hypothetical situations, for
   * example to run Generators for the purpose of discovering the properties
   * they depend on.
   */
  public void reset() {
    uncommittedGeneratedCupsByPrintWriter.clear();
    committedGeneratedCups.clear();
    newlyGeneratedTypeNames.clear();
    newlyGeneratedArtifacts = new ArtifactSet();
    cachedTypeNamesToReuse = null;
  }

  public Set<String> getActiveLinkerNames() {
    return compilerContext.getModule().getActiveLinkerNames();
  }

  /**
   * Gets newly committed artifacts.
   */
  public ArtifactSet getArtifacts() {
    return new ArtifactSet(newlyGeneratedArtifacts);
  }

  /**
   * Gets the previously cached rebind result for the current generator.
   */
  @Override
  public CachedGeneratorResult getCachedGeneratorResult() {
    return cachedRebindResult;
  }

  public GeneratorContext getCanonicalContext() {
    return this;
  }

  public CompilationState getCompilationState() {
    return compilationState;
  }

  /**
   * Gets all committed Java units.
   */
  public Map<String, GeneratedUnit> getGeneratedUnitMap() {
    return committedGeneratedCups;
  }

  @Override
  public final PropertyOracle getPropertyOracle() {
    return propertyOracle;
  }

  @Override
  public ResourceOracle getResourcesOracle() {
    return buildResourceOracle;
  }

  @Override
  public final TypeOracle getTypeOracle() {
    return compilationState.getTypeOracle();
  }

  @Override
  public boolean isGeneratorResultCachingEnabled() {
    return generatorResultCachingEnabled;
  }

  @Override
  public boolean isProdMode() {
    return isProdMode;
  }

  /**
   * This method is maintained for backwards compatibility.
   * {@link #runGeneratorIncrementally} should be used instead.
   */
  public String runGenerator(TreeLogger logger, Class<? extends Generator> generatorClass,
      String typeName) throws UnableToCompleteException {

    RebindResult result = runGeneratorIncrementally(logger, generatorClass, typeName);

    return result.getResultTypeName();
  }

  /**
   * Runs a generator incrementally, with support for managing the returned
   * {@link RebindResult} object, which can contain cached results. This is a
   * replacement for the {@link #runGenerator} method.
   * <p>
   * If the passed in generatorClass is an instance of
   * {@link IncrementalGenerator}, it's
   * {@link IncrementalGenerator#generateIncrementally} method will be called.
   * <p>
   * Otherwise, for backwards compatibility, the generatorClass will be wrapped
   * in a {@link IncrementalGenerator} instance, and it's
   * {@link Generator#generate} method will be called.
   *
   * @param logger
   * @param generatorClass
   * @param typeName
   * @return a RebindResult
   * @throws UnableToCompleteException
   */
  public RebindResult runGeneratorIncrementally(TreeLogger logger,
      Class<? extends Generator> generatorClass, String typeName) throws UnableToCompleteException {
    String msg = "Invoking generator " + generatorClass.getName();
    logger = logger.branch(TreeLogger.DEBUG, msg, null);

    Generator generator = generators.get(generatorClass);
    if (generator == null) {
      try {
        generator = generatorClass.newInstance();
        generators.put(generatorClass, generator);
      } catch (Throwable e) {
        logger.log(TreeLogger.ERROR, "Unexpected error trying to instantiate Generator '"
            + generatorClass.getName() + "'", e);
        throw new UnableToCompleteException();
      }
    }

    setCurrentGenerator(generatorClass);

    // Avoid call to System.currentTimeMillis() if not logging DEBUG level
    boolean loggable = logger.isLoggable(TreeLogger.DEBUG);
    long before = loggable ? System.currentTimeMillis() : 0L;

    String generatorClassName = generator.getClass().getName();

    PropertyOracle originalPropertyOracle = propertyOracle;
    try (GeneratorEvent event = new GeneratorEvent()) {
      event.generatorClass = generatorClassName;
      event.typeName = typeName;

      RebindResult result;
      // TODO(stalcup): refactor the Generator/PropertyOracle system (in a potentially backwards
      // incompatible way) so that all Generators are forced to accurately declare the names of
      // properties they care about.
      propertyOracle = new SubsetFilteringPropertyOracle(
          RuleGenerateWith.getAccessedPropertyNames(generator.getClass()), originalPropertyOracle,
          generatorClassName + "'s RunsLocal annotation may need to be updated.");
      if (generator instanceof IncrementalGenerator) {
        IncrementalGenerator incGenerator = (IncrementalGenerator) generator;

        // check version id for any previously cached rebind result
        if (cachedRebindResult != null) {
          Long cachedVersionId = (Long) cachedRebindResult.getClientData(GENERATOR_VERSION_ID_KEY);
          if (cachedVersionId != null && cachedVersionId != incGenerator.getVersionId()) {
            // remove from context
            if (logger.isLoggable(TreeLogger.TRACE)) {
              logger.log(TreeLogger.TRACE, "Got version mismatch with cached generator result for "
                  + typeName + ", invalidating cached result");
            }
            cachedRebindResult = null;
          }
        }

        // run the generator
        result = incGenerator.generateIncrementally(logger, this, typeName);

        // add version id to the returned result
        result.putClientData(GENERATOR_VERSION_ID_KEY, incGenerator.getVersionId());
      } else {
        // run a non-incremental generator
        result = IncrementalGenerator.generateNonIncrementally(logger, generator, this, typeName);
      }

      if (loggable) {
        long after = System.currentTimeMillis();
        msg =
            "Generator returned type '" + result.getResultTypeName() + "; mode "
                + result.getRebindMode() + "; in " + (after - before) + " ms";
        logger.log(TreeLogger.DEBUG, msg, null);
      }
      return result;
    } catch (AssertionError e) {
      // Catch and log the assertion as a convenience to the developer
      logger.log(TreeLogger.ERROR, "Generator '" + generatorClass.getName()
          + "' failed an assertion while rebinding '" + typeName + "'", e);
      throw new UnableToCompleteException();
    } catch (RuntimeException e) {
      logger.log(TreeLogger.ERROR, "Generator '" + generatorClass.getName()
          + "' threw an exception while rebinding '" + typeName + "'", e);
      throw new UnableToCompleteException();
    } finally {
      propertyOracle = originalPropertyOracle;
    }
  }

  /**
   * Set previously cached rebind result for currently active generator.
   */
  public void setCachedGeneratorResult(CachedGeneratorResult cachedRebindResult) {
    this.cachedRebindResult = (CachedGeneratorResultImpl) cachedRebindResult;
  }

  public void setCurrentGenerator(Class<? extends Generator> currentGenerator) {
    this.currentGenerator = currentGenerator;
  }

  public void setCurrentRebindBinaryTypeName(String currentRebindBinaryTypeName) {
    this.currentRebindBinaryTypeName = currentRebindBinaryTypeName;
  }

  public void setGeneratorResultCachingEnabled(boolean enabled) {
    this.generatorResultCachingEnabled = enabled;
  }

  /**
   * Sets the current transient property oracle to answer current property
   * questions.
   */
  public void setPropertyOracle(PropertyOracle propertyOracle) {
    this.propertyOracle = propertyOracle;
  }

  public void setRebindRuleResolver(RebindRuleResolver resolver) {
    this.rebindRuleResolver = resolver;
  }

  @Override
  public PrintWriter tryCreate(TreeLogger logger, String packageName, String simpleTypeName) {
    String typeName;
    if (packageName.length() == 0) {
      typeName = simpleTypeName;
    } else {
      typeName = packageName + '.' + simpleTypeName;
    }

    compilerContext.getMinimalRebuildCache().associateReboundTypeWithGeneratedCompilationUnitName(
        currentRebindBinaryTypeName, typeName);

    // Is type already known to the host?
    JClassType existingType = getTypeOracle().findType(packageName, simpleTypeName);
    if (existingType != null) {
      if (logger.isLoggable(TreeLogger.DEBUG)) {
        logger.log(TreeLogger.DEBUG, "Type '" + typeName
            + "' already exists and will not be re-created ", null);
      }
      return null;
    }

    // Type recently generated?
    if (newlyGeneratedTypeNames.contains(typeName)) {
      return null;
    }

    // The type isn't there, so we can let the caller create it. Remember that
    // it is pending so another attempt to create the same type will fail.
    Generated gcup;
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw, true) {
      /**
       * Overridden to force unix-style line endings for consistent behavior
       * across platforms.
       */
      @Override
      public void println() {
        super.print('\n');
        super.flush();
      }
    };
    if (this.genDir == null) {
      gcup = new GeneratedUnitImpl(sw, typeName);
    } else {
      File dir = new File(genDir, packageName.replace('.', File.separatorChar));
      // No need to check mkdirs result because an IOException will occur anyway
      dir.mkdirs();
      File srcFile = new File(dir, simpleTypeName + ".java");
      if (srcFile.exists()) {
        srcFile.delete();
      }
      gcup = new GeneratedUnitWithFile(srcFile, sw, typeName);
    }
    uncommittedGeneratedCupsByPrintWriter.put(pw, gcup);
    newlyGeneratedTypeNames.add(typeName);
    return pw;
  }

  @Override
  public OutputStream tryCreateResource(TreeLogger logger, String partialPath)
      throws UnableToCompleteException {

    logger =
        logger.branch(TreeLogger.DEBUG, "Preparing pending output resource '" + partialPath + "'",
            null);

    // Disallow null or empty names.
    if (partialPath == null || partialPath.trim().equals("")) {
      logger.log(TreeLogger.ERROR, "The resource name must be a non-empty string", null);
      throw new UnableToCompleteException();
    }

    // Disallow absolute paths.
    if (new File(partialPath).isAbsolute()) {
      logger
          .log(
              TreeLogger.ERROR,
              "Resource paths are intended to be relative to the compiled output directory and cannot be absolute",
              null);
      throw new UnableToCompleteException();
    }

    // Disallow backslashes (to promote consistency in calling code).
    if (partialPath.indexOf('\\') >= 0) {
      logger.log(TreeLogger.ERROR,
          "Resource paths must contain forward slashes (not backslashes) to denote subdirectories",
          null);
      throw new UnableToCompleteException();
    }

    // Check for public path collision.
    if (compilerContext.getPublicResourceOracle().getResource(partialPath) != null) {
      logger.log(TreeLogger.WARN, "Cannot create resource '" + partialPath
          + "' because it already exists on the public path", null);
      return null;
    }

    // See if the file is already committed.
    SortedSet<GeneratedResource> resources = allGeneratedArtifacts.find(GeneratedResource.class);
    for (GeneratedResource resource : resources) {
      if (partialPath.equals(resource.getPartialPath())) {
        return null;
      }
    }

    // See if the file is pending.
    if (pendingResources.containsKey(partialPath)) {
      // It is already pending.
      logger.log(TreeLogger.WARN, "The file '" + partialPath + "' is already a pending resource",
          null);
      return null;
    }
    PendingResource pendingResource = new PendingResource(partialPath);
    pendingResources.put(partialPath, pendingResource);
    return pendingResource;
  }

  /**
   * Adds a type name to the list of types to be reused from cache, if
   * available.
   *
   * @param typeName The fully qualified name of a type.
   *
   * @return true, if the type is available in the cache and was successfully
   *         added to the list for reuse, false otherwise.
   */
  @Override
  public boolean tryReuseTypeFromCache(String typeName) {
    if (!isGeneratorResultCachingEnabled() || cachedRebindResult == null
        || !cachedRebindResult.isTypeCached(typeName)) {
      return false;
    }

    if (cachedTypeNamesToReuse == null) {
      cachedTypeNamesToReuse = new ArrayList<String>();
    }
    cachedTypeNamesToReuse.add(typeName);
    return true;
  }

  private void abortUncommittedResources(TreeLogger logger) {
    if (pendingResources.isEmpty()) {
      // Nothing to do.
      return;
    }

    // Warn the user about uncommitted resources.
    logger = logger.branch(TreeLogger.WARN,
        "The following resources will not be created because they were never "
        + "committed (did you forget to call commit()?)", null);

    for (Entry<String, PendingResource> entry : pendingResources.entrySet()) {
      logger.log(TreeLogger.WARN, entry.getKey());
      entry.getValue().abort();
    }
    pendingResources.clear();
  }
}
