package edu.stanford.nlp.util;

import java.util.Map;
import java.util.Set;

/**
 * An extension of {@link ArrayCoreMap} with an immutable set of key,value
 * pairs that is used for equality and hashcode comparisons.
 * 
 * @author dramage
 */
public class HashableCoreMap extends ArrayCoreMap {

  /** Set of immutable keys */
  private final Set<Class<? extends TypesafeMap.Key<?>>> immutableKeys;
  
  /** Pre-computed hashcode */
  private final int hashcode;
  
  /**
   * Creates an instance of HashableCoreMap with initial key,value pairs
   * for the immutable, hashable keys as provided in the given map.
   */
  @SuppressWarnings("unchecked")
  public HashableCoreMap(Map<Class<? extends TypesafeMap.Key<?>>,Object> hashkey) {
    int keyHashcode = 0;
    int valueHashcode = 0;
    
    for (Map.Entry<Class<? extends TypesafeMap.Key<?>>,Object> entry : hashkey.entrySet()) {
      // NB it is important to compose these hashcodes in an order-independent
      // way, so we just add them all here.
      keyHashcode += entry.getKey().hashCode();
      valueHashcode += entry.getValue().hashCode();
      
      super.set((Class) entry.getKey(), entry.getValue());
    }
    
    this.immutableKeys = hashkey.keySet();
    this.hashcode = keyHashcode * 31 + valueHashcode;
  }
  
  /**
   * Creates an instance by copying values from the given other CoreMap,
   * using the values it associates with the given set of hashkeys for
   * the immutable, hashable keys used by hashcode and equals.
   */
  @SuppressWarnings("unchecked")
  public HashableCoreMap(ArrayCoreMap other, Set<Class<? extends TypesafeMap.Key<?>>> hashkey) {
    super(other);
    
    int keyHashcode = 0;
    int valueHashcode = 0;
    
    for (Class<? extends TypesafeMap.Key<?>> key : hashkey) {
      // NB it is important to compose these hashcodes in an order-independent
      // way, so we just add them all here.
      keyHashcode += key.hashCode();
      valueHashcode += super.get((Class)key).hashCode();
    }
    
    this.immutableKeys = hashkey;
    this.hashcode = keyHashcode * 31 + valueHashcode;
  }
  
  /**
   * Sets the value associated with the given key; if the the key is one
   * of the hashable keys, throws an exception.
   * 
   * @throws HashableCoreMapException Attempting to set the value for an
   *   immutable, hashable key.
   */
  @Override
  public <VALUE> VALUE set(Class<? extends Key<VALUE>> key, VALUE value) {
    
    if (immutableKeys.contains(key)) {
      throw new HashableCoreMapException("Attempt to change value " +
      		"of immutable field "+key.getSimpleName());
    }
    
    return super.set(key, value);
  }
  
  /**
   * Provides a hash code based on the immutable keys and values provided
   * to the constructor.
   */
  @Override
  public int hashCode() {
    return hashcode;
  }
  
  /**
   * If the provided object is a HashableCoreMap, equality is based only
   * upon the values of the immutable hashkeys; otherwise, defaults to
   * behavior of the superclass's equals method.
   */
  @SuppressWarnings("unchecked")
  @Override
  public boolean equals(Object o) {
    if (o instanceof HashableCoreMap) {
      HashableCoreMap other = (HashableCoreMap) o;
      if (!other.immutableKeys.equals(this.immutableKeys)) {
        return false;
      }
      for (Class<? extends TypesafeMap.Key<?>> key : immutableKeys) {
        if (!this.get((Class)key).equals(other.get((Class)key))) {
          return false;
        }
      }
      return true;
    } else {
      return super.equals(o);
    }
  }
  
  private static final long serialVersionUID = 1L;
  
  //
  // Exception type
  //
  
  /**
   * An exception thrown when attempting to change the value associated
   * with an (immutable) hash key in a HashableCoreMap.
   * 
   * @author dramage
   */
  public static class HashableCoreMapException extends RuntimeException {

    public HashableCoreMapException(String message) {
      super(message);
    }
    
    /**
     * 
     */
    private static final long serialVersionUID = 1L;
  }
  
}
