/*
 * Copyright (C) 2015 Square, 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.squareup.javapoet;

import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.lang.model.element.NestingKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;

import static com.squareup.javapoet.Util.checkArgument;
import static com.squareup.javapoet.Util.checkNotNull;

public final class TypeVariableName extends TypeName {
  public final String name;
  public final List<TypeName> bounds;

  private TypeVariableName(String name, List<TypeName> bounds) {
    // Strip java.lang.Object from bounds if it is present.
    List<TypeName> boundsNoObject = new ArrayList<>(bounds);
    boundsNoObject.remove(OBJECT);

    this.name = checkNotNull(name, "name == null");
    this.bounds = Collections.unmodifiableList(boundsNoObject);

    for (TypeName bound : this.bounds) {
      checkArgument(!bound.isPrimitive() && bound != VOID, "invalid bound: %s", bound);
    }
  }

  @Override public boolean equals(Object o) {
    return o instanceof TypeVariableName
        && ((TypeVariableName) o).name.equals(name)
        && ((TypeVariableName) o).bounds.equals(bounds);
  }

  @Override public int hashCode() {
    return name.hashCode() ^ bounds.hashCode();
  }

  @Override CodeWriter emit(CodeWriter out) throws IOException {
    return out.emitAndIndent(name);
  }

  /** Returns type variable named {@code name} without bounds. */
  public static TypeVariableName get(String name) {
    return new TypeVariableName(name, Collections.<TypeName>emptyList());
  }

  /** Returns type variable named {@code name} with {@code bounds}. */
  public static TypeVariableName get(String name, TypeName... bounds) {
    return new TypeVariableName(name, Arrays.asList(bounds));
  }

  /** Returns type variable named {@code name} with {@code bounds}. */
  public static TypeVariableName get(String name, Type... bounds) {
    return new TypeVariableName(name, TypeName.list(bounds));
  }

  /** Returns type variable equivalent to {@code mirror}. */
  public static TypeVariableName get(javax.lang.model.type.TypeVariable mirror) {
    String name = mirror.asElement().getSimpleName().toString();
    List<? extends TypeMirror> boundsMirrors = typeVariableBounds(mirror);

    List<TypeName> boundsTypeNames = new ArrayList<>();
    for (TypeMirror typeMirror : boundsMirrors) {
      boundsTypeNames.add(TypeName.get(typeMirror));
    }

    return new TypeVariableName(name, boundsTypeNames);
  }

  /**
   * Returns a list of type mirrors representing the unpacked bounds of {@code typeVariable}. This
   * is made gnarly by the need to unpack Java 8's new IntersectionType with reflection. We don't
   * have that type in Java 7, and {@link TypeVariable}'s array of bounds is sufficient anyway.
   */
  @SuppressWarnings("unchecked") // Gross things in support of Java 7 and Java 8.
  private static List<? extends TypeMirror> typeVariableBounds(
      javax.lang.model.type.TypeVariable typeVariable) {
    TypeMirror upperBound = typeVariable.getUpperBound();

    // On Java 8, unwrap an intersection type into its component bounds.
    if ("INTERSECTION".equals(upperBound.getKind().name())) {
      try {
        Method method = upperBound.getClass().getMethod("getBounds");
        return (List<? extends TypeMirror>) method.invoke(upperBound);
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }

    // On Java 7, intersection types exist but without explicit API. Use a (clumsy) heuristic.
    if (upperBound.getKind() == TypeKind.DECLARED) {
      TypeElement upperBoundElement = (TypeElement) ((DeclaredType) upperBound).asElement();
      if (upperBoundElement.getNestingKind() == NestingKind.ANONYMOUS) {
        List<TypeMirror> result = new ArrayList<>();
        result.add(upperBoundElement.getSuperclass());
        result.addAll(upperBoundElement.getInterfaces());
        return result;
      }
    } else if (upperBound.getKind() == TypeKind.TYPEVAR) {
      // Workaround for intersection types in Eclipse:
      // Eclipse returns an equivalent TypeVariable for intersection types
      // (wrapping the same internal TypeVariableBinding).
      TypeParameterElement upperBoundElement =
          (TypeParameterElement) ((javax.lang.model.type.TypeVariable) upperBound).asElement();
      if (upperBoundElement.equals(typeVariable.asElement())) {
        return upperBoundElement.getBounds();
      }
    }

    return Collections.singletonList(upperBound);
  }

  /** Returns type variable equivalent to {@code type}. */
  public static TypeVariableName get(TypeVariable<?> type) {
    return new TypeVariableName(type.getName(), TypeName.list(type.getBounds()));
  }
}

