/*
 * Copyright 2009 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.dev.jjs.ast.JDeclaredType;
import com.google.gwt.dev.resource.Resource;
import com.google.gwt.thirdparty.guava.common.hash.Hashing;
import com.google.gwt.thirdparty.guava.common.hash.HashingOutputStream;

import org.eclipse.jdt.core.compiler.CategorizedProblem;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Locale;

/**
 * Builds a {@link CompilationUnit}.
 */
public abstract class CompilationUnitBuilder {

  static class GeneratedCompilationUnitBuilder extends CompilationUnitBuilder {
    private final GeneratedUnit generatedUnit;

    public GeneratedCompilationUnitBuilder(GeneratedUnit generatedUnit) {
      this.generatedUnit = generatedUnit;
    }

    @Override
    public ContentId getContentId() {
      return new ContentId(getTypeName(), generatedUnit.getStrongHash());
    }

    @Override
    public String getLocation() {
      return getLocationFor(generatedUnit);
    }

    @Override
    public String getSourceMapPath() {
      return generatedUnit.getSourceMapPath();
    }

    @Override
    public String getTypeName() {
      return generatedUnit.getTypeName();
    }

    @Override
    protected String doGetSource() {
      return generatedUnit.getSource();
    }

    @Override
    protected CompilationUnit makeUnit(List<CompiledClass> compiledClasses,
        List<JDeclaredType> types, Dependencies dependencies,
        Collection<? extends JsniMethod> jsniMethods, MethodArgNamesLookup methodArgs,
        CategorizedProblem[] problems) {
      return new GeneratedCompilationUnit(generatedUnit, compiledClasses, types, dependencies,
          jsniMethods, methodArgs, problems);
    }
  }

  static class ResourceCompilationUnitBuilder extends CompilationUnitBuilder {
    /**
     * Not valid until source has been read.
     */
    private ContentId contentId;

    private long lastModifed = -1;

    private final Resource resource;

    private final String typeName;

    private ResourceCompilationUnitBuilder(Resource resource) {
      this.typeName = Shared.toTypeName(resource.getPath());
      this.resource = resource;
    }

    @Override
    public ContentId getContentId() {
      if (contentId == null) {
        getSource();
      }
      return contentId;
    }

    public long getLastModified() {
      if (lastModifed < 0) {
        return resource.getLastModified();
      } else {
        // Value when the source was actually read.
        return lastModifed;
      }
    }

    @Override
    public String getLocation() {
      return resource.getLocation();
    }

    public Resource getResource() {
      return resource;
    }

    @Override
    public String getSourceMapPath() {
      return getSourceMapPathFor(resource);
    }

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

    @Override
    protected String doGetSource() {
      /*
       * Pin the mod date first to be conservative, we'd rather a unit be seen
       * as too stale than too fresh.
       */
      lastModifed = resource.getLastModified();
      ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
      HashingOutputStream hasher = new HashingOutputStream(Hashing.murmur3_128(), out);
      try (InputStream in = resource.openContents()) {
        /*
         * In most cases openContents() will throw an exception, however in the case of a
         * ZipFileResource it might return null causing an NPE in Util.copyNoClose(),
         * see issue 4359.
         */
        if (in == null) {
          throw new RuntimeException("Unexpected error reading resource '" + resource + "'");
        }
        in.transferTo(hasher);
      } catch (IOException e) {
        throw new RuntimeException("Unexpected error reading resource '" + resource + "'", e);
      }
      contentId = new ContentId(getTypeName(), hasher.hash().toString().toUpperCase(Locale.ROOT));
      return out.toString(StandardCharsets.UTF_8);
    }

    @Override
    protected CompilationUnit makeUnit(List<CompiledClass> compiledClasses,
        List<JDeclaredType> types, Dependencies dependencies,
        Collection<? extends JsniMethod> jsniMethods, MethodArgNamesLookup methodArgs,
        CategorizedProblem[] problems) {
      return new SourceFileCompilationUnit(getResource(), getContentId(), compiledClasses, types,
          dependencies, jsniMethods, methodArgs, problems, getLastModified());
    }
  }

  static final class GeneratedCompilationUnit extends CompilationUnitImpl {
    private final GeneratedUnit generatedUnit;

    public GeneratedCompilationUnit(GeneratedUnit generatedUnit,
        List<CompiledClass> compiledClasses, List<JDeclaredType> types, Dependencies dependencies,
        Collection<? extends JsniMethod> jsniMethods, MethodArgNamesLookup methodArgs,
        CategorizedProblem[] problems) {
      super(compiledClasses, types, dependencies, jsniMethods, methodArgs, problems);
      this.generatedUnit = generatedUnit;
    }

