/*
 * Copyright 2008 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.InternalCompilerException;
import com.google.gwt.dev.util.DiskCache;
import com.google.gwt.dev.util.DiskCacheToken;
import com.google.gwt.dev.util.StringInterner;
import com.google.gwt.thirdparty.guava.common.annotations.VisibleForTesting;

import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.eclipse.jdt.internal.compiler.env.NameEnvironmentAnswer;

import java.io.Serializable;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Encapsulates the state of a single compiled class file.
 */
public class CompiledClass implements Serializable {

  private static final DiskCache diskCache = DiskCache.INSTANCE;

  static Collection<CompiledClass> copyForUnit(Collection<CompiledClass> in, CompilationUnit newUnit) {
    if (in == null) {
      return null;
    }
    List<CompiledClass> copy = new ArrayList<CompiledClass>();

    Map<CompiledClass, CompiledClass> enclosingClassMap = new HashMap<CompiledClass, CompiledClass>();
    for (CompiledClass cc : in) {
      CompiledClass copyCc = new CompiledClass(cc, newUnit);
      copy.add(copyCc);
      enclosingClassMap.put(cc, copyCc);
    }

    // Update the enclosing class references.   With enough effort, we could determine the
    // hierarchical relationship of compiled classes and initialize the copies with the
    // copied enclosing class, but this is less effort.
    for (CompiledClass copyCc : copy) {
      if (copyCc.enclosingClass == null) {
        continue;
      }
      CompiledClass newRef = enclosingClassMap.get(copyCc.enclosingClass);
      if (null == newRef) {
        throw new InternalCompilerException("Enclosing type not found for " + copyCc.sourceName);
      }
      copyCc.enclosingClass = newRef;
    }
    return Collections.unmodifiableCollection(copy);
  }

  /**
   * A token to retrieve this object's bytes from the disk cache. byte code is
   * placed in the cache when the object is deserialized.
   */
  private final DiskCacheToken classBytesToken;
  private CompiledClass enclosingClass;
  private final String internalName;
  private final boolean isLocal;
  private transient SoftReference<NameEnvironmentAnswer> nameEnvironmentAnswerReference;
  private String signatureHash;

  private final String sourceName;

  private CompilationUnit unit;

  /**
   * For mock construction in tests.
   */
  @VisibleForTesting
  protected CompiledClass(CompiledClass enclosingClass,
      String internalName, String sourceName) {
    this.enclosingClass = enclosingClass;
    this.internalName = StringInterner.get().intern(internalName);
    this.sourceName = StringInterner.get().intern(sourceName);
    this.isLocal = false;
    this.classBytesToken = null;
  }

  /**
   * Create a compiled class from raw class bytes.
   *
   * @param classBytes - byte code for this class
   * @param enclosingClass - outer class
   * @param isLocal Is this class a local class? (See the JLS rev 2 section
   *          14.3)
   * @param internalName the internal binary name for this class. e.g.
   *          {@code java/util/Map$Entry}. See
   *          {@link "http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html#14757"}
   * @param sourceName the qualified source name, e.g. {@code java.util.Map.Entry}.
   */
  CompiledClass(byte[] classBytes, CompiledClass enclosingClass, boolean isLocal,
      String internalName, String sourceName) {
    this.enclosingClass = enclosingClass;
    this.internalName = StringInterner.get().intern(internalName);
    this.sourceName = StringInterner.get().intern(sourceName);
    this.classBytesToken = new DiskCacheToken(diskCache.writeByteArray(classBytes));
    this.isLocal = isLocal;
  }

  /**
   * Used for cloning all compiled classes in one compilation unit.
   */
  private CompiledClass(CompiledClass orig, CompilationUnit newUnit) {
    this.enclosingClass = orig.enclosingClass;
    this.internalName = orig.internalName;
    this.sourceName = orig.sourceName;
    this.classBytesToken = orig.classBytesToken;
    this.isLocal = orig.isLocal;
    this.unit = newUnit;
    this.signatureHash = orig.signatureHash;
  }

  /**
   * Reads the bytes of the compiled class from the disk cache.
   */
  public byte[] getBytes() {
    return classBytesToken.readByteArray();
  }

  public CompiledClass getEnclosingClass() {
    return enclosingClass;
  }

  /**
   * Returns the class internal binary name for this type, e.g.
   * {@code java/util/Map$Entry}.
   */
  public String getInternalName() {
    return internalName;
  }

  /**
   * Returns the enclosing package, e.g. {@code java.util}.
   */
  public String getPackageName() {
    return Shared.getPackageNameFromBinary(internalName);
  }

  /**
   * Returns a hash code on the byte code of the class.
   */
  public String getSignatureHash() {
    if (signatureHash == null) {
      signatureHash = BytecodeSignatureMaker.getCompileDependencySignature(getBytes());
    }
    return signatureHash;
  }

  /**
   * Returns the qualified source name, e.g. {@code java.util.Map.Entry}.
   */
  public String getSourceName() {
    return sourceName;
  }

  public CompilationUnit getUnit() {
    return unit;
  }

  /**
   * Returns <code>true</code> if this is a local type, or if this type is
   * nested inside of any local type.
   */
  public boolean isLocal() {
    return isLocal;
  }

  @Override
  public String toString() {
    return internalName;
  }

  NameEnvironmentAnswer getNameEnvironmentAnswer() throws ClassFormatException {
    NameEnvironmentAnswer nameEnvironmentAnswer = nameEnvironmentAnswerReference == null
        ? null : nameEnvironmentAnswerReference.get();
    if (nameEnvironmentAnswer == null) {
      ClassFileReader cfr =
          new ClassFileReader(getBytes(), unit.getResourceLocation().toCharArray(), true);
      nameEnvironmentAnswer = new NameEnvironmentAnswer(cfr, null);
      nameEnvironmentAnswerReference = new SoftReference<>(nameEnvironmentAnswer);
    }
    return nameEnvironmentAnswer;
  }

  void initUnit(CompilationUnit unit) {
    this.unit = unit;
  }
}
