/*
 * 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.classLoader.Language;
import com.ibm.wala.core.util.strings.ImmutableByteArray;
import com.ibm.wala.core.util.strings.StringStuff;
import com.ibm.wala.core.util.strings.UTF8Convert;
import com.ibm.wala.util.collections.HashMapFactory;
import java.util.Map;

/**
 * A method descriptor; something like: (Ljava/langString;)Ljava/lang/Class;
 *
 * <p>Descriptors are canonical
 */
public final class Descriptor {

  /** A mapping from Key -&gt; Descriptor */
  private static final Map<Key, Descriptor> map = HashMapFactory.make();

  /** key holds the logical value of this descriptor */
  private final Key key;

  /**
   * @param parameters the parameters for a descriptor
   * @param returnType the return type
   * @return the canonical representative for this descriptor value
   */
  public static Descriptor findOrCreate(TypeName[] parameters, TypeName returnType) {
    if (returnType == null) {
      throw new IllegalArgumentException("null returnType");
    }
    if (parameters != null && parameters.length == 0) {
      parameters = null;
    }
    Key k = new Key(returnType, parameters);
    Descriptor result = map.get(k);
    if (result == null) {
      result = new Descriptor(k);
      map.put(k, result);
    }
    return result;
  }

  /**
   * @param b a byte array holding the string representation of this descriptor
   * @return the canonical representative for this descriptor value
   */
  public static Descriptor findOrCreate(Language l, ImmutableByteArray b)
      throws IllegalArgumentException {
    TypeName returnType = StringStuff.parseForReturnTypeName(l, b);
    TypeName[] parameters = StringStuff.parseForParameterNames(l, b);
    Key k = new Key(returnType, parameters);
    Descriptor result = map.get(k);
    if (result == null) {
      result = new Descriptor(k);
      map.put(k, result);
    }
    return result;
  }

  public static Descriptor findOrCreate(ImmutableByteArray b) throws IllegalArgumentException {
    return findOrCreate(Language.JAVA, b);
  }

  /**
   * @param s string representation of this descriptor
   * @return the canonical representative for this descriptor value
   */
  public static Descriptor findOrCreateUTF8(String s) throws IllegalArgumentException {
    return findOrCreateUTF8(Language.JAVA, s);
  }

  /**
   * @param s string representation of this descriptor
   * @return the canonical representative for this descriptor value
   */
  public static Descriptor findOrCreateUTF8(Language l, String s) throws IllegalArgumentException {
    byte[] b = UTF8Convert.toUTF8(s);
    return findOrCreate(l, new ImmutableByteArray(b));
  }

  /** @param key "value" of this descriptor */
  private Descriptor(Key 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();
  }

  /** @return a unicode string representation of this descriptor */
  public String toUnicodeString() {
    return key.toUnicodeString();
  }

  /** @return the name of the return type of this descriptor */
  public TypeName getReturnType() {
    return key.returnType;
  }

  /** @return the type names for the parameters in this descriptor */
  public TypeName[] getParameters() {
    return key.parameters;
  }

  /** @return number of parameters in this descriptor */
  public int getNumberOfParameters() {
    return key.parameters == null ? 0 : key.parameters.length;
  }

  /** value that defines a descriptor: used to canonicalize instances */
  private static class Key {
    private final TypeName returnType;

    private final TypeName[] parameters;

    private final int hashCode; // cached for efficiency

    Key(TypeName returnType, TypeName[] parameters) {
      this.returnType = returnType;
      this.parameters = parameters;
      if (parameters != null) {
        assert parameters.length > 0;
      }
      hashCode = computeHashCode();
    }

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

    public int computeHashCode() {
      int result = returnType.hashCode() * 5309;
      if (parameters != null) {
        for (int i = 0; i < parameters.length; i++) {
          result += parameters[i].hashCode() * (5323 ^ i);
        }
      }
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      assert obj instanceof Key;
      Key other = (Key) obj;
      if (!returnType.equals(other.returnType)) {
        return false;
      }
      if (parameters == null) {
        return (other.parameters == null) ? true : false;
      }
      if (other.parameters == null) {
        return false;
      }
      if (parameters.length != other.parameters.length) {
        return false;
      }
      for (int i = 0; i < parameters.length; i++) {
        if (!parameters[i].equals(other.parameters[i])) {
          return false;
        }
      }
      return true;
    }

    @Override
    public String toString() {
      StringBuilder result = new StringBuilder();
      result.append('(');
      if (parameters != null) {
        for (TypeName p : parameters) {
          result.append(p);
          appendSemicolonIfNeeded(result, p);
        }
      }
      result.append(')');
      result.append(returnType);
      appendSemicolonIfNeeded(result, returnType);
      return result.toString();
    }

    public String toUnicodeString() {
      StringBuilder result = new StringBuilder();
      result.append('(');
      if (parameters != null) {
        for (TypeName p : parameters) {
          result.append(p.toUnicodeString());
          appendSemicolonIfNeeded(result, p);
        }
      }
      result.append(')');
      result.append(returnType);
      appendSemicolonIfNeeded(result, returnType);
      return result.toString();
    }

    private static void appendSemicolonIfNeeded(StringBuilder result, TypeName p) {
      if (p.isArrayType()) {
        TypeName e = p.getInnermostElementType();
        String x = e.toUnicodeString();
        if (x.startsWith("L") || x.startsWith("P")) {
          result.append(';');
        }
      } else {
        String x = p.toUnicodeString();
        if (x.startsWith("L") || x.startsWith("P")) {
          result.append(';');
        }
      }
    }
  }
}
