/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * Copyright (c) 2007 - 2009 Pentaho Corporation and Contributors.  All rights reserved.
 */

package org.pentaho.reporting.libraries.base.util;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.Arrays;

/**
 * A fast linked-hashmap that avoids any unneccessay work. It is slightly slower than an ordinary hashmap but
 * faster than a combined hashmap and list-index that would be needed to get this functionality on JDK 1.2.
 * The map is as fast as the LinkedHashMap of JDK 1.4+.
 *
 * @author Thomas Morgner
 * @noinspection ProtectedField
 */
public class LinkedMap implements Cloneable, Serializable
{
  /**
   * A cache map entry class holding both the key and value and acting as member of a linked list.
   */
  protected static final class MapEntry implements Serializable
  {
    /**
     * The precomputed hashkey of the key.
     */
    protected final int hashKey;
    /**
     * The key object, which is never null and which never changes.
     */
    protected final Object key;
    /**
     * The current value object (can be null).
     */
    protected Object value;
    /**
     * The link to the previous entry in the list.
     */
    protected MapEntry previous;
    /**
     * The link to the next entry in the list.
     */
    protected MapEntry next;
    /**
     * The link to the next entry in the bucket that has the same hashkey.
     */
    protected MapEntry collisionNext;

    /**
     * Creates a new map-entry for the given key and value.
     *
     * @param key     the key, never null.
     * @param hashKey the precomputed hashkey for the key.
     * @param value   the value, never null.
     */
    protected MapEntry(final Object key, final int hashKey, final Object value)
    {
      if (key == null)
      {
        throw new NullPointerException();
      }
      this.key = key;
      this.hashKey = hashKey;
      this.value = value;
    }

    /**
     * Returns the previous entry in the list or null if this is the first entry.
     *
     * @return the previous entry.
     */
    public MapEntry getPrevious()
    {
      return previous;
    }

    /**
     * Redefines the previous entry in the list or null if this is the first entry.
     *
     * @param previous the previous entry.
     */
    public void setPrevious(final MapEntry previous)
    {
      this.previous = previous;
    }

    /**
     * Returns the next entry in the list or null if this is the last entry.
     *
     * @return the next entry.
     */
    public MapEntry getNext()
    {
      return next;
    }

    /**
     * Redefines the next entry in the list or null if this is the last entry.
     *
     * @param next the next entry.
     */
    public void setNext(final MapEntry next)
    {
      this.next = next;
    }

    /**
     * Returns the current value.
     *
     * @return the value, never null.
     */
    public Object getValue()
    {
      return value;
    }

    /**
     * Redefines the current value.
     *
     * @param value the value, never null.
     */
    public void setValue(final Object value)
    {
      this.value = value;
    }

    /**
     * Returns the next map-entry in the bucket. If more than one object maps into the same hash-bucket, this map
     * stores the entries as linked list.
     *
     * @return the next entry.
     */
    public MapEntry getCollisionNext()
    {
      return collisionNext;
    }

    /**
     * Defines the next map-entry in the bucket. If more than one object maps into the same hash-bucket, this map
     * stores the entries as linked list.
     *
     * @param collisionNext the next entry.
     */
    public void setCollisionNext(final MapEntry collisionNext)
    {
      if (collisionNext == this) throw new IllegalStateException();
      this.collisionNext = collisionNext;
    }
  }

  private static final int MAXIMUM_CAPACITY = 1 << 30;
  private static final Object NULL_MARKER = new Object();
  private int size;
  private int mask;
  private float loadFactor;
  private int capacity;
  private MapEntry[] backend;
  private MapEntry firstEntry;
  private MapEntry lastEntry;

  /**
   * Default constructor. Creates a map for 16 entries with a default load-factor of 0.75.
   */
  public LinkedMap()
  {
    this(16, 0.75f);
  }

  /**
   * Creates a new map with the given initial number of buckets and the given loadfactor. A load factor
   * greater 1 will always cause hash-collisions, while lower loadfactors reduce the likelyhood of collisions.
   *
   * @param initialCapacity the initial capacity.
   * @param loadFactor      the load factor of the bucket-array.
   */
  public LinkedMap(int initialCapacity, final float loadFactor)
  {
    if (initialCapacity > MAXIMUM_CAPACITY)
    {
      initialCapacity = MAXIMUM_CAPACITY;
    }
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
    {
      throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    }

    int capacity = 1;
    int mask = 0;
    while (capacity < initialCapacity)
    {
      mask = (mask << 1) | 1;
      capacity <<= 1;
    }
    this.mask = mask;
    this.loadFactor = loadFactor;
    this.backend = new MapEntry[capacity];
    this.capacity = (int) Math.ceil(capacity * loadFactor);
  }