    @Override
    public CachedCompilationUnit asCachedCompilationUnit() {
      return new CachedCompilationUnit(this, astToken);
    }

    @Override
    public long getLastModified() {
      return generatedUnit.creationTime();
    }

    @Override
    public String getResourceLocation() {
      return getLocationFor(generatedUnit);
    }

    @Override
    public String getResourcePath() {
      return Shared.toPath(generatedUnit.getTypeName());
    }

    @Override
    public String getTypeName() {
      return generatedUnit.getTypeName();
    }

    @Deprecated
    @Override
    public boolean isGenerated() {
      return true;
    }

    @Deprecated
    @Override
    public boolean isSuperSource() {
      return false;
    }

    @Override
    ContentId getContentId() {
      return new ContentId(getTypeName(), generatedUnit.getStrongHash());
    }

    String getSource() {
      return generatedUnit.getSource();
    }
  }

  public static CompilationUnitBuilder create(GeneratedUnit generatedUnit) {
    return new GeneratedCompilationUnitBuilder(generatedUnit);
  }

  public static CompilationUnitBuilder create(Resource resource) {
    return new ResourceCompilationUnitBuilder(resource);
  }

  /**
   * Given a resource, returns the filename that will appear in the source map.
   */
  public static String getSourceMapPathFor(Resource resource) {
    return resource.getPathPrefix() + resource.getPath();
  }

  static String getLocationFor(GeneratedUnit generatedUnit) {
    String location = generatedUnit.optionalFileLocation();
    if (location != null) {
      return location;
    }
    return "generated://" + generatedUnit.getStrongHash() + "/"
        + Shared.toPath(generatedUnit.getTypeName());
  }

  private List<CompiledClass> compiledClasses;
  private Dependencies dependencies;
  private Collection<? extends JsniMethod> jsniMethods;
  private MethodArgNamesLookup methodArgs;
  private CategorizedProblem[] problems;

  /**
   * Caches source until JSNI methods can be collected.
   */
  private transient String source;

  private List<JDeclaredType> types;

  protected CompilationUnitBuilder() {
  }

  public CompilationUnit build() {
    // Free the source now.
    source = null;
    assert compiledClasses != null;
    assert types != null;
    assert dependencies != null;
    assert jsniMethods != null;
    assert methodArgs != null;
    return makeUnit(compiledClasses, types, dependencies, jsniMethods, methodArgs, problems);
  }

  public abstract ContentId getContentId();

  /**
   * Returns the location that should appear in JDT error messages.
   */
  public abstract String getLocation();

  public String getSource() {
    if (source == null) {
      source = doGetSource();
    }
    return source;
  }

  /**
   * Returns the location for this resource as it should appear in a sourcemap.
   * For a regular source file, it should be a path relative to one of the classpath entries
   * from the ResourceLoader. For generated files, it should be "gen/" followed by the path where
   * the source file would show up in the generated files directory if the "-gen" compiler option
   * were enabled.
   */
  public abstract String getSourceMapPath();

  /**
   * Returns the type source name.
   */
  public abstract String getTypeName();

  public CompilationUnitBuilder setClasses(List<CompiledClass> compiledClasses) {
    this.compiledClasses = compiledClasses;
    return this;
  }

  public CompilationUnitBuilder setDependencies(Dependencies dependencies) {
    this.dependencies = dependencies;
    return this;
  }

  public CompilationUnitBuilder setJsniMethods(Collection<? extends JsniMethod> jsniMethods) {
    this.jsniMethods = jsniMethods;
    return this;
  }

  public CompilationUnitBuilder setMethodArgs(MethodArgNamesLookup methodArgs) {
    this.methodArgs = methodArgs;
    return this;
  }

  public CompilationUnitBuilder setProblems(CategorizedProblem[] problems) {
    this.problems = problems;
    return this;
  }

  public CompilationUnitBuilder setTypes(List<JDeclaredType> types) {
    this.types = types;
    return this;
  }

  @Override
  public final String toString() {
    return getLocation();
  }

  protected abstract String doGetSource();

  protected abstract CompilationUnit makeUnit(List<CompiledClass> compiledClasses,
      List<JDeclaredType> types, Dependencies dependencies,
      Collection<? extends JsniMethod> jsniMethods, MethodArgNamesLookup methodArgs,
      CategorizedProblem[] errors);
}
