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

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.JWildcardType.BoundType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.dev.javac.Resolver;
import com.google.gwt.dev.javac.TypeParameterLookup;
import com.google.gwt.dev.javac.typemodel.JClassType;
import com.google.gwt.dev.javac.typemodel.JGenericType;
import com.google.gwt.dev.javac.typemodel.JParameterizedType;
import com.google.gwt.dev.javac.typemodel.JRealClassType;
import com.google.gwt.dev.javac.typemodel.JTypeParameter;
import com.google.gwt.dev.javac.typemodel.JWildcardType;
import com.google.gwt.dev.util.Name;

import org.objectweb.asm.signature.SignatureVisitor;

import java.util.ArrayList;
import java.util.List;

/**
 * Resolve a single parameterized type.
 */
public class ResolveTypeSignature extends EmptySignatureVisitor {

  private final Resolver resolver;
  private final TreeLogger logger;
  private final JType[] returnTypeRef;
  private final TypeParameterLookup lookup;
  private final char wildcardMatch;
  private final JClassType enclosingClass;

  private JClassType outerClass;
  private final List<JType[]> args = new ArrayList<JType[]>();
  private int arrayDepth = 0;

  /**
   * Resolve a parameterized type.
   *
   * @param resolver
   * @param logger
   * @param returnTypeRef "pointer" to return location, ie. 1-element array
   * @param lookup
   * @param enclosingClass
   */
  public ResolveTypeSignature(Resolver resolver, TreeLogger logger,
      JType[] returnTypeRef, TypeParameterLookup lookup, JClassType enclosingClass) {
    this(resolver, logger, returnTypeRef, lookup, enclosingClass, '=');
  }

  public ResolveTypeSignature(Resolver resovler, TreeLogger logger,
      JType[] returnTypeRef, TypeParameterLookup lookup, JClassType enclosingClass,
      char wildcardMatch) {
    this.resolver = resovler;
    this.logger = logger;
    this.returnTypeRef = returnTypeRef;
    this.lookup = lookup;
    this.enclosingClass = enclosingClass;
    this.wildcardMatch = wildcardMatch;
  }

  @Override
  public SignatureVisitor visitArrayType() {
    ++arrayDepth;
    return this;
  }

  @Override
  public void visitBaseType(char descriptor) {
    switch (descriptor) {
      case 'V':
        returnTypeRef[0] = JPrimitiveType.VOID;
        break;
      case 'B':
        returnTypeRef[0] = JPrimitiveType.BYTE;
        break;
      case 'J':
        returnTypeRef[0] = JPrimitiveType.LONG;
        break;
      case 'Z':
        returnTypeRef[0] = JPrimitiveType.BOOLEAN;
        break;
      case 'I':
        returnTypeRef[0] = JPrimitiveType.INT;
        break;
      case 'S':
        returnTypeRef[0] = JPrimitiveType.SHORT;
        break;
      case 'C':
        returnTypeRef[0] = JPrimitiveType.CHAR;
        break;
      case 'F':
        returnTypeRef[0] = JPrimitiveType.FLOAT;
        break;
      case 'D':
        returnTypeRef[0] = JPrimitiveType.DOUBLE;
        break;
      default:
        throw new IllegalStateException("Unrecognized base type " + descriptor);
    }
    // this is the last visitor called on this visitor
    visitEnd();
  }

  @Override
  public void visitClassType(String internalName) {
    assert Name.isInternalName(internalName);
    outerClass = enclosingClass;
    JRealClassType classType = resolver.findByInternalName(internalName);
    if (classType == null) {
      logger.log(TreeLogger.ERROR, "Unable to find class " + internalName);
      // Replace bound with Object if we can't find the class.
      returnTypeRef[0] = resolver.getTypeOracle().getJavaLangObject();
      return;
    }
    if (!resolver.resolveClass(logger, classType)) {
      // already logged why it failed.
      // Ignores the return value to be consistent with the behavior of
      // CompilationUnitTypeOracleUpdater.
    }
    returnTypeRef[0] = classType;
  }