  /**
   * Ensures that the map contains enough space to store the next entry.
   */
  private void ensureSize()
  {
    final MapEntry[] backend = this.backend;
    if (size <= (capacity))
    {
      return;
    }

    // expand ..
    final MapEntry[] newBackend = new MapEntry[(backend.length << 1)];
    final int newMask = (mask << 1) | 1;
    transferEntry(newBackend, newMask);

    this.mask = newMask;
    this.backend = newBackend;
    this.capacity = (int) Math.ceil(loadFactor * backend.length);
  }

  private void transferEntry(final MapEntry[] newBackend, final int newMask)
  {
    for (int i = 0; i < this.backend.length; i++)
    {
      MapEntry entry = this.backend[i];
      while (entry != null)
      {
        final MapEntry next = entry.collisionNext;
        final int insertIndex = entry.hashKey & newMask;
        entry.setCollisionNext(newBackend[insertIndex]);
        newBackend[insertIndex] = entry;
        entry = next;
      }
    }

    for (int i = 0; i < newBackend.length; i++)
    {
      MapEntry mapEntry = newBackend[i];
      while (mapEntry != null)
      {
        final int insertIndex = mapEntry.hashKey & newMask;
        if (i != insertIndex) throw new IllegalStateException();
        mapEntry = mapEntry.collisionNext;
      }

    }
  }

  /**
   * Returns the number of entries in the map.
   *
   * @return the number of entries.
   */
  public int size()
  {
    return size;
  }

  /**
   * Stores the given value in the map using the provided key. Both key and value can be null.
   *
   * @param key   the key.
   * @param value the value to be stored under the key.
   * @return the previous value stored under this key or null of the entry is new.
   */
  public Object put(final Object key, final Object value)
  {
    final Object realKey = ensureKey(key);
    final int hashKey = cleanHash(realKey.hashCode());

    ensureSize();

    final int index = hashKey & mask;
    final MapEntry existingEntry = backend[index];
    if (existingEntry == null)
    {
      final MapEntry entry = new MapEntry(realKey, hashKey, value);
      addNewRecord(index, entry);
      return null;
    }


    MapEntry colEntry = existingEntry;
    while (true)
    {
      // The root entry exists and matches the current key.
      if (colEntry.hashKey == hashKey &&
          colEntry.key.equals(realKey))
      {
        // that means, we just have to update the value inside and move the entry to the last position
        // in the list (to make it look like a remove/add operation.
        return updateRecord(value, colEntry);
      }

      if (colEntry.collisionNext == null)
      {
        // create a new entry in the backend-array ...
        final MapEntry entry = new MapEntry(realKey, hashKey, value);
        addCollisionRecord(index, entry);
        return null;
      }

      colEntry = colEntry.collisionNext;
    }
  }

  /**
   * Updates an existing record and reinserts the record at the end of the linked list.
   *
   * @param value    the new value value.
   * @param colEntry the entry record that should be updated.
   * @return the old value in the entry or null, if there is no old entry.
   */
  private Object updateRecord(final Object value, final MapEntry colEntry)
  {
    final Object oldValue = colEntry.value;
    // replace the value ..
    colEntry.value = (value);
    // and reconnect the entry at the end of the queue ..
    final MapEntry firstEntry = this.firstEntry;
    final MapEntry lastEntry = this.lastEntry;
    if (lastEntry == colEntry)
    {
      // also covers the case where last = first = col
      return oldValue;
    }
    if (firstEntry == colEntry)
    {
      this.firstEntry = colEntry.next;
      if (this.firstEntry != null)
      {
        this.firstEntry.previous = null;
      }
      colEntry.previous = (lastEntry);
      colEntry.next = (null);
      lastEntry.next = (colEntry);
      this.lastEntry = colEntry;

      asserta();
      return oldValue;
    }

    final MapEntry prevEntry = colEntry.previous;
    final MapEntry nextEntry = colEntry.next;
    prevEntry.next = (nextEntry);
    nextEntry.previous = (prevEntry);
    colEntry.previous = (lastEntry);
    colEntry.next = (null);
    lastEntry.next = (colEntry);
    this.lastEntry = colEntry;

    asserta();
    return oldValue;
  }

