/*
 * Copyright 2014 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.util.Name.InternalName;
import com.google.gwt.thirdparty.guava.common.base.Function;
import com.google.gwt.thirdparty.guava.common.base.Joiner;
import com.google.gwt.thirdparty.guava.common.base.Predicate;
import com.google.gwt.thirdparty.guava.common.base.Strings;
import com.google.gwt.thirdparty.guava.common.collect.FluentIterable;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
import com.google.gwt.thirdparty.guava.common.collect.Iterables;
import com.google.gwt.thirdparty.guava.common.collect.Lists;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.impl.BooleanConstant;
import org.eclipse.jdt.internal.compiler.impl.StringConstant;
import org.eclipse.jdt.internal.compiler.lookup.AnnotationBinding;
import org.eclipse.jdt.internal.compiler.lookup.BaseTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.compiler.lookup.ElementValuePair;
import org.eclipse.jdt.internal.compiler.lookup.FieldBinding;
import org.eclipse.jdt.internal.compiler.lookup.LocalTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.NestedTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.SyntheticArgumentBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
 * Utility functions to interact with JDT classes.
 */
public final class JdtUtil {
  private static final String JSO_CLASS = "com/google/gwt/core/client/JavaScriptObject";

  /**
   * Returns a source name from an array of names.
   */
  public static String asDottedString(char[][] name) {
    return join(name, ".");
  }

  /**
   * Returns a string name from an array of names using {@code separator}.
   */
  public static String join(char[][] name, String separator) {
    StringBuilder result = new StringBuilder();
    if (name.length > 0) {
      result.append(name[0]);
    }

    for (int i = 1; i < name.length; ++i) {
      result.append(separator);
      result.append(name[i]);
    }
    return result.toString();
  }

  /**
   * Returns the fully qualified source name of the type (class, interface or enum) from reference
   * binding.
   * <p>
   * JDT Core (at least 3.11.0.v20150407) returns <code>$Local$</code> synthetic name for local
   * classes.<br>
   * This method aware about local classes and instead of <code>$Local$</code> synthetic name
   * returns fully qualified name of the local class in form of anonymous class (e.g.
   * <code>test.Class1$2</code>).
   * </p>
   */
  public static String getQualifiedSourceName(ReferenceBinding binding) {
    if (binding instanceof LocalTypeBinding) {
      // Using here constantPoolName() instead of coumpoundName due JDT not computing the
      // right compoundName for lambdas inside local class.
      return InternalName.toBinaryName(String.valueOf(binding.constantPoolName()));
    }

    return asDottedString(binding.compoundName);
  }

  /**
   * Returns the top type of the compilation unit that defines
   * {@code binding}.
   */
  public static String getDefiningCompilationUnitType(ReferenceBinding binding) {
    // Get the compilation unit type name.
    // TODO(rluble): check that this is valid for classes declared in the same compilation unit
    // top scope.
    return asDottedString(binding.outermostEnclosingType().compoundName);
  }

  public static String getSourceName(TypeBinding classBinding) {
    return getSourceName(CharOperation.charToString(classBinding.qualifiedPackageName()),
        CharOperation.charToString(classBinding.qualifiedSourceName()));
  }

  public static String getSourceName(String qualifiedPackageName, String qualifiedSourceName) {
    return Joiner.on(".").skipNulls().join(new String[] {
        Strings.emptyToNull(qualifiedPackageName),
        qualifiedSourceName});
  }

  public static String getBinaryName(TypeBinding classBinding) {
    return getBinaryName(CharOperation.charToString(classBinding.qualifiedPackageName()),
        CharOperation.charToString(classBinding.qualifiedSourceName()));
  }

  public static String getBinaryName(String qualifiedPackageName, String qualifiedSourceName) {
    return Joiner.on(".").skipNulls().join(new String[] {
        Strings.emptyToNull(qualifiedPackageName),
        qualifiedSourceName.replace('.','$')});
  }

  public static boolean isInnerClass(ReferenceBinding binding) {
    return binding.isNestedType() && !binding.isStatic();
  }

