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

import com.google.devtools.j2objc.util.ElementUtil;
import com.google.devtools.j2objc.util.TypeUtil;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.TypeParameterElement;
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.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.WildcardType;

/**
 * Generates various kinds of names for types:
 * - Unique type signature for identifying nodes.
 * - Qualified names used by suppress lists.
 * - Readable names for printing edges.
 *
 * @author Keith Stanger
 */
public class NameUtil {

  private final ElementUtil elementUtil;
  private final TypeUtil typeUtil;
  private final Map<Element, String> captureNames = new HashMap<>();
  private static int captureCount = 1;

  public NameUtil(TypeUtil typeUtil) {
    this.elementUtil = typeUtil.elementUtil();
    this.typeUtil = typeUtil;
  }

  /**
   * Generates a unique signature for this type that can be used as a key.
   */
  public String getSignature(TypeMirror type) {
    StringBuilder sb = new StringBuilder();
    buildTypeSignature(type, sb);
    return sb.toString();
  }

  private void buildTypeSignature(TypeMirror type, StringBuilder sb) {
    switch (type.getKind()) {
      case BOOLEAN:
      case BYTE:
      case CHAR:
      case DOUBLE:
      case FLOAT:
      case INT:
      case LONG:
      case SHORT:
      case VOID:
        sb.append(TypeUtil.getBinaryName(type));
        break;
      case ARRAY:
        sb.append('[');
        buildTypeSignature(((ArrayType) type).getComponentType(), sb);
        break;
      case DECLARED:
        buildDeclaredType((DeclaredType) type, sb);
        sb.append(';');
        break;
      case TYPEVAR:
        buildTypeVariable((TypeVariable) type, sb);
        break;
      case INTERSECTION:
        sb.append("&{");
        for (TypeMirror bound : ((IntersectionType) type).getBounds()) {
          buildTypeSignature(bound, sb);
        }
        sb.append('}');
        break;
      case WILDCARD:
        TypeMirror upperBound = ((WildcardType) type).getExtendsBound();
        TypeMirror lowerBound = ((WildcardType) type).getSuperBound();
        if (upperBound != null) {
          sb.append('+');
          buildTypeSignature(upperBound, sb);
        } else if (lowerBound != null) {
          sb.append('-');
          buildTypeSignature(lowerBound, sb);
        } else {
          sb.append('*');
        }
        break;
      default:
        throw new AssertionError("Unexpected type kind: " + type.getKind());
    }
  }

  private void buildDeclaredType(DeclaredType type, StringBuilder sb) {
    TypeMirror enclosing = type.getEnclosingType();
    if (TypeUtil.isNone(enclosing)) {
      sb.append('L');
      buildElementSignature(type.asElement(), sb);
    } else {
      buildDeclaredType((DeclaredType) enclosing, sb);
      String binaryName = elementUtil.getBinaryName(TypeUtil.asTypeElement(type));
      String enclosingBinaryName = elementUtil.getBinaryName(TypeUtil.asTypeElement(enclosing));
      assert binaryName.startsWith(enclosingBinaryName + "$");
      sb.append('.').append(binaryName.substring(enclosingBinaryName.length() + 1));
    }
    buildTypeArguments(type, sb);
  }

  private void buildTypeArguments(DeclaredType type, StringBuilder sb) {
    Element elem = type.asElement();
    List<? extends TypeMirror> typeArguments = type.getTypeArguments();
    if (!typeArguments.isEmpty()) {
      sb.append('<');
      for (TypeMirror typeArg : typeArguments) {
        TypeParameterElement typeParam = TypeUtil.asTypeParameterElement(typeArg);
        if (typeParam != null && elem.equals(typeParam.getEnclosingElement())) {
          // The type param is directly declared by the type being emitted so we don't need to fully
          // qualify it as buildTypeSignature() would.
          sb.append('T');
          sb.append(ElementUtil.getName(typeParam));
          sb.append(';');
        } else {
          buildTypeSignature(typeArg, sb);
        }
      }
      sb.append('>');
    }
  }

  private void buildTypeVariable(TypeVariable type, StringBuilder sb) {
    TypeParameterElement typeParam = (TypeParameterElement) type.asElement();
    if (isCapture(typeParam)) {
      String name = captureNames.get(typeParam);
      if (name == null) {
        name = "!CAP" + captureCount++ + '!';
        captureNames.put(typeParam, name);
      }
      sb.append(name);
    } else {
      sb.append("T:");
      buildElementSignature(typeParam.getEnclosingElement(), sb);
      sb.append(':').append(ElementUtil.getName(typeParam)).append(';');
    }
  }