  /**
   * Adds a new map-entry to an already filled bucket.
   *
   * @param index where to add the new record in the map.
   * @param entry the new entry to be added.
   */
  private void addCollisionRecord(final int index, final MapEntry entry)
  {
    entry.setCollisionNext(backend[index]);
    backend[index] = entry;

    final MapEntry lastEntry = this.lastEntry;
    entry.previous = (lastEntry);
    entry.next = (null);
    lastEntry.next = (entry);
    this.lastEntry = entry;
    size += 1;

    asserta();
  }

  /**
   * Adds a completely new record to an previously empty bucket.
   *
   * @param index the index of the bucket to be updated.
   * @param entry the new map-entry.
   */
  private void addNewRecord(final int index, final MapEntry entry)
  {
    // thats easy ...
    final MapEntry lastEntry = this.lastEntry;
    if (lastEntry == null)
    {
      firstEntry = entry;
    }
    else
    {
      entry.previous = (lastEntry);
      lastEntry.next = (entry);
    }
    this.lastEntry = entry;
    backend[index] = entry;
    size += 1;

    asserta();
  }

  /**
   * Retrieves the object stored under the given key from the map.
   *
   * @param key the key for which a value should be located.
   * @return the value or null, if the map did not contain a value for the key.
   */
  public Object get(final Object key)
  {
    final Object realKey = ensureKey(key);
    final int hashKey = cleanHash(realKey.hashCode());

    final int index = hashKey & mask;
    final MapEntry existingEntry = backend[index];
    if (existingEntry == null)
    {
      return null;
    }

    MapEntry colEntry = existingEntry;
    while (colEntry != null)
    {
      // The root entry exists and matches the current key.
      if (colEntry.hashKey == hashKey && colEntry.key.equals(realKey))
      {
        return colEntry.value;
      }
      colEntry = colEntry.collisionNext;
    }
    return null;
  }

  /**
   * Removes the object stored under the given key from the map.
   *
   * @param key the key for which a value should be located.
   * @return the value or null, if the map did not contain a value for the key.
   */
  public Object remove(final Object key)
  {
    final Object realKey = ensureKey(key);
    final int hashKey = cleanHash(realKey.hashCode());

    final int index = hashKey & mask;
    final MapEntry existingEntry = backend[index];
    if (existingEntry == null)
    {
      return null;
    }

    MapEntry prevEntry = null;
    MapEntry colEntry = existingEntry;
    while (colEntry != null)
    {
      // The root entry exists and matches the current key.
      if (colEntry.hashKey == hashKey && colEntry.key.equals(realKey))
      {
        final Object value = colEntry.value;
        if (prevEntry == null)
        {
          // this is a root level entry ..
          backend[index] = colEntry.collisionNext;
        }
        else
        {
          prevEntry.setCollisionNext(colEntry.collisionNext);
        }

        // now check the first and last entry ...
        if (firstEntry == lastEntry)
        {
          // there is ony one entry.
          firstEntry = null;
          lastEntry = null;
          size -= 1;

          asserta();
          return value;
        }

        if (firstEntry == colEntry)
        {
          final MapEntry nextfirstEntry = colEntry.next;
          if (nextfirstEntry != null)
          {
            nextfirstEntry.previous = (null);
            colEntry.next = null;
          }
          firstEntry = nextfirstEntry;
        }
        else if (lastEntry == colEntry)
        {
          final MapEntry nextLastEntry = colEntry.previous;
          if (nextLastEntry != null)
          {
            nextLastEntry.next = (null);
            colEntry.previous = null;
          }
          lastEntry = nextLastEntry;
        }

        if (colEntry.previous != null)
        {
          colEntry.previous.next = colEntry.next;
        }
        if (colEntry.next != null)
        {
          colEntry.next.previous = colEntry.previous;
        }
        size -= 1;

        asserta();
        return value;
      }

      prevEntry = colEntry;
      colEntry = colEntry.collisionNext;
    }

    asserta();
    return null;
  }