  /**
   * Get a readable method description from {@code methodBinding} conforming with JSNI formatting.
   * <p>
   * See examples:
   * <ul>
   * <li>a constructor of class A with a java.lang.String parameter will be formatted as
   * "new(Ljava/lang/String;).</li>
   * <li>a method with name m with an parameter of class java.lang.Object and return type boolean
   * will be formatted as "m(Ljava/lang/Object;</li>
   * </ul>,
   */
  public static String formatMethodSignature(MethodBinding methodBinding) {
    ReferenceBinding declaringClassBinding = methodBinding.declaringClass;
    StringBuilder methodNameWithSignature = new StringBuilder();
    String methodName = String.valueOf(methodBinding.selector);
    List<TypeBinding> parameterTypeBindings = Lists.newArrayList();
    if (methodName.equals("<init>")) {
      // It is a constructor.
      // (1) use the JSNI methodName instead of <init>.
      methodName = "new";
      // (2) add the implicit constructor parameters types for non static inner classes.
      if (isInnerClass(declaringClassBinding)) {
        NestedTypeBinding nestedBinding = (NestedTypeBinding) declaringClassBinding;
        if (nestedBinding.enclosingInstances != null) {
          for (SyntheticArgumentBinding argumentBinding : nestedBinding.enclosingInstances) {
            parameterTypeBindings.add(argumentBinding.type);
          }
        }
      }
    }

    parameterTypeBindings.addAll(Arrays.asList(methodBinding.parameters));
    methodNameWithSignature.append(methodName);
    methodNameWithSignature.append("(");
    for (TypeBinding parameterTypeBinding : parameterTypeBindings) {
      methodNameWithSignature.append(parameterTypeBinding.signature());
    }
    methodNameWithSignature.append(")");
    return methodNameWithSignature.toString();
  }

  public static String formatBinding(MethodBinding methodBinding) {
    String accessModifier = null;
    if (methodBinding.isProtected()) {
      accessModifier = "protected";
    } else if (methodBinding.isPrivate()) {
      accessModifier = "private";
    } else if (methodBinding.isPublic()) {
      accessModifier = "public";
    }
    return Joiner.on(" ").skipNulls().join(
        accessModifier,
        methodBinding.isStatic() ? "static" : null,
        getSourceName(methodBinding.declaringClass) + "." +
            formatMethodSignature(methodBinding)
    );
  }

  private JdtUtil() {
  }

  public static String getAnnotationParameterString(
      AnnotationBinding annotationBinding, String parameterName) {
    if (annotationBinding != null) {
      for (ElementValuePair parameterNameValuePair : annotationBinding.getElementValuePairs()) {
        if (parameterNameValuePair.getValue() instanceof StringConstant &&
            parameterName.equals(String.valueOf(parameterNameValuePair.getName()))) {
          return ((StringConstant) parameterNameValuePair.getValue()).stringValue();
        }
      }
    }
    return null;
  }

  public static boolean getAnnotationParameterBoolean(
      AnnotationBinding annotationBinding, String parameterName, boolean defaultValue) {
    Boolean booleanParameterValue = getAnnotationParameterBoolean(annotationBinding, parameterName);
    return booleanParameterValue == null ? defaultValue : booleanParameterValue;
  }

  public static Boolean getAnnotationParameterBoolean(
      AnnotationBinding annotationBinding, String parameterName) {
    if (annotationBinding != null) {
      for (ElementValuePair parameterNameValuePair : annotationBinding.getElementValuePairs()) {
        if (parameterNameValuePair.getValue() instanceof BooleanConstant &&
            parameterName.equals(String.valueOf(parameterNameValuePair.getName()))) {
          return ((BooleanConstant) parameterNameValuePair.getValue()).booleanValue();
        }
      }
    }
    return null;
  }

  public static AnnotationBinding getAnnotationByName(Annotation[] annotations, String name) {
    if (annotations == null) {
      return null;
    }
    for (Annotation annotation : annotations) {
      AnnotationBinding annotationBinding = annotation.getCompilerAnnotation();
      if (matchAnnotationName(annotationBinding, name)) {
        return annotationBinding;
      }
    }
    return null;
  }

  public static AnnotationBinding getAnnotationByName(
      AnnotationBinding[] annotationsBindings, String name) {
    if (annotationsBindings == null) {
      return null;
    }
    for (AnnotationBinding annotationBinding : annotationsBindings) {
      if (matchAnnotationName(annotationBinding, name)) {
        return annotationBinding;
      }
    }
    return null;
  }

  private static boolean matchAnnotationName(AnnotationBinding annotationBinding, String name) {
    if (annotationBinding == null) {
      return false;
    }
    return name.equals(CharOperation.toString(annotationBinding.getAnnotationType().compoundName));
  }

  public static TypeBinding getAnnotationParameterTypeBinding(
      AnnotationBinding annotationBinding, String parameterName) {
    if (annotationBinding != null) {
      for (ElementValuePair parameterNameValuePair : annotationBinding.getElementValuePairs()) {
        if (parameterNameValuePair.getValue() instanceof Class &&
            parameterName.equals(String.valueOf(parameterNameValuePair.getName()))) {
          return (TypeBinding) parameterNameValuePair.getValue();
        }
      }
    }
    return null;
  }