  @Override
  public void visitEnd() {
    if (returnTypeRef[0] == null) {
      return;
    }
    resolveGenerics();
  }

  @Override
  public void visitInnerClassType(String innerName) {
    // Called after visitClass has already been called, and we will
    // successively refine the class by going into its inner classes.
    assert returnTypeRef[0] != null;
    resolveGenerics();
    outerClass = (JClassType) returnTypeRef[0];
    JClassType searchClass = outerClass;
    try {
      JParameterizedType pt = searchClass.isParameterized();
      if (pt != null) {
        searchClass = pt.getBaseType();
      }
      returnTypeRef[0] = searchClass.getNestedType(innerName);
    } catch (NotFoundException e) {
      logger.log(TreeLogger.ERROR, "Unable to resolve inner class " + innerName
          + " in " + searchClass, e);
    }
  }

  @Override
  public void visitTypeArgument() {
    JType[] arg = new JType[1]; // This could be int[] for example
    arg[0] = resolver.getTypeOracle().getWildcardType(
        JWildcardType.BoundType.UNBOUND,
        resolver.getTypeOracle().getJavaLangObject());
    args.add(arg);
  }

  @Override
  public SignatureVisitor visitTypeArgument(char wildcard) {
    JType[] arg = new JType[1];
    args.add(arg);
    // TODO(jat): should we pass enclosingClass here instead of null?
    // not sure what the enclosing class of a type argument means, but
    // I haven't found a case where it is actually used while processing
    // the type argument.
    return new ResolveTypeSignature(resolver, logger, arg, lookup, null, wildcard);
  }

  @Override
  public void visitTypeVariable(String name) {
    returnTypeRef[0] = lookup.lookup(name);
    // this is the last visitor called on this visitor
    visitEnd();
  }

  /**
   * Merge the bounds from the declared type parameters into the type arguments
   * for this type if necessary.
   *
   * <pre>
   * Example:
   * class Foo<T extends Bar> ...
   *
   * Foo<?> foo
   *
   * foo needs to have bounds ? extends Bar.
   * </pre>
   *
   * <p>
   * Currently we only deal with unbound wildcards as above, which matches
   * existing TypeOracleUpdater behavior. However, this may need to be
   * extended.
   *
   * @param typeParams
   * @param typeArgs
   */
  private void mergeTypeParamBounds(JTypeParameter[] typeParams,
      JClassType[] typeArgs) {
    int n = typeArgs.length;
    for (int i = 0; i < n; ++i) {
      JWildcardType wildcard = typeArgs[i] == null ? null : typeArgs[i].isWildcard();
      // right now we only replace Foo<?> with the constraints defined on the
      // definition (which appears to match the existing TypeOracleUpdater)
      // but other cases may need to be handled.
      if (wildcard != null
          && wildcard.getBoundType() == BoundType.UNBOUND
          && wildcard.getBaseType() == resolver.getTypeOracle().getJavaLangObject()
          && typeParams[i].getBaseType() != null) {
        typeArgs[i] = resolver.getTypeOracle().getWildcardType(
            BoundType.UNBOUND, typeParams[i].getBaseType());
      }
    }
  }