  private void asserta()
  {
    if (firstEntry == null)
    {
      return;
    }
    if (firstEntry.previous != null)
    {
      throw new NullPointerException();
    }
  }

  /**
   * Checks, whether the map contains an entry for the key.
   *
   * @param key the key for which a value should be located.
   * @return true if the map contains a value for the key, false otherwise.
   */
  public boolean containsKey(final Object key)
  {
    final Object realKey = ensureKey(key);
    final int hashKey = cleanHash(realKey.hashCode());
    final int index = hashKey & mask;
    final MapEntry existingEntry = backend[index];
    if (existingEntry == null)
    {
      return false;
    }

    MapEntry colEntry = existingEntry;
    while (colEntry != null)
    {
      // The root entry exists and matches the current key.
      if (colEntry.hashKey == hashKey && colEntry.key.equals(realKey))
      {
        return true;
      }
      colEntry = colEntry.collisionNext;
    }
    return false;
  }

  /**
   * Returns the keys used in this map as array. The keys are returned in the insertation order.
   *
   * @param data the object array that should receive the keys.
   * @return the array filled with the keys.
   */
  public Object[] keys(final Object[] data)
  {
    final Object[] list;
    if (data.length < size)
    {
      list = (Object[]) Array.newInstance(data.getClass().getComponentType(), size);
    }
    else
    {
      list = data;
    }

    int index = 0;
    MapEntry entry = firstEntry;
    while (entry != null)
    {
      final Object o = entry.key;
      if (o == NULL_MARKER)
      {
        list[index] = (null);
      }
      else
      {
        list[index] = (o);
      }
      entry = entry.getNext();
      index += 1;
    }
    return list;
  }

  /**
   * Returns the keys used in this map as array. The keys are returned in the insertation order.
   *
   * @return the array filled with the keys.
   */
  public Object[] keys()
  {
    return keys(new Object[size]);
  }

  /**
   * Returns the values used in this map as array. The values are returned in the insertation order.
   *
   * @return the array filled with the values.
   */
  public Object[] values()
  {
    return values(new Object[size]);
  }

  /**
   * Returns the values used in this map as array. The values are returned in the insertation order.
   *
   * @param data the object array that should receive the values.
   * @return the array filled with the values.
   */
  public Object[] values(final Object[] data)
  {
    final Object[] list;
    if (data.length < size)
    {
      list = (Object[]) Array.newInstance(data.getClass().getComponentType(), size);
    }
    else
    {
      list = data;
    }
    int index = 0;
    MapEntry entry = firstEntry;
    while (entry != null)
    {
      final Object o = entry.value;
      list[index] = (o);
      entry = entry.getNext();
      index += 1;
    }
    return list;
  }

  /**
   * Clears the map and removes all map records.
   */
  public void clear()
  {
    if (firstEntry == null)
    {
      return;
    }
    firstEntry = null;
    lastEntry = null;
    Arrays.fill(backend, null);
    size = 0;
  }

  /**
   * A helper to ensure that null-keys are maped into a special marker object.
   *
   * @param o the potential key.
   * @return the null-marker.
   */
  private static Object ensureKey(final Object o)
  {
    if (o == null)
    {
      return NULL_MARKER;
    }
    return o;
  }

  /**
   * Ensures that the hashcode produced by the key is sane. This does some bit-juggeling to avoid incorrect
   * hashkey implementations.
   *
   * @param h the original hashcode.
   * @return the cleaned hashcode.
   */
  private static int cleanHash(int h)
  {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
  }

  /**
   * Clones this map.
   *
   * @return the cloned map.
   * @throws CloneNotSupportedException
   */
  public Object clone() throws CloneNotSupportedException
  {
    final LinkedMap map = (LinkedMap) super.clone();
    map.backend = (MapEntry[]) backend.clone();
    Arrays.fill(map.backend, null);
    map.firstEntry = null;
    map.lastEntry = null;
    map.size = 0;
    MapEntry entry = firstEntry;
    while (entry != null)
    {
      map.put(entry.key, entry.value);
      entry = entry.getNext();
    }
    return map;
  }

  /**
   * Checks whether this collection is empty.
   *
   * @return true, if the collection is empty, false otherwise.
   */
  public boolean isEmpty()
  {
    return size == 0;
  }
}
