/*******************************************************************************
 * 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 java.io.Serializable;
import java.io.UTFDataFormatException;
import java.util.Map;

import com.ibm.wala.util.collections.HashMapFactory;
import com.ibm.wala.util.debug.Assertions;
import com.ibm.wala.util.strings.Atom;
import com.ibm.wala.util.strings.ImmutableByteArray;
import com.ibm.wala.util.strings.StringStuff;

/**
 * We've introduced this class to canonicalize Atoms that represent package names.
 * 
 * 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 -> TypeName
   */
  private final static Map<TypeNameKey, TypeName> map = HashMapFactory.make();

  private static 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 final int getDerivedMask() {
    return key.dim;
  }

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

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

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

  /**
   * Return the innermost element type reference for an array
   */
  public final 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 final static class TypeNameKey implements Serializable {
    /**
     * 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
     *                  
     *  When the mask is > 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. 
     *  
     *  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() {
      StringBuffer result = new StringBuffer();
      toStringPrefix(result);
      
      if (packageName != null) {
        result.append(packageName.toString());
        result.append("/");
      }
      result.append(className.toString());

      return result.toString();
    }

    private void toStringPrefix(StringBuffer result) {
      boolean isPrimitive = (dim==-1) || (dim&ElementMask)==PrimitiveMask;
      if (dim != -1) {
        for (int d = (dim&ElementMask) == PrimitiveMask? dim>>ElementBits: dim; d != 0; d>>=ElementBits) {
          switch (d&ElementMask) {
          case ArrayMask:
            result.append("[");
            break;
          case PointerMask:
            result.append("*");
            break;
          case ReferenceMask:
            result.append("&");
            break;
          }
        }
      }
      if (!isPrimitive) {
        result.append("L");
      } else if (packageName != null && isPrimitive) {
        result.append("P");        
      }
    }

    public String toUnicodeString() {
      try {
        StringBuffer result = new StringBuffer();
        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;
  }
}
