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

import com.google.common.base.Preconditions;
import com.google.devtools.j2objc.util.ElementUtil;
import com.google.devtools.j2objc.util.NameTable;
import com.google.devtools.j2objc.util.TypeUtil;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ElementVisitor;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.NestingKind;
import javax.lang.model.element.PackageElement;
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 javax.lang.model.type.TypeVisitor;

/**
 * Element class for types created during translation.
 *
 * @author Keith Stanger
 */
public class GeneratedTypeElement extends GeneratedElement implements TypeElement {

  private final TypeMirror superclass;
  private List<TypeMirror> interfaces = new ArrayList<>();
  private final NestingKind nestingKind;
  private final Name qualifiedName;
  private final String header;
  private final boolean isIosType;

  protected GeneratedTypeElement(
      String name, ElementKind kind, Element enclosingElement, TypeMirror superclass,
      NestingKind nestingKind, String header, boolean isIosType, boolean synthetic) {
    super(Preconditions.checkNotNull(name), checkElementKind(kind), enclosingElement, synthetic);
    this.superclass = superclass;
    this.nestingKind = nestingKind;
    qualifiedName = new NameImpl(getQualifiedPrefix(enclosingElement) + name);
    this.header = header;
    this.isIosType = isIosType;
  }

  public static GeneratedTypeElement mutableCopy(TypeElement element) {
    GeneratedTypeElement generatedTypeElement =
        new GeneratedTypeElement(
            element.getSimpleName().toString(),
            element.getKind(),
            element.getEnclosingElement(),
            element.getSuperclass(),
            element.getNestingKind(),
            ElementUtil.getHeader(element),
            ElementUtil.isIosType(element),
            ElementUtil.isSynthetic(element));
    generatedTypeElement.addModifiers(element.getModifiers());
    generatedTypeElement.addAnnotationMirrors(element.getAnnotationMirrors());
    return generatedTypeElement;
  }

  private static GeneratedTypeElement newEmulatedType(
      String qualifiedName, ElementKind kind, TypeMirror superclass) {
    int idx = qualifiedName.lastIndexOf('.');
    String packageName = idx < 0 ? "" : qualifiedName.substring(0, idx);
    PackageElement packageElement = new GeneratedPackageElement(packageName);
    return new GeneratedTypeElement(
        qualifiedName.substring(idx + 1), kind, packageElement, superclass, NestingKind.TOP_LEVEL,
        null, false, false);
  }

  public static GeneratedTypeElement newEmulatedClass(String qualifiedName, TypeMirror superclass) {
    return newEmulatedType(qualifiedName, ElementKind.CLASS, superclass);
  }

  public static GeneratedTypeElement newEmulatedInterface(String qualifiedName) {
    return newEmulatedType(qualifiedName, ElementKind.INTERFACE, null);
  }

  public static GeneratedTypeElement newIosType(
      String name, ElementKind kind, TypeElement superclass, String header) {
    return new GeneratedTypeElement(
        name, kind, null, superclass != null ? superclass.asType() : null, NestingKind.TOP_LEVEL,
        header, true, false);
  }

  public static GeneratedTypeElement newIosClass(
      String name, TypeElement superclass, String header) {
    return newIosType(name, ElementKind.CLASS, superclass, header);
  }

  public static GeneratedTypeElement newIosInterface(String name, String header) {
    return newIosType(name, ElementKind.INTERFACE, null, header);
  }

  public static GeneratedTypeElement newPackageInfoClass(
      PackageElement pkgElem, TypeUtil typeUtil) {
    return (GeneratedTypeElement) new GeneratedTypeElement(
        NameTable.PACKAGE_INFO_CLASS_NAME, ElementKind.CLASS, pkgElem,
        typeUtil.getJavaObject().asType(), NestingKind.TOP_LEVEL, null, false, false)
        .addModifiers(Modifier.PRIVATE);
  }

  private static ElementKind checkElementKind(ElementKind kind) {
    Preconditions.checkArgument(kind.isClass() || kind.isInterface());
    return kind;
  }

  private static String getQualifiedPrefix(Element enclosing) {
    if (enclosing == null) {
      return "";
    } else if (ElementUtil.isTypeElement(enclosing)) {
      return ((TypeElement) enclosing).getQualifiedName().toString() + '.';
    } else if (ElementUtil.isPackage(enclosing)) {
      PackageElement pkg = (PackageElement) enclosing;
      return pkg.isUnnamed() ? "" : pkg.getQualifiedName().toString() + '.';
    } else {
      return getQualifiedPrefix(enclosing.getEnclosingElement());
    }
  }

  @Override
  public TypeMirror asType() {
    return new Mirror();
  }

  @Override
  public Name getQualifiedName() {
    return qualifiedName;
  }

  public String getHeader() {
    return header;
  }

  public boolean isIosType() {
    return isIosType;
  }

  @Override
  public TypeMirror getSuperclass() {
    return superclass;
  }

  @Override
  public NestingKind getNestingKind() {
    return nestingKind;
  }

  @Override
  public List<? extends TypeMirror> getInterfaces() {
    return interfaces;
  }

  public List<? extends TypeMirror> getDirectSupertypes() {
    List<TypeMirror> result = new ArrayList<>(interfaces);
    if (superclass != null) {
      result.add(0, superclass);  // Superclass must be first.
    }
    return result;
  }

  public void addInterface(TypeMirror t) {
    interfaces.add(t);
  }

  public void addInterfaces(Collection<? extends TypeMirror> types) {
    interfaces.addAll(types);
  }

  @Override
  public List<? extends TypeParameterElement> getTypeParameters() {
    return Collections.emptyList();
  }

  @Override
  public <R, P> R accept(ElementVisitor<R, P> v, P p) {
    return v.visitType(this, p);
  }

  /** The associated TypeMirror. */
  public class Mirror extends AbstractTypeMirror implements DeclaredType {

    @Override
    public TypeKind getKind() {
      return TypeKind.DECLARED;
    }

    @Override
    public Element asElement() {
      return GeneratedTypeElement.this;
    }

    @Override
    public TypeMirror getEnclosingType() {
      TypeElement declaringClass = ElementUtil.getDeclaringClass(GeneratedTypeElement.this);
      return declaringClass == null ? null : declaringClass.asType();
    }

    @Override
    public List<? extends TypeMirror> getTypeArguments() {
      return Collections.emptyList();
    }

    @Override
    public <A extends Annotation> A getAnnotation(Class<A> annotationType) {
      return GeneratedTypeElement.this.getAnnotation(annotationType);
    }

    @Override
    public List<? extends AnnotationMirror> getAnnotationMirrors() {
      return GeneratedTypeElement.this.getAnnotationMirrors();
    }

    @Override
    public <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationType) {
      return GeneratedTypeElement.this.getAnnotationsByType(annotationType);
    }

    @Override
    public <R, P> R accept(TypeVisitor<R, P> v, P p) {
      return v.visitDeclared(this, p);
    }

    @Override
    public boolean equals(Object obj) {
      return obj instanceof Mirror && ((Mirror) obj).asElement().equals(GeneratedTypeElement.this);
    }

    @Override
    public int hashCode() {
      return 31 * GeneratedTypeElement.this.hashCode();
    }

    @Override
    public String toString() {
      return GeneratedTypeElement.this.getQualifiedName().toString();
    }
  }
}