  private JType resolveGeneric(JType type, JClassType outer,
      JClassType[] typeArgs) {
    JGenericType genericType = (JGenericType) type.isGenericType();
    if (genericType != null) {
      int actual = typeArgs.length;
      JTypeParameter[] typeParams = genericType.getTypeParameters();
      int expected = typeParams.length;
      if (actual == 0 && expected > 0) {
        // If no type parameters were supplied, this is a raw type usage.
        type = genericType.getRawType();
      } else {
        if (actual != expected) {
          throw new IllegalStateException("Incorrect # of type parameters to "
              + genericType.getQualifiedBinaryName() + ": expected " + expected
              + ", actual=" + actual);
        }
        JClassType genericEnc = genericType.getEnclosingType();
        if (outer == null && genericEnc != null) {
          // Sometimes the signature is like Foo$Bar<H> even if Foo is a
          // generic class. The cases I have seen are where Foo's type
          // parameter is also named H and has the same bounds. That
          // manifests itself as getting visitClassType("Foo$Bar") and
          // then VisitTypeArgument/etc, rather than the usual
          // visitClassType("Foo"), visitTypeArgument/etc,
          // visitInnerClass("Bar"), visitTypeArgument/etc.
          //
          // So, in this case we have to build our own chain of enclosing
          // classes here, properly parameterizing any generics along the
          // way.
          // TODO(jat): more testing to validate this assumption
          JClassType[] outerArgs = null;
          JGenericType genericEncGeneric = genericEnc.isGenericType();
          if (genericEncGeneric != null) {
            JTypeParameter[] encTypeParams = genericEncGeneric.getTypeParameters();
            int n = encTypeParams.length;
            outerArgs = new JClassType[n];
            for (int i = 0; i < n; ++i) {
              outerArgs[i] = lookup.lookup(encTypeParams[i].getName());
              if (outerArgs[i] == null) {
                // check to see if our current type has a parameter of the same
                // name, and use it if so.
                for (int j = 0; j < expected; ++j) {
                  if (typeParams[j].getName().equals(encTypeParams[j].getName())) {
                    outerArgs[i] = typeArgs[j];
                    break;
                  }
                }
              }
              assert outerArgs[i] != null : "Unable to resolve type parameter "
                  + encTypeParams[i].getName() + " in enclosing type "
                  + genericEnc + " of type " + genericType;
            }
          }
          outer = (JClassType) resolveGeneric(genericEnc, null, outerArgs);
        }
        try {
          mergeTypeParamBounds(typeParams, typeArgs);
          type = resolver.getTypeOracle().getParameterizedType(genericType,
              outer, typeArgs);
        } catch (IllegalArgumentException e) {
          // Can't use toString on typeArgs as they aren't completely built
          // yet, so we have to roll our own.
          StringBuilder buf = new StringBuilder();
          buf.append("Unable to build parameterized type ");
          buf.append(genericType);
          String prefix = " with args <";
          for (JClassType typeArg : typeArgs) {
            buf.append(prefix).append(typeArg.getName());
            prefix = ", ";
          }
          if (", ".equals(prefix)) {
            buf.append('>');
          }
          logger.log(TreeLogger.ERROR, buf.toString(), e);
          type = genericType.getRawType();
        }
      }
    }
    return type;
  }

  private void resolveGenerics() {
    JGenericType genericType = (JGenericType) returnTypeRef[0].isGenericType();
    if (genericType != null) {
      int actual = args.size();
      JClassType[] typeArgs = new JClassType[actual];
      for (int i = 0; i < actual; ++i) {
        JType type = args.get(i)[0];
        if (!(type instanceof JClassType)) {
          logger.log(TreeLogger.ERROR, "Parameterized type argument is " + type
              + ", expected reference type");
        } else {
          typeArgs[i] = (JClassType) type;
        }
      }
      returnTypeRef[0] = resolveGeneric(genericType, outerClass, typeArgs);
      args.clear();
    }
    for (int i = 0; i < arrayDepth; ++i) {
      returnTypeRef[0] = resolver.getTypeOracle().getArrayType(returnTypeRef[0]);
    }
    switch (wildcardMatch) {
      case '=':
        // nothing to do for an exact match
        break;
      case '*':
        returnTypeRef[0] = resolver.getTypeOracle().getWildcardType(
            JWildcardType.BoundType.UNBOUND, (JClassType) returnTypeRef[0]);
        break;
      case '+':
        // ? extends T
        returnTypeRef[0] = resolver.getTypeOracle().getWildcardType(
            JWildcardType.BoundType.EXTENDS, (JClassType) returnTypeRef[0]);
        break;
      case '-':
        // ? super T
        returnTypeRef[0] = resolver.getTypeOracle().getWildcardType(
            JWildcardType.BoundType.SUPER, (JClassType) returnTypeRef[0]);
        break;
    }
    if (returnTypeRef[0] instanceof JClassType) {
      // Only JClassTypes can be an outer class
      outerClass = (JClassType) returnTypeRef[0];
    }
  }
}