  private void buildElementSignature(Element elem, StringBuilder sb) {
    if (ElementUtil.isTypeElement(elem)) {
      sb.append(elementUtil.getBinaryName((TypeElement) elem).replace('.', '/'));
    } else if (ElementUtil.isExecutableElement(elem)) {
      buildExecutableElementSignature((ExecutableElement) elem, sb);
    } else {
      throw new AssertionError("Unexpected element: " + elem + " with kind: " + elem.getKind());
    }
  }

  private void buildExecutableElementSignature(ExecutableElement elem, StringBuilder sb) {
    buildElementSignature(elem.getEnclosingElement(), sb);
    sb.append('.').append(ElementUtil.getName(elem)).append('(');
    for (VariableElement var : elem.getParameters()) {
      buildTypeSignature(typeUtil.erasure(var.asType()), sb);
    }
    sb.append(')');
  }

  /**
   * Generates a readable name for this type.
   */
  public static String getName(TypeMirror type) {
    StringBuilder sb = new StringBuilder();
    buildTypeName(type, sb);
    return sb.toString();
  }

  private static void buildTypeName(TypeMirror type, StringBuilder sb) {
    switch (type.getKind()) {
      case BOOLEAN:
      case BYTE:
      case CHAR:
      case DOUBLE:
      case FLOAT:
      case INT:
      case LONG:
      case SHORT:
        sb.append(TypeUtil.getName(type));
        break;
      case ARRAY:
        buildTypeName(((ArrayType) type).getComponentType(), sb);
        sb.append("[]");
        break;
      case DECLARED: {
        sb.append(ElementUtil.getName(TypeUtil.asTypeElement(type)));
        List<? extends TypeMirror> typeArguments = ((DeclaredType) type).getTypeArguments();
        if (!typeArguments.isEmpty()) {
          sb.append('<');
          boolean first = true;
          for (TypeMirror typeArg : typeArguments) {
            if (!first) {
              sb.append(", ");
            }
            first = false;
            buildTypeName(typeArg, sb);
          }
          sb.append('>');
        }
        break;
      }
      case TYPEVAR:
        sb.append(ElementUtil.getName(((TypeVariable) type).asElement()));
        break;
      case INTERSECTION: {
        boolean first = true;
        for (TypeMirror bound : ((IntersectionType) type).getBounds()) {
          if (!first) {
            sb.append(" & ");
          }
          first = false;
          buildTypeName(bound, sb);
        }
        break;
      }
      case WILDCARD: {
        sb.append('?');
        TypeMirror extendsBound = ((WildcardType) type).getExtendsBound();
        TypeMirror superBound = ((WildcardType) type).getSuperBound();
        if (extendsBound != null) {
          sb.append(" extends ");
          buildTypeName(extendsBound, sb);
        }
        if (superBound != null) {
          sb.append(" super ");
          buildTypeName(superBound, sb);
        }
        break;
      }
      default:
        throw new AssertionError("Unexpected type: " + type + " with kind: " + type.getKind());
    }
  }

  /**
   * Gets the qualified name for this type, as expected by a NameList instance.
   */
  public static String getQualifiedName(TypeMirror type) {
    switch (type.getKind()) {
      case DECLARED:
        return getQualifiedNameForTypeElement((TypeElement) ((DeclaredType) type).asElement());
      case TYPEVAR:
        return getQualifiedNameForElement(((TypeVariable) type).asElement());
      case INTERSECTION:
        return "&";
      case WILDCARD:
        return "?";
      default:
        throw new AssertionError("Unexpected type: " + type + " with kind: " + type.getKind());
    }
  }

  private static String getQualifiedNameForTypeElement(TypeElement type) {
    switch (type.getNestingKind()) {
      case ANONYMOUS:
        return getQualifiedNameForElement(type.getEnclosingElement()) + ".$";
      case LOCAL:
        return getQualifiedNameForElement(type.getEnclosingElement()) + '.'
            + ElementUtil.getName(type);
      default:
        return ElementUtil.getQualifiedName(type);
    }
  }

  private static String getQualifiedNameForElement(Element e) {
    if (isCapture(e)) {
      return "!";
    } else if (ElementUtil.isTypeElement(e)) {
      return getQualifiedNameForTypeElement((TypeElement) e);
    } else if (ElementUtil.isExecutableElement(e) || ElementUtil.isTypeParameterElement(e)) {
      return getQualifiedNameForElement(e.getEnclosingElement()) + '.' + ElementUtil.getName(e);
    }
    return getQualifiedNameForElement(e.getEnclosingElement());
  }

  private static boolean isCapture(Element e) {
    return ElementUtil.isTypeParameterElement(e)
        && e.getEnclosingElement().getKind() == ElementKind.OTHER;
  }
}