  public static TypeBinding[] getAnnotationParameterTypeBindingArray(
      AnnotationBinding annotationBinding, String parameterName) {
    if (annotationBinding == null) {
      return null;
    }

    for (ElementValuePair parameterNameValuePair : annotationBinding.getElementValuePairs()) {
      Object value = parameterNameValuePair.getValue();
      if (!parameterName.equals(String.valueOf(parameterNameValuePair.getName()))) {
        continue;
      }
      if (value instanceof Object[]) {
        Object[] values = (Object[]) value;
        TypeBinding bindings[] = new TypeBinding[values.length];
        System.arraycopy(values, 0, bindings, 0, values.length);
        return bindings;
      }
      assert value instanceof TypeBinding;
      return new TypeBinding[] {(TypeBinding) value};
    }
    return null;
  }

  public static StringConstant[] getAnnotationParameterStringConstantArray(
      AnnotationBinding annotationBinding, String parameterName) {
    if (annotationBinding == null) {
      return null;
    }
    for (ElementValuePair parameterNameValuePair : annotationBinding.getElementValuePairs()) {
      if (!parameterName.equals(String.valueOf(parameterNameValuePair.getName()))) {
        continue;
      }
      Object value = parameterNameValuePair.getValue();
      if (value instanceof Object[]) {
        Object[] values = (Object[]) value;
        StringConstant[] stringConstants = new StringConstant[values.length];
        System.arraycopy(values, 0, stringConstants, 0, values.length);
        return stringConstants;
      }
      assert value instanceof StringConstant;
      return new StringConstant[] {(StringConstant) value};
    }
    return null;
  }

  public static Set<String> getSuppressedWarnings(Annotation[] annotations) {
    if (annotations == null) {
      return ImmutableSet.of();
    }
    AnnotationBinding suppressWarnings =
        getAnnotationByName(annotations, SuppressWarnings.class.getName());
    if (suppressWarnings != null) {
      StringConstant[] values =
          JdtUtil.getAnnotationParameterStringConstantArray(suppressWarnings, "value");
      return FluentIterable.from(Arrays.asList(values))
          .transform(new Function<StringConstant, String>() {
            @Override
            public String apply(StringConstant value) {
              return value.stringValue();
            }
          })
          .toSet();
    }
    return ImmutableSet.of();
  }

  public static String signature(FieldBinding binding) {
    StringBuilder sb = new StringBuilder();
    sb.append(binding.declaringClass.constantPoolName());
    sb.append('.');
    sb.append(binding.name);
    sb.append(':');
    sb.append(binding.type.signature());
    return sb.toString();
  }

  public static String signature(MethodBinding binding) {
    StringBuilder sb = new StringBuilder();
    sb.append(binding.declaringClass.constantPoolName());
    sb.append('.');
    sb.append(binding.selector);
    sb.append('(');
    for (TypeBinding paramType : binding.parameters) {
      sb.append(paramType.signature());
    }
    sb.append(')');
    sb.append(binding.returnType.signature());
    return sb.toString();
  }

  public static String signature(TypeBinding binding) {
    assert !binding.isIntersectionType18() && !binding.isIntersectionType() : binding.debugName();
    if (binding.isBaseType()) {
      return String.valueOf(binding.sourceName());
    } else {
      return String.valueOf(binding.constantPoolName());
    }
  }

  public static void setClassDispositionFromBinding(SourceTypeBinding binding, JDeclaredType type) {
    if (binding.isNestedType()) {
      if (isLocalClass(binding)) {
        type.setClassDisposition(JDeclaredType.NestedClassDisposition.LOCAL);
      } else if (binding.isAnonymousType()) {
        type.setClassDisposition(JDeclaredType.NestedClassDisposition.ANONYMOUS);
      } else if (isInnerClass(binding)) {
        type.setClassDisposition(JDeclaredType.NestedClassDisposition.INNER);
      } else if (isStaticClass(binding)) {
        type.setClassDisposition(JDeclaredType.NestedClassDisposition.STATIC);
      }
    } else {
      type.setClassDisposition(JDeclaredType.NestedClassDisposition.TOP_LEVEL);
    }
  }

  public static boolean isLocalClass(SourceTypeBinding binding) {
    return binding.isLocalType() && !binding.isAnonymousType();
  }

  public static boolean isStaticClass(SourceTypeBinding binding) {
    return binding.isNestedType() && binding.isStatic();
  }

  /**
   * Returns {@code true} if {@code typeBinding} is {@code JavaScriptObject} or
   * any subtype.
   */
  public static boolean isJso(TypeBinding typeBinding) {
    if (!(typeBinding instanceof ReferenceBinding)) {
      return false;
    }
    ReferenceBinding binding = (ReferenceBinding) typeBinding;
    while (binding != null) {
      if (JSO_CLASS.equals(String.valueOf(binding.constantPoolName()))) {
        return true;
      }
      binding = binding.superclass();
    }
    return false;
  }

