/*
 * Copyright 2008 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package java.util;

import com.google.gwt.core.client.JavaScriptObject;

/**
 * Implementation of Map interface based on a hash table. <a
 * href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/HashMap.html">[Sun
 * docs]</a>
 * 
 * @param <K> key type
 * @param <V> value type
 */
abstract class AbstractHashMap<K, V> extends AbstractMap<K, V> {
  /*
   * Implementation notes:
   * 
   * String keys are stored in a separate map from non-String keys. String keys
   * are mapped to their values via a JS associative map, stringMap. String keys
   * could collide with intrinsic properties (like watch, constructor) so we
   * prepend each key with a ':' inside of stringMap.
   * 
   * Integer keys are used to index all non-string keys. A key's hashCode is the
   * index in hashCodeMap which should contain that key. Since several keys may
   * have the same hash, each value in hashCodeMap is actually an array
   * containing all entries whose keys share the same hash.
   */
  private final class EntrySet extends AbstractSet<Entry<K, V>> {

    @Override
    public void clear() {
      AbstractHashMap.this.clear();
    }

    @Override
    public boolean contains(Object o) {
      if (o instanceof Map.Entry) {
        Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
        Object key = entry.getKey();
        if (AbstractHashMap.this.containsKey(key)) {
          Object value = AbstractHashMap.this.get(key);
          return AbstractHashMap.this.equals(entry.getValue(), value);
        }
      }
      return false;
    }

    @Override
    public Iterator<Entry<K, V>> iterator() {
      return new EntrySetIterator();
    }

    @Override
    public boolean remove(Object entry) {
      if (contains(entry)) {
        Object key = ((Map.Entry<?, ?>) entry).getKey();
        AbstractHashMap.this.remove(key);
        return true;
      }
      return false;
    }

    @Override
    public int size() {
      return AbstractHashMap.this.size();
    }
  }

  /**
   * Iterator for <code>EntrySetImpl</code>.
   */
  private final class EntrySetIterator implements Iterator<Entry<K, V>> {
    private final Iterator<Map.Entry<K, V>> iter;
    private Map.Entry<K, V> last = null;

    /**
     * Constructor for <code>EntrySetIterator</code>.
     */
    public EntrySetIterator() {
      List<Map.Entry<K, V>> list = new ArrayList<Map.Entry<K, V>>();
      if (nullSlotLive) {
        list.add(new MapEntryNull());
      }
      addAllStringEntries(list);
      addAllHashEntries(list);
      this.iter = list.iterator();
    }

    public boolean hasNext() {
      return iter.hasNext();
    }

    public Map.Entry<K, V> next() {
      return last = iter.next();
    }

    public void remove() {
      if (last == null) {
        throw new IllegalStateException("Must call next() before remove().");
      } else {
        iter.remove();
        AbstractHashMap.this.remove(last.getKey());
        last = null;
      }
    }
  }

  private final class MapEntryNull extends AbstractMapEntry<K, V> {

    public K getKey() {
      return null;
    }

    public V getValue() {
      return nullSlot;
    }

    public V setValue(V object) {
      return putNullSlot(object);
    }
  }

  // Instantiated from JSNI
  @SuppressWarnings("unused")
  private final class MapEntryString extends AbstractMapEntry<K, V> {

    private final String key;

    public MapEntryString(String key) {
      this.key = key;
    }

    @SuppressWarnings("unchecked")
    public K getKey() {
      return (K) key;
    }

    public V getValue() {
      return getStringValue(key);
    }

    public V setValue(V object) {
      return putStringValue(key, object);
    }
  }

  /**
   * A map of integral hashCodes onto entries.
   */
  // Used from JSNI.
  @SuppressWarnings("unused")
  private transient JavaScriptObject hashCodeMap;

  /**
   * This is the slot that holds the value associated with the "null" key.
   */
  private transient V nullSlot;

  private transient boolean nullSlotLive;

  private int size;

  /**
   * A map of Strings onto values.
   */
  // Used from JSNI.
  @SuppressWarnings("unused")
  private transient JavaScriptObject stringMap;

  {
    clearImpl();
  }

  public AbstractHashMap() {
  }

  public AbstractHashMap(int ignored) {
    // This implementation of HashMap has no need of initial capacities.
    this(ignored, 0);
  }

  public AbstractHashMap(int ignored, float alsoIgnored) {
    // This implementation of HashMap has no need of load factors or capacities.
    if (ignored < 0 || alsoIgnored < 0) {
      throw new IllegalArgumentException(
          "initial capacity was negative or load factor was non-positive");
    }
  }

  public AbstractHashMap(Map<? extends K, ? extends V> toBeCopied) {
    this.putAll(toBeCopied);
  }

  @Override
  public void clear() {
    clearImpl();
  }

  public abstract Object clone();

  @Override
  public boolean containsKey(Object key) {
    return (key == null) ? nullSlotLive : (!(key instanceof String)
        ? hasHashValue(key, getHashCode(key)) : hasStringValue((String) key));
  }