@@ -653,41 +653,8 @@
 on;%0A
-import java.lang.reflect.Method;%0A
 impo
@@ -861,200 +861,28 @@
 ent.
-NestingKind;%0Aimport javax.lang.model.element.TypeElement;%0Aimport javax.lang.model.element.TypeParameterElement;%0Aimport javax.lang.model.type.DeclaredType;%0Aimport javax.lang.model.type.TypeKind
+TypeParameterElement
 ;%0Aim
@@ -2794,364 +2794,60 @@
     
-String name = mirror.asElement().getSimpleName().toString();%0A    List%3C? extends TypeMirror%3E boundsMirrors = typeVariableBounds(mirror);%0A%0A    List%3CTypeName%3E boundsTypeNames = new ArrayList%3C%3E();%0A    for (TypeMirror typeMirror : boundsMirrors) %7B%0A      boundsTypeNames.add(TypeName.get(typeMirror));%0A    %7D%0A%0A    return new TypeVariableName(name, boundsTypeNames
+return get((TypeParameterElement) mirror.asElement()
 );%0A 
@@ -2855,21 +2855,16 @@
 %7D%0A%0A  /**
-%0A   *
  Returns
@@ -2868,2002 +2868,467 @@
 rns 
-a list of type mirrors representing the unpacked bounds of %7B@code typeVariable%7D. This%0A   * is made gnarly by the need to unpack Java 8's new IntersectionType with reflection. We don't%0A   * have that type in Java 7, and %7B@link TypeVariable%7D's array of bounds is sufficient anyway.%0A   */%0A  @SuppressWarnings(%22unchecked%22) // Gross things in support of Java 7 and Java 8.%0A  private static List%3C? extends TypeMirror%3E typeVariableBounds(%0A      javax.lang.model.type.
+type variable equivalent to %7B@code element%7D. */%0A  public static TypeVariableName get(
 Type
-Variable typeVariable) %7B%0A    TypeMirror upperBound = typeVariable.getUpperBound();%0A%0A    // On Java 8, unwrap an intersection type into its component bounds.%0A    if (%22INTERSECTION%22.equals(upperBound.getKind().name())) %7B%0A      try %7B%0A        Method method = upperBound.getClass().getMethod(%22getBounds%22);%0A        return (List%3C? extends TypeMirror%3E) method.invoke(upperBound);%0A      %7D catch (Exception e) %7B%0A        throw new RuntimeException(e);%0A      %7D%0A    %7D%0A%0A    // On Java 7, intersection types exist but without explicit API. Use a (clumsy) heuristic.%0A    if (upperBound.getKind() == TypeKind.DECLARED) %7B%0A      TypeElement upperBoundElement = (TypeElement) ((DeclaredType) upperBound).asElement
+ParameterElement element) %7B%0A    String name = element.getSimpleName().toString();%0A    List%3C? extends TypeMirror%3E boundsMirrors = element.getBounds
 ();%0A
+%0A
     
-  if (upperBoundElement.getNestingKind() == NestingKind.ANONYMOUS) %7B%0A        List%3CTypeMirror%3E result = new ArrayList%3C%3E();%0A        result.add(upperBoundElement.getSuperclass());%0A        result.addAll(upperBoundElement.getInterfaces());%0A        return result;%0A      %7D%0A    %7D else if (upperBound.getKind() == TypeKind.TYPEVAR) %7B%0A      // Workaround for intersection types in Eclipse:%0A      // Eclipse returns an equivalent TypeVariable for intersection types%0A      // (wrapping the same internal TypeVariableBinding).%0A      TypeParameterElement upperBoundElement =%0A          (TypeParameterElement) ((javax.lang.model.type.TypeVariable) upperBound).asElement();%0A      if (upperBoundElement.equals(typeVariable.asElement())) %7B%0A        return upperBoundElement.getBounds();%0A      %7D%0A    %7D%0A%0A    return Collections.singletonList(upperBound
+List%3CTypeName%3E boundsTypeNames = new ArrayList%3C%3E();%0A    for (TypeMirror typeMirror : boundsMirrors) %7B%0A      boundsTypeNames.add(TypeName.get(typeMirror));%0A    %7D%0A%0A    return new TypeVariableName(name, boundsTypeNames
 );%0A 

Simplify TypeVariableName.get(TypeVariable)