/*
 * 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.jdt.TypeRefVisitor;

import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.MarkerAnnotation;
import org.eclipse.jdt.internal.compiler.ast.NormalAnnotation;
import org.eclipse.jdt.internal.compiler.ast.SingleMemberAnnotation;
import org.eclipse.jdt.internal.compiler.impl.Constant;
import org.eclipse.jdt.internal.compiler.lookup.BinaryTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Check a {@link CompilationUnitDeclaration} for references to binary types
 * outside the context of an annotation.
 */
public class BinaryTypeReferenceRestrictionsChecker {
  /**
   * Records the location from which a {@link BinaryTypeBinding} is referenced.
   */
  static class BinaryTypeReferenceSite {
    private final BinaryTypeBinding binaryTypeBinding;
    private final Expression expression;

    BinaryTypeReferenceSite(Expression expression,
        BinaryTypeBinding binaryTypeBinding) {
      this.expression = expression;
      this.binaryTypeBinding = binaryTypeBinding;
    }

    public BinaryTypeBinding getBinaryTypeBinding() {
      return binaryTypeBinding;
    }

    public Expression getExpression() {
      return expression;
    }
  }

  /**
   * Visits a {@link CompilationUnitDeclaration} and records all expressions
   * which use a {@link BinaryTypeBinding} that are not part of an annotation
   * context.
   */
  static class BinaryTypeReferenceVisitor extends TypeRefVisitor {
    private final List<BinaryTypeReferenceSite> binaryTypeReferenceSites;

    public BinaryTypeReferenceVisitor(CompilationUnitDeclaration cud,
        List<BinaryTypeReferenceSite> binaryTypeReferenceSites) {
      super(cud);
      this.binaryTypeReferenceSites = binaryTypeReferenceSites;
    }

    @Override
    public boolean visit(MarkerAnnotation annotation, BlockScope scope) {
      // Ignore annotations
      return false;
    }

    @Override
    public boolean visit(MarkerAnnotation annotation, ClassScope scope) {
      // Ignore annotations
      return false;
    }

    @Override
    public boolean visit(NormalAnnotation annotation, BlockScope scope) {
      // Ignore annotations
      return false;
    }

    @Override
    public boolean visit(NormalAnnotation annotation, ClassScope scope) {
      // Ignore annotations
      return false;
    }

    @Override
    public boolean visit(SingleMemberAnnotation annotation, BlockScope scope) {
      // Ignore annotations
      return false;
    }

    @Override
    public boolean visit(SingleMemberAnnotation annotation, ClassScope scope) {
      // Ignore annotations
      return false;
    }

    @Override
    protected void onBinaryTypeRef(BinaryTypeBinding binding,
        CompilationUnitDeclaration unitOfReferrer, Expression expression) {
      if (expression.constant != null && expression.constant != Constant.NotAConstant) {
        // Allow compile time constants from classes provided only in binary form.
        return;
      }
      binaryTypeReferenceSites.add(new BinaryTypeReferenceSite(expression, binding));
    }

    @Override
    protected void onTypeRef(SourceTypeBinding referencedType,
        CompilationUnitDeclaration unitOfReferrer) {
      // do nothing
    }
  }

  /**
   * Scans a {@link CompilationUnitDeclaration} for expressions that use
   * {@link BinaryTypeBinding}s outside the context of an annotation. An error
   * is reported against the {@link CompilationUnitDeclaration} for the first
   * instance of each unique {@link BinaryTypeBinding}.
   */
  public static void check(CompilationUnitDeclaration cud) {
    List<BinaryTypeReferenceSite> binaryTypeReferenceSites = findAllBinaryTypeReferenceSites(cud);
    Set<BinaryTypeBinding> alreadySeenTypeBindings = new HashSet<BinaryTypeBinding>();

    for (BinaryTypeReferenceSite binaryTypeReferenceSite : binaryTypeReferenceSites) {
      BinaryTypeBinding binaryTypeBinding = binaryTypeReferenceSite.getBinaryTypeBinding();
      if (alreadySeenTypeBindings.contains(binaryTypeBinding)) {
        continue;
      }
      alreadySeenTypeBindings.add(binaryTypeBinding);

      String fileName = String.valueOf(binaryTypeBinding.getFileName());
      if (fileName.endsWith(".java")) {
        // This binary name is valid; it is a reference to a unit that was
        // compiled in a previous JDT run.
        continue;
      }
      String qualifiedTypeName = binaryTypeBinding.debugName();
      String error = formatBinaryTypeRefErrorMessage(qualifiedTypeName);

      // TODO(mmendez): provide extra help info?
      GWTProblem.recordError(binaryTypeReferenceSite.getExpression(), cud,
          error, null);
    }
  }

  static List<BinaryTypeReferenceSite> findAllBinaryTypeReferenceSites(
      CompilationUnitDeclaration cud) {
    List<BinaryTypeReferenceSite> binaryTypeReferenceSites = new ArrayList<BinaryTypeReferenceSite>();
    BinaryTypeReferenceVisitor binaryTypeReferenceVisitor = new BinaryTypeReferenceVisitor(
        cud, binaryTypeReferenceSites);
    cud.traverse(binaryTypeReferenceVisitor, cud.scope);
    return binaryTypeReferenceSites;
  }

  static String formatBinaryTypeRefErrorMessage(String qualifiedTypeName) {
    return "No source code is available for type " + qualifiedTypeName
        + "; did you forget to inherit a required module?";
  }

  private BinaryTypeReferenceRestrictionsChecker() {
    // Not instantiable
  }
}