  @Override
  public boolean containsValue(Object value) {
    if (nullSlotLive && equals(nullSlot, value)) {
      return true;
    } else if (containsStringValue(value)) {
      return true;
    } else if (containsHashValue(value)) {
      return true;
    }
    return false;
  }

  @Override
  public Set<Map.Entry<K, V>> entrySet() {
    return new EntrySet();
  }

  @Override
  public V get(Object key) {
    return (key == null) ? nullSlot : (!(key instanceof String) ? getHashValue(
        key, getHashCode(key)) : getStringValue((String) key));
  }

  @Override
  public V put(K key, V value) {
    return (key == null) ? putNullSlot(value) : (!(key instanceof String)
        ? putHashValue(key, value, getHashCode(key)) : putStringValue(
            (String) key, value));
  }

  @Override
  public V remove(Object key) {
    return (key == null) ? removeNullSlot() : (!(key instanceof String)
        ? removeHashValue(key, getHashCode(key))
        : removeStringValue((String) key));
  }

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

  /**
   * Subclasses must override to return a whether or not two keys or values are
   * equal.
   */
  protected abstract boolean equals(Object value1, Object value2);

  /**
   * Subclasses must override to return a hash code for a given key. The key is
   * guaranteed to be non-null and not a String.
   */
  protected abstract int getHashCode(Object key);

  private native void addAllHashEntries(Collection<?> dest) /*-{
    var hashCodeMap = this.@java.util.AbstractHashMap::hashCodeMap;
    for (var hashCode in hashCodeMap) {
      // sanity check that it's really an integer
      if (hashCode == parseInt(hashCode)) {
        var array = hashCodeMap[hashCode];
        for (var i = 0, c = array.length; i < c; ++i) {
          dest.@java.util.Collection::add(Ljava/lang/Object;)(array[i]);
        }
      }
    }
  }-*/;

  private native void addAllStringEntries(Collection<?> dest) /*-{
    var stringMap = this.@java.util.AbstractHashMap::stringMap;
    for (var key in stringMap) {
      // only keys that start with a colon ':' count
      if (key.charCodeAt(0) == 58) {
        var entry = @java.util.AbstractHashMap$MapEntryString::new(Ljava/util/AbstractHashMap;Ljava/lang/String;)(this, key.substring(1));
        dest.@java.util.Collection::add(Ljava/lang/Object;)(entry);
      }
    }
  }-*/;

  private void clearImpl() {
    hashCodeMap = JavaScriptObject.createArray();
    stringMap = JavaScriptObject.createObject();
    nullSlotLive = false;
    nullSlot = null;
    size = 0;
  }

  /**
   * Returns true if hashCodeMap contains any Map.Entry whose value is Object
   * equal to <code>value</code>.
   */
  private native boolean containsHashValue(Object value) /*-{
    var hashCodeMap = this.@java.util.AbstractHashMap::hashCodeMap;
    for (var hashCode in hashCodeMap) {
      // sanity check that it's really one of ours
      if (hashCode == parseInt(hashCode)) {
        var array = hashCodeMap[hashCode];
        for (var i = 0, c = array.length; i < c; ++i) {
          var entry = array[i];
          var entryValue = entry.@java.util.Map$Entry::getValue()();
          if (this.@java.util.AbstractHashMap::equalsBridge(Ljava/lang/Object;Ljava/lang/Object;)(value, entryValue)) {
            return true;
          }
        }
      }
    }
    return false;
  }-*/;

  /**
   * Returns true if stringMap contains any key whose value is Object equal to
   * <code>value</code>.
   */
  private native boolean containsStringValue(Object value) /*-{
    var stringMap = this.@java.util.AbstractHashMap::stringMap;
    for (var key in stringMap) {
      // only keys that start with a colon ':' count
      if (key.charCodeAt(0) == 58) {
        var entryValue = stringMap[key];
        if (this.@java.util.AbstractHashMap::equalsBridge(Ljava/lang/Object;Ljava/lang/Object;)(value, entryValue)) {
          return true;
        }
      }
    }
    return false;
  }-*/;

  /**
   * Bridge method from JSNI that keeps us from having to make polymorphic calls
   * in JSNI. By putting the polymorphism in Java code, the compiler can do a
   * better job of optimizing in most cases.
   */
  @SuppressWarnings("unused")
  private boolean equalsBridge(Object value1, Object value2) {
    return equals(value1, value2);
  }

  /**
   * Returns the Map.Entry whose key is Object equal to <code>key</code>,
   * provided that <code>key</code>'s hash code is <code>hashCode</code>;
   * or <code>null</code> if no such Map.Entry exists at the specified
   * hashCode.
   */
  private native V getHashValue(Object key, int hashCode) /*-{
    var array = this.@java.util.AbstractHashMap::hashCodeMap[hashCode];
    if (array) {
      for (var i = 0, c = array.length; i < c; ++i) {
        var entry = array[i];
        var entryKey = entry.@java.util.Map$Entry::getKey()();
        if (this.@java.util.AbstractHashMap::equalsBridge(Ljava/lang/Object;Ljava/lang/Object;)(key, entryKey)) {
          return entry.@java.util.Map$Entry::getValue()();
        }
      }
    }
    return null;
  }-*/;

