/*
 * 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.devtools.j2objc.gen;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.j2objc.Options;
import com.google.devtools.j2objc.ast.AbstractTypeDeclaration;
import com.google.devtools.j2objc.ast.CompilationUnit;
import com.google.devtools.j2objc.ast.TreeUtil;
import com.google.devtools.j2objc.types.HeaderImportCollector;
import com.google.devtools.j2objc.types.ImplementationImportCollector;
import com.google.devtools.j2objc.types.Import;
import com.google.devtools.j2objc.util.ElementUtil;
import com.google.devtools.j2objc.util.NameTable;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.TypeElement;

/**
 * Contains the generated source code and additional context for a single Java
 * type. Must not hold references to any AST nodes or bindings because we need
 * those to be cleaned up by the garbage collector.
 */
public class GeneratedType {

  private final String typeName;
  private final boolean isPrivate;
  private final List<String> superTypes;
  private final Set<Import> headerForwardDeclarations;
  private final Set<Import> headerIncludes;
  private final Set<Import> implementationForwardDeclarations;
  private final Set<Import> implementationIncludes;
  private final String publicDeclarationCode;
  private final String privateDeclarationCode;
  private final String implementationCode;
  private final GeneratedSourceMappings generatedSourceMappings;

  private GeneratedType(
      String typeName,
      boolean isPrivate,
      List<String> superTypes,
      Set<Import> headerForwardDeclarations,
      Set<Import> headerIncludes,
      Set<Import> implementationForwardDeclarations,
      Set<Import> implementationIncludes,
      String publicDeclarationCode,
      String privateDeclarationCode,
      String implementationCode,
      GeneratedSourceMappings generatedSourceMappings) {
    this.typeName = Preconditions.checkNotNull(typeName);
    this.isPrivate = isPrivate;
    this.superTypes = Preconditions.checkNotNull(superTypes);
    this.headerForwardDeclarations = Preconditions.checkNotNull(headerForwardDeclarations);
    this.headerIncludes = Preconditions.checkNotNull(headerIncludes);
    this.implementationForwardDeclarations =
        Preconditions.checkNotNull(implementationForwardDeclarations);
    this.implementationIncludes = Preconditions.checkNotNull(implementationIncludes);
    this.publicDeclarationCode = Preconditions.checkNotNull(publicDeclarationCode);
    this.privateDeclarationCode = Preconditions.checkNotNull(privateDeclarationCode);
    this.implementationCode = Preconditions.checkNotNull(implementationCode);
    this.generatedSourceMappings = Preconditions.checkNotNull(generatedSourceMappings);
  }

  public static GeneratedType fromTypeDeclaration(AbstractTypeDeclaration typeNode) {
    TypeElement typeElement = typeNode.getTypeElement();
    CompilationUnit unit = TreeUtil.getCompilationUnit(typeNode);
    NameTable nameTable = unit.getEnv().nameTable();
    boolean emitLineDirectives = unit.getEnv().options().emitLineDirectives();

    ImmutableList.Builder<String> superTypes = ImmutableList.builder();
    TypeElement superclass = ElementUtil.getSuperclass(typeElement);
    if (superclass != null) {
      superTypes.add(nameTable.getFullName(superclass));
    }
    for (TypeElement superInterface : ElementUtil.getInterfaces(typeElement)) {
      superTypes.add(nameTable.getFullName(superInterface));
    }

    HeaderImportCollector headerCollector =
        new HeaderImportCollector(unit, HeaderImportCollector.Filter.PUBLIC_ONLY);
    typeNode.accept(headerCollector);

    HeaderImportCollector privateDeclarationCollector =
        new HeaderImportCollector(unit, HeaderImportCollector.Filter.PRIVATE_ONLY);
    typeNode.accept(privateDeclarationCollector);

    ImplementationImportCollector importCollector = new ImplementationImportCollector(unit);
    typeNode.accept(importCollector);

    SourceBuilder builder = new SourceBuilder(emitLineDirectives);
    GeneratedSourceMappings generatedSourceMappings = new GeneratedSourceMappings();
    TypeDeclarationGenerator.generate(builder, typeNode, generatedSourceMappings);
    String publicDeclarationCode = builder.toString();

    builder = new SourceBuilder(emitLineDirectives);
    TypePrivateDeclarationGenerator.generate(builder, typeNode);

    String privateDeclarationCode;
    String implementationCode;
    Options options = unit.getEnv().options();
    if (unit.getEnv().translationUtil().generateImplementation(typeElement)) {
      builder = new SourceBuilder(options.emitLineDirectives());
      TypePrivateDeclarationGenerator.generate(builder, typeNode);
      privateDeclarationCode = builder.toString();

      builder = new SourceBuilder(options.emitLineDirectives());
      TypeImplementationGenerator.generate(builder, typeNode);
      implementationCode = builder.toString();
    } else {
      privateDeclarationCode = "";
      implementationCode = String.format(
          "// Implementation not generated because %s is on the bootclasspath.\n", 
          ElementUtil.getQualifiedName(typeElement));
    }

    ImmutableSet.Builder<Import> implementationIncludes = ImmutableSet.builder();
    implementationIncludes.addAll(privateDeclarationCollector.getSuperTypes());
    implementationIncludes.addAll(importCollector.getImports());

    return new GeneratedType(
        nameTable.getFullName(typeElement),
        typeNode.hasPrivateDeclaration(),
        superTypes.build(),
        ImmutableSet.copyOf(headerCollector.getForwardDeclarations()),
        ImmutableSet.copyOf(headerCollector.getSuperTypes()),
        ImmutableSet.copyOf(privateDeclarationCollector.getForwardDeclarations()),
        implementationIncludes.build(),
        publicDeclarationCode,
        privateDeclarationCode,
        implementationCode,
        generatedSourceMappings);
  }

  /**
   * The name of the ObjC type declared by this GeneratedType.
   */
  public String getTypeName() {
    return typeName;
  }

  public boolean isPrivate() {
    return isPrivate;
  }

  /**
   * The list of type names that must be declared prior to this type because it
   * inherits from them.
   */
  public List<String> getSuperTypes() {
    return superTypes;
  }

  public Set<Import> getHeaderForwardDeclarations() {
    return headerForwardDeclarations;
  }

  public Set<Import> getHeaderIncludes() {
    return headerIncludes;
  }

  public Set<Import> getImplementationForwardDeclarations() {
    return implementationForwardDeclarations;
  }

  public Set<Import> getImplementationIncludes() {
    return implementationIncludes;
  }

  public String getPublicDeclarationCode() {
    return publicDeclarationCode;
  }

  public String getPrivateDeclarationCode() {
    return privateDeclarationCode;
  }

  public String getImplementationCode() {
    return implementationCode;
  }

  public GeneratedSourceMappings getGeneratedSourceMappings() {
    return generatedSourceMappings;
  }

  @Override
  public String toString() {
    return typeName != null ? typeName : "<no-type>";
  }
}