  /**
   * Returns {@code true} if {@code typeBinding} is a subtype of
   * {@code JavaScriptObject}, but not {@code JavaScriptObject} itself.
   */
  public static boolean isJsoSubclass(TypeBinding typeBinding) {
    if (!(typeBinding instanceof ReferenceBinding)) {
      return false;
    }
    ReferenceBinding binding = (ReferenceBinding) typeBinding;
    return isJso(binding.superclass());
  }

  public static Iterable<ReferenceBinding> getSuperInterfacesRequiringInitialization(
      ReferenceBinding type) {
    Iterable<ReferenceBinding> interfaces = Collections.emptyList();
    for (ReferenceBinding interfaceType : type.superInterfaces()) {
      interfaces =
          Iterables.concat(interfaces, getSuperInterfacesRequiringInitialization(interfaceType));
      if (hasDefaultMethods(interfaceType)) {
        interfaces = Iterables.concat(interfaces, Collections.singleton(interfaceType));
      }
    }
    return interfaces;
  }

  private static boolean hasDefaultMethods(ReferenceBinding interfaceType) {
    return Iterables.any(Arrays.asList(interfaceType.methods()), new Predicate<MethodBinding>() {
      @Override
      public boolean apply(MethodBinding methodBinding) {
        return methodBinding.isDefaultMethod();
      }
    });
  }

  /*
   * Implicit conversion helpers.
   */

  public static boolean requiresBoxing(int implicitConversion) {
    return implicitConversion != -1
        && (implicitConversion & TypeIds.BOXING) != 0;
  }

  public static boolean requiresUnboxing(int implicitConversion) {
    return implicitConversion != -1
        && (implicitConversion & TypeIds.UNBOXING) != 0;
  }

  public static BaseTypeBinding getBoxingPrimitiveType(ClassScope scope, int implicitConversion) {
    int typeId = (implicitConversion & TypeIds.IMPLICIT_CONVERSION_MASK) >> 4;
    return getBaseTypeBinding(scope, typeId);
  }

  public static BaseTypeBinding getUnboxingPrimitiveType(
      ClassScope scope, int implicitConversion) {
    if (needsCastBeforeUnbox(scope, implicitConversion)) {
      int typeId = (implicitConversion & TypeIds.IMPLICIT_CONVERSION_MASK) >> 4;
      return getBaseTypeBinding(scope, typeId);
    }
    int compileTypeId = implicitConversion & TypeIds.COMPILE_TYPE_MASK;
    return getBaseTypeBinding(scope, compileTypeId);
  }

  public static BaseTypeBinding getBaseTypeBinding(ClassScope scope, int typeId) {
    return  (BaseTypeBinding) TypeBinding.wellKnownType(scope, typeId);
  }

  public static ReferenceBinding getBoxedTypeBinding(
      ClassScope scope, BaseTypeBinding primitiveType) {
    return (ReferenceBinding) scope.boxing(primitiveType);
  }

  public static boolean needsCastBeforeUnbox(ClassScope scope, int implicitConversion) {
    // values of specific types like j.l.Object need casting before auto unboxing.
    int compileTypeId = implicitConversion & TypeIds.COMPILE_TYPE_MASK;
    return !(TypeBinding.wellKnownType(scope, compileTypeId) instanceof BaseTypeBinding);
  }

  private static final String VALUE_SUFFIX = "Value";
  private static final String VALUE_OF_METHOD_NAME = "valueOf";

  private static final char[] VALUE_SUFFIX_ = VALUE_SUFFIX.toCharArray();
  private static final char[] VALUE_OF_ = VALUE_OF_METHOD_NAME.toCharArray();

  public static MethodBinding getBoxingMethodBinding(
      ClassScope scope, BaseTypeBinding primitiveType) {
    ReferenceBinding boxType = (ReferenceBinding) scope.boxing(primitiveType);
    MethodBinding valueOfMethod = boxType.getExactMethod(VALUE_OF_,
        new TypeBinding[]{primitiveType}, scope.compilationUnitScope());
    assert valueOfMethod != null;
    return valueOfMethod;
  }

  public static MethodBinding getUnboxingMethodBinding(
      ClassScope scope, BaseTypeBinding primitiveType) {
    ReferenceBinding boxType = (ReferenceBinding) scope.boxing(primitiveType);
    char[] selector = CharOperation.concat(primitiveType.simpleName, VALUE_SUFFIX_);

    MethodBinding valueMethod =
        boxType.getExactMethod(selector, new TypeBinding[0], scope.compilationUnitScope());
    assert valueMethod != null;
    return valueMethod;
  }
}
