/*
 * Copyright 2017-present Facebook, 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.facebook.buck.jvm.java.abi;

import com.google.common.base.Preconditions;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.IntersectionType;
import javax.lang.model.type.NoType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleElementVisitor8;
import javax.lang.model.util.SimpleTypeVisitor8;
import org.objectweb.asm.Type;

/** Computes type descriptors from {@link Element}s or {@link TypeMirror}s. */
class DescriptorFactory {
  private final Elements elements;

  public DescriptorFactory(Elements elements) {
    this.elements = elements;
  }

  public String getDescriptor(TypeMirror type) {
    return getType(type).getDescriptor();
  }

  @Nullable
  public String getDescriptor(Element element) {
    ElementKind kind = element.getKind();
    if (kind.isClass() || kind.isInterface()) {
      return null;
    }
    return getType(element).getDescriptor();
  }

  private Type getType(Element element) {
    return Preconditions.checkNotNull(
        element.accept(
            new SimpleElementVisitor8<Type, Void>() {
              @Override
              protected Type defaultAction(Element e, Void aVoid) {
                throw new IllegalArgumentException(
                    String.format("Unexpected element kind: %s", element.getKind()));
              }

              @Override
              public Type visitExecutable(ExecutableElement e, Void aVoid) {
                Stream<TypeMirror> parameterTypeStream =
                    e.getParameters().stream().map(Element::asType);

                if (MoreElements.isInnerClassConstructor(e)) {
                  parameterTypeStream =
                      Stream.concat(
                          Stream.of(MoreElements.getOuterClass(e).asType()), parameterTypeStream);
                }

                return Type.getMethodType(
                    getType(e.getReturnType()),
                    parameterTypeStream
                        .map(DescriptorFactory.this::getType)
                        .toArray(size -> new Type[size]));
              }

              @Override
              public Type visitVariable(VariableElement e, Void aVoid) {
                return getType(e.asType());
              }
            },
            null));
  }

  public Type getType(TypeMirror typeMirror) {
    return Preconditions.checkNotNull(
        typeMirror.accept(
            new SimpleTypeVisitor8<Type, Void>() {
              @Override
              protected Type defaultAction(TypeMirror t, Void aVoid) {
                throw new IllegalArgumentException(
                    String.format("Unexpected type kind: %s", t.getKind()));
              }

              @Override
              public Type visitPrimitive(PrimitiveType type, Void aVoid) {
                switch (type.getKind()) {
                  case BOOLEAN:
                    return Type.BOOLEAN_TYPE;
                  case BYTE:
                    return Type.BYTE_TYPE;
                  case CHAR:
                    return Type.CHAR_TYPE;
                  case SHORT:
                    return Type.SHORT_TYPE;
                  case INT:
                    return Type.INT_TYPE;
                  case LONG:
                    return Type.LONG_TYPE;
                  case FLOAT:
                    return Type.FLOAT_TYPE;
                  case DOUBLE:
                    return Type.DOUBLE_TYPE;
                    // $CASES-OMITTED$
                  default:
                    throw new IllegalArgumentException(
                        String.format("Unexpected type kind: %s", type.getKind()));
                }
              }

              @Override
              public Type visitNoType(NoType t, Void aVoid) {
                if (t.getKind() != TypeKind.VOID) {
                  throw new IllegalArgumentException(
                      String.format("Unexpected type kind: %s", t.getKind()));
                }

                return Type.VOID_TYPE;
              }

              @Override
              public Type visitArray(ArrayType t, Void aVoid) {
                return Type.getObjectType("[" + getDescriptor(t.getComponentType()));
              }

              @Override
              public Type visitDeclared(DeclaredType t, Void aVoid) {
                // The erasure of a parameterized type is just the unparameterized version (JLS8 4.6)
                return Type.getObjectType(getInternalName((TypeElement) t.asElement()));
              }

              @Override
              public Type visitTypeVariable(TypeVariable t, Void aVoid) {
                // The erasure of a type variable is the erasure of its leftmost bound (JLS8 4.6)
                // If there's only one bound, getUpperBound returns it directly; if there's more than
                // one it returns an IntersectionType
                return getType(t.getUpperBound());
              }

              @Override
              public Type visitIntersection(IntersectionType t, Void aVoid) {
                // The erasure of a type variable is the erasure of its leftmost bound (JLS8 4.6)
                return getType(t.getBounds().get(0));
              }
            },
            null));
  }

  /** Gets the internal form of the binary name; see JVMS8 4.2.1 */
  public String getInternalName(TypeMirror typeMirror) {
    return getType(typeMirror).getInternalName();
  }

  /** Gets the internal form of the binary name; see JVMS8 4.2.1 */
  public String getInternalName(TypeElement typeElement) {
    return elements.getBinaryName(typeElement).toString().replace('.', '/');
  }
}

@@ -5521,77 +5521,531 @@
     
-return Type.getObjectType(getInternalName((TypeElement) t.as
+TypeElement typeElement = (TypeElement) t.asElement();%0A%0A                if (typeElement.getQualifiedName().contentEquals(%22%22)) %7B%0A                  // javac 7 uses a DeclaredType to model an intersection type%0A                  if (typeElement.getKind() == ElementKind.INTERFACE) %7B%0A                    return getType(typeElement.getInterfaces().get(0));%0A                  %7D%0A%0A                  return getType(typeElement.getSuperclass());%0A                %7D%0A%0A                return Type.getObjectType(getInternalName(type
 Element
-()
 ));%0A

handle javac 7 intersection types