  /**
   * Returns the value for the given key in the stringMap. Returns
   * <code>null</code> if the specified key does not exist.
   */
  private native V getStringValue(String key) /*-{
    return this.@java.util.AbstractHashMap::stringMap[':' + key];
  }-*/;

  /**
   * Returns true if the a key exists in the hashCodeMap that is Object equal to
   * <code>key</code>, provided that <code>key</code>'s hash code is
   * <code>hashCode</code>.
   */
  private native boolean hasHashValue(Object key, int hashCode) /*-{
    var array = this.@java.util.AbstractHashMap::hashCodeMap[hashCode];
    if (array) {
      for (var i = 0, c = array.length; i < c; ++i) {
        var entry = array[i];
        var entryKey = entry.@java.util.Map$Entry::getKey()();
        if (this.@java.util.AbstractHashMap::equalsBridge(Ljava/lang/Object;Ljava/lang/Object;)(key, entryKey)) {
          return true;
        }
      }
    }
    return false;
  }-*/;

  /**
   * Returns true if the given key exists in the stringMap.
   */
  private native boolean hasStringValue(String key) /*-{
    return (':' + key) in this.@java.util.AbstractHashMap::stringMap;
  }-*/;

  /**
   * Sets the specified key to the specified value in the hashCodeMap. Returns
   * the value previously at that key. Returns <code>null</code> if the
   * specified key did not exist.
   */
  private native V putHashValue(K key, V value, int hashCode) /*-{
    var array = this.@java.util.AbstractHashMap::hashCodeMap[hashCode];
    if (array) {
      for (var i = 0, c = array.length; i < c; ++i) {
        var entry = array[i];
        var entryKey = entry.@java.util.Map$Entry::getKey()();
        if (this.@java.util.AbstractHashMap::equalsBridge(Ljava/lang/Object;Ljava/lang/Object;)(key, entryKey)) {
          // Found an exact match, just update the existing entry
          var previous = entry.@java.util.Map$Entry::getValue()();
          entry.@java.util.Map$Entry::setValue(Ljava/lang/Object;)(value);
          return previous;
        }
      }
    } else {
      array = this.@java.util.AbstractHashMap::hashCodeMap[hashCode] = [];
    }
    var entry = @java.util.MapEntryImpl::new(Ljava/lang/Object;Ljava/lang/Object;)(key, value);
    array.push(entry);
    ++this.@java.util.AbstractHashMap::size;
    return null;
  }-*/;

  private V putNullSlot(V value) {
    V result = nullSlot;
    nullSlot = value;
    if (!nullSlotLive) {
      nullSlotLive = true;
      ++size;
    }
    return result;
  }

  /**
   * Sets the specified key to the specified value in the stringMap. Returns the
   * value previously at that key. Returns <code>null</code> if the specified
   * key did not exist.
   */
  private native V putStringValue(String key, V value) /*-{
    var result, stringMap = this.@java.util.AbstractHashMap::stringMap;
    key = ':' + key;
    if (key in stringMap) {
      result = stringMap[key];
    } else {
      ++this.@java.util.AbstractHashMap::size;
    }
    stringMap[key] = value;
    return result;
  }-*/;

  /**
   * Removes the pair whose key is Object equal to <code>key</code> from
   * <code>hashCodeMap</code>, provided that <code>key</code>'s hash code
   * is <code>hashCode</code>. Returns the value that was associated with the
   * removed key, or null if no such key existed.
   */
  private native V removeHashValue(Object key, int hashCode) /*-{
    var array = this.@java.util.AbstractHashMap::hashCodeMap[hashCode];
    if (array) {
      for (var i = 0, c = array.length; i < c; ++i) {
        var entry = array[i];
        var entryKey = entry.@java.util.Map$Entry::getKey()();
        if (this.@java.util.AbstractHashMap::equalsBridge(Ljava/lang/Object;Ljava/lang/Object;)(key, entryKey)) {
          if (array.length == 1) {
            // remove the whole array
            delete this.@java.util.AbstractHashMap::hashCodeMap[hashCode];
          } else {
            // splice out the entry we're removing
            array.splice(i, 1);
          }
          --this.@java.util.AbstractHashMap::size;
          return entry.@java.util.Map$Entry::getValue()();
        }
      }
    }
    return null;
  }-*/;

  private V removeNullSlot() {
    V result = nullSlot;
    nullSlot = null;
    if (nullSlotLive) {
      nullSlotLive = false;
      --size;
    }
    return result;
  }

  /**
   * Removes the specified key from the stringMap and returns the value that was
   * previously there. Returns <code>null</code> if the specified key does not
   * exist.
   */
  private native V removeStringValue(String key) /*-{
    var result, stringMap = this.@java.util.AbstractHashMap::stringMap;
    key = ':' + key;
    if (key in stringMap) {
      result = stringMap[key];
      --this.@java.util.AbstractHashMap::size;
      delete stringMap[key];
    }
    return result;
  }-*/;
}
