/*
 * Copyright 2014 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 java.util.AbstractMap.SimpleEntry;
import java.util.Map.Entry;
import javaemul.internal.ArrayHelper;
import javaemul.internal.JsUtils;

/**
 * A simple wrapper around JavaScriptObject to provide {@link java.util.Map}-like semantics for any
 * key type.
 * <p>
 * Implementation notes:
 * <p>
 * A key's hashCode is the index in backingMap 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.
 */
class InternalHashCodeMap<K, V> implements Iterable<Entry<K, V>> {

  private final InternalJsMap<Object> backingMap = new InternalJsMap<>();
  private AbstractHashMap<K, V> host;
  private int size;

  public InternalHashCodeMap(AbstractHashMap<K, V> host) {
    this.host = host;
  }

  public V put(K key, V value) {
    int hashCode = hash(key);
    Entry<K, V>[] chain = getChainOrEmpty(hashCode);

    if (chain.length == 0) {
      // This is a new chain, put it to the map.
      backingMap.set(hashCode, chain);
    } else {
      // Chain already exists, perhaps key also exists.
      Entry<K, V> entry = findEntryInChain(key, chain);
      if (entry != null) {
        return entry.setValue(value);
      }
    }
    chain[chain.length] = new SimpleEntry<K, V>(key, value);
    size++;
    host.structureChanged();
    return null;
  }

  public V remove(Object key) {
    int hashCode = hash(key);
    Entry<K, V>[] chain = getChainOrEmpty(hashCode);
    for (int i = 0; i < chain.length; i++) {
      Entry<K, V> entry = chain[i];
      if (host.equals(key, entry.getKey())) {
        if (chain.length == 1) {
          ArrayHelper.setLength(chain, 0);
          // remove the whole array
          backingMap.delete(hashCode);
        } else {
          // splice out the entry we're removing
          ArrayHelper.removeFrom(chain, i, 1);
        }
        size--;
        host.structureChanged();
        return entry.getValue();
      }
    }
    return null;
  }

  public Map.Entry<K, V> getEntry(Object key) {
    return findEntryInChain(key, getChainOrEmpty(hash(key)));
  }

  private Map.Entry<K, V> findEntryInChain(Object key, Entry<K, V>[] chain) {
    for (Entry<K, V> entry : chain) {
      if (host.equals(key, entry.getKey())) {
        return entry;
      }
    }
    return null;
  }

  public int size() {
    return size;
  }

  @Override
  public Iterator<Entry<K, V>> iterator() {
    return new Iterator<Map.Entry<K, V>>() {
      final InternalJsMap.Iterator<?> chains = backingMap.entries();
      int itemIndex = 0;
      Entry<K, V>[] chain = newEntryChain();
      Entry<K, V> lastEntry = null;

      @Override
      public boolean hasNext() {
        if (itemIndex < chain.length) {
          return true;
        }
        InternalJsMap.IteratorEntry<?> current = chains.next();
        if (!current.isDone()) {
          // Move to the beginning of next chain
          chain = JsUtils.uncheckedCast(current.getValue());
          itemIndex = 0;
          return true;
        }
        return false;
      }

      @Override
      public Entry<K, V> next() {
        lastEntry = chain[itemIndex++];
        return lastEntry;
      }

      @Override
      public void remove() {
        InternalHashCodeMap.this.remove(lastEntry.getKey());
        // Unless we are in a new chain, all items have shifted so our itemIndex should as well...
        if (itemIndex != 0) {
          itemIndex--;
        }
      }
    };
  }

  private Entry<K, V>[] getChainOrEmpty(int hashCode) {
    Entry<K, V>[] chain = JsUtils.uncheckedCast(backingMap.get(hashCode));
    return chain == null ? newEntryChain() : chain;
  }

  private Entry<K, V>[] newEntryChain() {
    return JsUtils.uncheckedCast(new Object[0]);
  }

  /**
   * Returns hash code of the key as calculated by {@link AbstractHashMap#getHashCode(Object)} but
   * also handles null keys as well.
   */
  private int hash(Object key) {
    return host.getHashCode(key);
  }
}
