package org.checkerframework.dataflow.expression;

import java.util.List;
import java.util.Objects;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.javacutil.AnnotationProvider;
import org.checkerframework.javacutil.TypesUtils;
import org.plumelib.util.StringsPlume;

/** JavaExpression for array creations. {@code new String[]()}. */
public class ArrayCreation extends JavaExpression {

  /**
   * List of dimensions expressions. A {code null} element means that there is no dimension
   * expression for the given array level.
   */
  protected final List<@Nullable JavaExpression> dimensions;

  /** List of initializers. */
  protected final List<JavaExpression> initializers;

  /**
   * Creates an ArrayCreation object.
   *
   * @param type array type
   * @param dimensions list of dimension expressions; a {@code null} element means that there is no
   *     dimension expression for the given array level
   * @param initializers list of initializer expressions
   */
  public ArrayCreation(
      TypeMirror type,
      List<@Nullable JavaExpression> dimensions,
      List<JavaExpression> initializers) {
    super(type);
    assert type.getKind() == TypeKind.ARRAY;
    this.dimensions = dimensions;
    this.initializers = initializers;
  }

  /**
   * Returns a list representing the dimensions of this array creation. A {code null} element means
   * that there is no dimension expression for the given array level.
   *
   * @return a list representing the dimensions of this array creation
   */
  public List<@Nullable JavaExpression> getDimensions() {
    return dimensions;
  }

  public List<JavaExpression> getInitializers() {
    return initializers;
  }

  @SuppressWarnings("unchecked") // generic cast
  @Override
  public <T extends JavaExpression> @Nullable T containedOfClass(Class<T> clazz) {
    for (JavaExpression n : dimensions) {
      if (n != null && n.getClass() == clazz) {
        return (T) n;
      }
    }
    for (JavaExpression n : initializers) {
      if (n.getClass() == clazz) {
        return (T) n;
      }
    }
    return null;
  }

  @Override
  public boolean isDeterministic(AnnotationProvider provider) {
    return listIsDeterministic(dimensions, provider) && listIsDeterministic(initializers, provider);
  }

  @Override
  public boolean isAssignableByOtherCode() {
    return true;
  }

  @Override
  public boolean isModifiableByOtherCode() {
    return true;
  }

  @Override
  public int hashCode() {
    return Objects.hash(dimensions, initializers, getType().toString());
  }

  @Override
  public boolean equals(@Nullable Object obj) {
    if (!(obj instanceof ArrayCreation)) {
      return false;
    }
    ArrayCreation other = (ArrayCreation) obj;
    return this.dimensions.equals(other.getDimensions())
        && this.initializers.equals(other.getInitializers())
        // It might be better to use Types.isSameType(getType(), other.getType()), but I
        // don't have a Types object.
        && getType().toString().equals(other.getType().toString());
  }

  @Override
  public boolean syntacticEquals(JavaExpression je) {
    if (!(je instanceof ArrayCreation)) {
      return false;
    }
    ArrayCreation other = (ArrayCreation) je;
    return JavaExpression.syntacticEqualsList(this.dimensions, other.dimensions)
        && JavaExpression.syntacticEqualsList(this.initializers, other.initializers)
        && getType().toString().equals(other.getType().toString());
  }

  @Override
  public boolean containsSyntacticEqualJavaExpression(JavaExpression other) {
    return syntacticEquals(other)
        || JavaExpression.listContainsSyntacticEqualJavaExpression(dimensions, other)
        || JavaExpression.listContainsSyntacticEqualJavaExpression(initializers, other);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    if (dimensions.isEmpty()) {
      sb.append("new " + type);
    } else {
      sb.append("new " + TypesUtils.getInnermostComponentType((ArrayType) type));
      for (JavaExpression dim : dimensions) {
        sb.append("[");
        sb.append(dim == null ? "" : dim);
        sb.append("]");
      }
    }
    if (!initializers.isEmpty()) {
      sb.append(" {");
      sb.append(StringsPlume.join(", ", initializers));
      sb.append("}");
    }
    return sb.toString();
  }

  @Override
  public String toStringDebug() {
    return "\""
        + super.toStringDebug()
        + "\""
        + " type="
        + type
        + " dimensions="
        + dimensions
        + " initializers="
        + initializers;
  }

  @Override
  public <R, P> R accept(JavaExpressionVisitor<R, P> visitor, P p) {
    return visitor.visitArrayCreation(this, p);
  }
}
