/*
 * Copyright (c) 2002 - 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;

import com.ibm.wala.core.util.strings.Atom;
import com.ibm.wala.core.util.strings.ImmutableByteArray;
import com.ibm.wala.core.util.strings.StringStuff;
import com.ibm.wala.util.collections.HashMapFactory;
import com.ibm.wala.util.debug.Assertions;
import java.io.Serializable;
import java.io.UTFDataFormatException;
import java.util.Map;

/**
 * We've introduced this class to canonicalize Atoms that represent package names.
 *
 * <p>NB: All package names should use '/' and not '.' as a separator. eg. Ljava/lang/Class
 */
public final class TypeName implements Serializable {

  public static final byte ArrayMask = 0x01;
  public static final byte PointerMask = 0x02;
  public static final byte ReferenceMask = 0x03;
  public static final byte PrimitiveMask = 0x04;

  public static final byte ElementMask = 0x07;
  public static final byte ElementBits = 3;

  /* Serial version */
  private static final long serialVersionUID = -3256390509887654326L;

  /** canonical mapping from TypeNameKey -&gt; TypeName */
  private static final Map<TypeNameKey, TypeName> map = HashMapFactory.make();

  private static synchronized TypeName findOrCreate(TypeNameKey t) {
    TypeName result = map.get(t);
    if (result == null) {
      result = new TypeName(t);
      map.put(t, result);
    }
    return result;
  }

  /** The key object holds all the information about a type name */
  private final TypeNameKey key;

  public static TypeName findOrCreate(ImmutableByteArray name, int start, int length)
      throws IllegalArgumentException {
    Atom className = Atom.findOrCreate(StringStuff.parseForClass(name, start, length));
    ImmutableByteArray p = StringStuff.parseForPackage(name, start, length);
    Atom packageName = (p == null) ? null : Atom.findOrCreate(p);
    int dim = StringStuff.parseForArrayDimensionality(name, start, length);
    boolean innermostPrimitive = StringStuff.classIsPrimitive(name, start, length);
    if (innermostPrimitive) {
      if (dim == 0) {
        dim = -1;
      } else {
        dim <<= ElementBits;
        dim |= PrimitiveMask;
      }
    }
    TypeNameKey t = new TypeNameKey(packageName, className, dim);
    return findOrCreate(t);
  }

  public static TypeName findOrCreate(ImmutableByteArray name) throws IllegalArgumentException {
    if (name == null) {
      throw new IllegalArgumentException("name is null");
    }
    return findOrCreate(name, 0, name.length());
  }

  public static TypeName findOrCreate(String name) throws IllegalArgumentException {
    ImmutableByteArray b = ImmutableByteArray.make(name);
    return findOrCreate(b);
  }

  public static TypeName findOrCreateClass(Atom packageName, Atom className) {
    if (packageName == null) {
      throw new IllegalArgumentException("null packageName");
    }
    if (className == null) {
      throw new IllegalArgumentException("null className");
    }
    TypeNameKey T = new TypeNameKey(packageName, className, 0);
    return findOrCreate(T);
  }

  public static TypeName findOrCreate(Atom packageName, Atom className, int dim) {
    TypeNameKey T = new TypeNameKey(packageName, className, dim);
    return findOrCreate(T);
  }

  /** This should be the only constructor */
  private TypeName(TypeNameKey key) {
    this.key = key;
  }

  @Override
  public boolean equals(Object obj) {
    return this == obj;
  }

  @Override
  public int hashCode() {
    return key.hashCode();
  }

  @Override
  public String toString() {
    return key.toString();
  }

  public String toUnicodeString() {
    return key.toUnicodeString();
  }

  /**
   * @param s a String like Ljava/lang/Object
   * @return the corresponding TypeName
   * @throws IllegalArgumentException if s is null
   */
  public static TypeName string2TypeName(String s) throws IllegalArgumentException {
    if (s == null) {
      throw new IllegalArgumentException("s is null");
    }
    byte[] val = s.getBytes();

    return findOrCreate(new ImmutableByteArray(val));
  }

  public static TypeName findOrCreateClassName(String packageName, String className) {
    Atom p = Atom.findOrCreateUnicodeAtom(packageName);
    Atom c = Atom.findOrCreateUnicodeAtom(className);
    return findOrCreateClass(p, c);
  }

  /** @return the name of the array element type for an array */
  public TypeName parseForArrayElementName() {
    int newDim;
    if ((key.dim & ElementMask) == PrimitiveMask) {
      int tmpDim = key.dim >> (2 * ElementBits);
      if (tmpDim == 0) {
        newDim = -1;
      } else {
        newDim = (tmpDim << ElementBits) | PrimitiveMask;
      }
    } else {
      newDim = key.dim >> ElementBits;
    }

    return findOrCreate(key.packageName, key.className, newDim);
  }

