/*
 * Copyright (c) 2006 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */
package com.ibm.wala.types.annotations;

import com.ibm.wala.shrike.shrikeCT.AnnotationsReader;
import com.ibm.wala.shrike.shrikeCT.AnnotationsReader.AnnotationAttribute;
import com.ibm.wala.shrike.shrikeCT.AnnotationsReader.ElementValue;
import com.ibm.wala.shrike.shrikeCT.InvalidClassFileException;
import com.ibm.wala.types.ClassLoaderReference;
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.collections.HashSetFactory;
import com.ibm.wala.util.collections.Pair;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;

/** Represents a member annotation, e.g., Java 5.0 class file annotations */
public class Annotation {

  private final TypeReference type;

  /**
   * named arguments to the annotation, represented as a mapping from name to value. Note that for
   * Java annotation arguments, the values are always Strings, independent of their actual type in
   * the bytecode.
   */
  private final Map<String, ElementValue> namedArguments;

  /**
   * unnamed arguments to the annotation (e.g., constructor arguments for C# attributes),
   * represented as an array of pairs (T,V), where T is the argument type and V is the value. The
   * array preserves the order in which the arguments were passed. If null, there are no unnamed
   * arguments.
   */
  private final Pair<TypeReference, Object>[] unnamedArguments;

  private Annotation(
      TypeReference type,
      Map<String, ElementValue> namedArguments,
      Pair<TypeReference, Object>[] unnamedArguments) {
    this.type = type;
    if (namedArguments == null) {
      throw new IllegalArgumentException("namedArguments is null");
    }
    this.namedArguments = namedArguments;
    this.unnamedArguments = unnamedArguments;
  }

  public static Annotation makeUnnamedAndNamed(
      TypeReference t,
      Map<String, ElementValue> namedArguments,
      Pair<TypeReference, Object>[] unnamedArguments) {
    return new Annotation(t, namedArguments, unnamedArguments);
  }

  public static Annotation makeWithUnnamed(
      TypeReference t, Pair<TypeReference, Object>[] unnamedArguments) {
    return new Annotation(t, Collections.<String, ElementValue>emptyMap(), unnamedArguments);
  }

  public static Annotation make(TypeReference t) {
    return new Annotation(t, Collections.<String, ElementValue>emptyMap(), null);
  }

  public static Annotation makeWithNamed(
      TypeReference t, Map<String, ElementValue> namedArguments) {
    return new Annotation(t, namedArguments, null);
  }

  public static Collection<Annotation> getAnnotationsFromReader(
      AnnotationsReader r, ClassLoaderReference clRef) throws InvalidClassFileException {
    if (r != null) {
      AnnotationAttribute[] allAnnotations = r.getAllAnnotations();
      Collection<Annotation> result = convertToAnnotations(clRef, allAnnotations);
      return result;
    } else {
      return Collections.emptySet();
    }
  }

  /**
   * If r != null, return parameter annotations as an array with length equal to number of
   * annotatable parameters. Otherwise, return null.
   */
  @SuppressWarnings("unchecked")
  public static Collection<Annotation>[] getParameterAnnotationsFromReader(
      AnnotationsReader r, ClassLoaderReference clRef) throws InvalidClassFileException {
    if (r != null) {
      AnnotationAttribute[][] allAnnots = r.getAllParameterAnnotations();
      Collection<Annotation>[] result = new Collection[allAnnots.length];
      Arrays.setAll(result, i -> convertToAnnotations(clRef, allAnnots[i]));
      return result;
    } else {
      return null;
    }
  }

  protected static Collection<Annotation> convertToAnnotations(
      ClassLoaderReference clRef, AnnotationAttribute[] allAnnotations) {
    Collection<Annotation> result = HashSetFactory.make();
    for (AnnotationAttribute annot : allAnnotations) {
      String type = annot.type;
      type = type.replaceAll(";", "");
      TypeReference t = TypeReference.findOrCreate(clRef, type);
      result.add(makeWithNamed(t, annot.elementValues));
    }
    return result;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder("Annotation type " + type);
    if (unnamedArguments != null) {
      sb.append('[');
      for (Pair<TypeReference, Object> arg : unnamedArguments) {
        sb.append(' ').append(arg.fst.getName().getClassName()).append(':').append(arg.snd);
      }
      sb.append(" ]");
    }
    if (!namedArguments.isEmpty()) {
      sb.append(' ').append(new TreeMap<>(namedArguments));
    }
    return sb.toString();
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((namedArguments == null) ? 0 : namedArguments.hashCode());
    result = prime * result + ((type == null) ? 0 : type.hashCode());
    result = prime * result + Arrays.hashCode(unnamedArguments);
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null) return false;
    if (getClass() != obj.getClass()) return false;
    Annotation other = (Annotation) obj;
    if (namedArguments == null) {
      if (other.namedArguments != null) return false;
    } else if (!namedArguments.equals(other.namedArguments)) return false;
    if (type == null) {
      if (other.type != null) return false;
    } else if (!type.equals(other.type)) return false;
    if (!Arrays.equals(unnamedArguments, other.unnamedArguments)) return false;
    return true;
  }

  /**
   * Get the unnamed arguments to the annotation (e.g., constructor arguments for C# attributes),
   * represented as an array of pairs (T,V), where T is the argument type and V is the value. The
   * array preserves the order in which the arguments were passed. If null, there are no unnamed
   * arguments.
   */
  public Pair<TypeReference, Object>[] getUnnamedArguments() {
    return unnamedArguments;
  }

  /** Get the named arguments to the annotation, represented as a mapping from name to value */
  public Map<String, ElementValue> getNamedArguments() {
    return namedArguments;
  }

  /** Get the type of the annotation */
  public TypeReference getType() {
    return type;
  }
}