  /** @return a type name that represents an array of this element type */
  private TypeName getDerivedTypeForElementType(byte mask) {
    int newDim;
    if (key.dim == -1) {
      newDim = mask << ElementBits | PrimitiveMask;
    } else if ((key.dim & ElementMask) == PrimitiveMask) {
      newDim = (((key.dim & ~ElementMask) | mask) << ElementBits) | PrimitiveMask;
    } else {
      newDim = key.dim << ElementBits | mask;
    }

    return findOrCreate(key.packageName, key.className, newDim);
  }

  public TypeName getArrayTypeForElementType() {
    return getDerivedTypeForElementType(ArrayMask);
  }

  public TypeName getPointerTypeForElementType() {
    return getDerivedTypeForElementType(PointerMask);
  }

  public TypeName getReferenceTypeForElementType() {
    return getDerivedTypeForElementType(ReferenceMask);
  }

  /**
   * @return the dimensionality of the type. By convention, class types have dimensionality 0,
   *     primitives -1, and arrays the number of [ in their descriptor.
   */
  public int getDerivedMask() {
    return key.dim;
  }

  /** Does 'this' refer to a class? */
  public boolean isClassType() {
    return key.dim == 0;
  }

  /** Does 'this' refer to an array? */
  public boolean isArrayType() {
    return key.dim > 0;
  }

  /** Does 'this' refer to a primitive type */
  public boolean isPrimitiveType() {
    return key.dim == -1;
  }

  /** Return the innermost element type reference for an array */
  public TypeName getInnermostElementType() {
    short newDim = ((key.dim & ElementMask) == PrimitiveMask) ? (short) -1 : 0;
    return findOrCreate(key.packageName, key.className, newDim);
  }

  /**
   * A key into the dictionary; this is just like a type name, but uses value equality instead of
   * object equality.
   */
  private static final class TypeNameKey implements Serializable {
    private static final long serialVersionUID = -8284030936836318929L;

    /** The package, like "java/lang". null means the unnamed package. */
    private final Atom packageName;

    /** The class name, like "Object" or "Z" */
    private final Atom className;

    /**
     * Dimensionality: -1 => primitive 0 => class >0 => mask of levels of array, reference, pointer
     *
     * <p>When the mask is &gt; 0, it represents levels of type qualifiers (in C terminology) for
     * array, reference and pointer types. There is also a special mask for when the innermost type
     * is a primitive. The mask is a bitfield laid out in inverse dimension order.
     *
     * <p>For instance, a single-dimension array is simply the value ArrayMask, padded with leading
     * zeros. A single-dimension array of primitives is ArrayMask<<ElementBits | PrimitiveMask. An
     * array of pointers to objects would be (ArrayMask<<ElementBits) | PointerMask; an array of
     * pointers to a primitive type would have the primitive mask on the end:
     * ((ArrayMask<<ElementBits) | PointerMask)<<ElementBits | PrimitiveMask
     */
    private final int dim;

    /** This should be the only constructor */
    private TypeNameKey(Atom packageName, Atom className, int dim) {
      this.packageName = packageName;
      this.className = className;
      this.dim = dim;
    }

    @Override
    public boolean equals(Object obj) {
      if (obj instanceof TypeNameKey) {
        TypeNameKey other = (TypeNameKey) obj;
        return className == other.className && packageName == other.packageName && dim == other.dim;
      } else {
        return false;
      }
    }

    /** TODO: cache for efficiency? */
    @Override
    public int hashCode() {
      int result = className.hashCode() * 5009 + dim * 5011;
      if (packageName != null) {
        result += packageName.hashCode();
      }
      return result;
    }

    @Override
    public String toString() {
      StringBuilder result = new StringBuilder();
      toStringPrefix(result);

      if (packageName != null) {
        result.append(packageName);
        result.append('/');
      }
      result.append(className.toString());

      return result.toString();
    }

    private void toStringPrefix(StringBuilder result) {
      boolean isPrimitive = (dim == -1) || (dim & ElementMask) == PrimitiveMask;
      if (dim != -1) {
        for (int d = (dim & ElementMask) == PrimitiveMask ? dim >> ElementBits : dim;
            d != 0;
            d >>= ElementBits) {
          final int masked = d & ElementMask;
          switch (masked) {
            case ArrayMask:
              result.append('[');
              break;
            case PointerMask:
              result.append('*');
              break;
            case ReferenceMask:
              result.append('&');
              break;
            default:
              throw new UnsupportedOperationException(
                  "unexpected masked type-name component " + masked);
          }
        }
      }
      if (!isPrimitive) {
        result.append('L');
      } else if (packageName != null && isPrimitive) {
        result.append('P');
      }
    }

    public String toUnicodeString() {
      try {
        StringBuilder result = new StringBuilder();
        toStringPrefix(result);

        if (packageName != null) {
          result.append(packageName.toUnicodeString());
          result.append('/');
        }
        result.append(className.toUnicodeString());

        return result.toString();
      } catch (UTFDataFormatException e) {
        e.printStackTrace();
        Assertions.UNREACHABLE();
        return null;
      }
    }
  }

  /** @return the Atom naming the package for this type. */
  public Atom getPackage() {
    return key.packageName;
  }

  /** @return the Atom naming the class for this type (without package) */
  public Atom getClassName() {
    return key.className;
  }
}
