/*
 * HashTable.java
 *
 * Copyright (C) 2002-2007 Peter Graves
 * Copyright (C) 2010 Erik Huelsmann
 * $Id$
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * As a special exception, the copyright holders of this library give you
 * permission to link this library with independent modules to produce an
 * executable, regardless of the license terms of these independent
 * modules, and to copy and distribute the resulting executable under
 * terms of your choice, provided that you also meet, for each linked
 * independent module, the terms and conditions of the license of that
 * module.  An independent module is a module which is not derived from
 * or based on this library.  If you modify this library, you may extend
 * this exception to your version of the library, but you are not
 * obligated to do so.  If you do not wish to do so, delete this
 * exception statement from your version.
 */
package org.armedbear.lisp;

import java.util.concurrent.locks.ReentrantLock;
import static org.armedbear.lisp.Lisp.*;

public class HashTable 
    extends LispObject
    implements org.armedbear.lisp.protocol.Hashtable
{

    protected static final float loadFactor = 0.75f;
    protected final LispObject rehashSize;
    protected final LispObject rehashThreshold;
    // The rounded product of the capacity and the load factor. When the number
    // of elements exceeds the threshold, the implementation calls rehash().
    protected int threshold;
    // Array containing the actual key-value mappings.
    @SuppressWarnings("VolatileArrayField")
    protected volatile HashEntry[] buckets;
    // The number of key-value pairs.
    protected volatile int count;
    final Comparator comparator;
    final private ReentrantLock lock = new ReentrantLock();

    protected HashTable(Comparator c, int size, LispObject rehashSize,
            LispObject rehashThreshold) {
        this.rehashSize = rehashSize;
        this.rehashThreshold = rehashThreshold;
        buckets = new HashEntry[size];
        threshold = (int) (size * loadFactor);
        comparator = c;
    }

    protected static int calculateInitialCapacity(int size) {
        int capacity = 1;
        while (capacity < size) {
            capacity <<= 1;
        }
        return capacity;
    }

    public static HashTable newEqHashTable(int size, LispObject rehashSize,
            LispObject rehashThreshold) {
        return new HashTable(new Comparator(), size, rehashSize, rehashThreshold);
    }

    public static HashTable newEqlHashTable(int size, LispObject rehashSize,
            LispObject rehashThreshold) {
        return new HashTable(new EqlComparator(), size, rehashSize, rehashThreshold);
    }

    public static HashTable newEqualHashTable(int size, LispObject rehashSize,
            LispObject rehashThreshold) {
        return new HashTable(new EqualComparator(), size, rehashSize, rehashThreshold);
    }

    public static LispObject newEqualpHashTable(int size, LispObject rehashSize,
            LispObject rehashThreshold) {
        return new HashTable(new EqualpComparator(), size, rehashSize, rehashThreshold);
    }

    public final LispObject getRehashSize() {
        return rehashSize;
    }

    public final LispObject getRehashThreshold() {
        return rehashThreshold;
    }

    public int getSize() {
        return buckets.length;
    }

    public int getCount() {
        return count;
    }

    @Override
    public LispObject typeOf() {
        return Symbol.HASH_TABLE;
    }

    @Override
    public LispObject classOf() {
        return BuiltInClass.HASH_TABLE;
    }

    @Override
    public LispObject typep(LispObject type) {
        if (type == Symbol.HASH_TABLE) {
            return T;
        }
        if (type == BuiltInClass.HASH_TABLE) {
            return T;
        }
        return super.typep(type);
    }

    @Override
    public boolean equalp(LispObject obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof HashTable) {
            HashTable ht = (HashTable) obj;
            if (count != ht.count) {
                return false;
            }
            if (getTest() != ht.getTest()) {
                return false;
            }
            LispObject entries = ENTRIES();
            while (entries != NIL) {
                LispObject entry = entries.car();
                LispObject key = entry.car();
                LispObject value = entry.cdr();
                if (!value.equalp(ht.get(key))) {
                    return false;
                }
                entries = entries.cdr();
            }
            return true;
        }
        return false;
    }

    @Override
    public LispObject getParts() {
        // No need to take out a read lock, for the same reason as MAPHASH
        HashEntry[] b = buckets;
        LispObject parts = NIL;
        for (int i = 0; i < b.length; i++) {
            HashEntry e = b[i];
            while (e != null) {
                parts = parts.push(new Cons("KEY [bucket " + i + "]", e.key));
                parts = parts.push(new Cons("VALUE", e.value));
                e = e.next;
            }
        }
        return parts.nreverse();
    }

    public void clear() {
        lock.lock();
        try {
            buckets = new HashEntry[buckets.length];
            count = 0;
        } finally {
            lock.unlock();
        }
    }

    // gethash key hash-table &optional default => value, present-p
    public LispObject gethash(LispObject key) {
        LispObject value = get(key);
        final LispObject presentp;
        if (value == null) {
            value = presentp = NIL;
        } else {
            presentp = T;
        }
        return LispThread.currentThread().setValues(value, presentp);
    }

    // gethash key hash-table &optional default => value, present-p
    public LispObject gethash(LispObject key, LispObject defaultValue) {
        LispObject value = get(key);
        final LispObject presentp;
        if (value == null) {
            value = defaultValue;
            presentp = NIL;
        } else {
            presentp = T;
        }
        return LispThread.currentThread().setValues(value, presentp);
    }

    public LispObject gethash1(LispObject key) {
        final LispObject value = get(key);
        return value != null ? value : NIL;
    }

    public LispObject puthash(LispObject key, LispObject newValue) {
        put(key, newValue);
        return newValue;
    }

    // remhash key hash-table => generalized-boolean
    public LispObject remhash(LispObject key) {
        // A value in a Lisp hash table can never be null, so...
        return remove(key) != null ? T : NIL;
    }

    @Override
    public String printObject() {
        if (Symbol.PRINT_READABLY.symbolValue(LispThread.currentThread()) != NIL) {
            error(new PrintNotReadable(list(Keyword.OBJECT, this)));
            return null; // Not reached.
        }
        StringBuilder sb = new StringBuilder(getTest().princToString());
        sb.append(' ');
        sb.append(Symbol.HASH_TABLE.princToString());
        sb.append(' ');
        sb.append(count);
        if (count == 1) {
            sb.append(" entry");
        } else {
            sb.append(" entries");
        }
        sb.append(", ");
        sb.append(buckets.length);
        sb.append(" buckets");
        return unreadableString(sb.toString());
    }

    public Symbol getTest() {
        return comparator.getTest();
    }

    protected HashEntry getEntry(LispObject key) {
        HashEntry[] b = buckets;
        int hash = comparator.hash(key);
        HashEntry e = b[hash & (b.length - 1)];
        while (e != null) {
            if (hash == e.hash &&
                    (key == e.key || comparator.keysEqual(key, e.key))) {
                return e;
            }
            e = e.next;
        }
        return null;
    }

    public LispObject get(LispObject key) {
        HashEntry e = getEntry(key);
        LispObject v = (e == null) ? null : e.value;

        if (e == null || v != null) {
            return v;
        }

        lock.lock();
        try {
            return e.value;
        } finally {
            lock.unlock();
        }
    }

    public void put(LispObject key, LispObject value) {
        lock.lock();
        try {
            HashEntry e = getEntry(key);
            if (e != null) {
                e.value = value;
            } else {
                // Not found. We need to add a new entry.
                if (++count > threshold) {
                    rehash();
                }

                int hash = comparator.hash(key);
                int index = hash & (buckets.length - 1);
                buckets[index] = new HashEntry(key, hash, value, buckets[index]);
            }
        } finally {
            lock.unlock();
        }
    }

    public LispObject remove(LispObject key) {
        lock.lock();
        try {
            int index = comparator.hash(key) & (buckets.length - 1);

            HashEntry e = buckets[index];
            HashEntry last = null;
            while (e != null) {
                if (comparator.keysEqual(key, e.key)) {
                    if (last == null) {
                        buckets[index] = e.next;
                    } else {
                        last.next = e.next;
                    }
                    --count;
                    return e.value;
                }
                last = e;
                e = e.next;
            }
            return null;
        } finally {
            lock.unlock();
        }
    }

    protected void rehash() {
        lock.lock();
        try {
            final int newCapacity = buckets.length * 2;
            threshold = (int) (newCapacity * loadFactor);
            int mask = newCapacity - 1;
            HashEntry[] newBuckets = new HashEntry[newCapacity];

            for (int i = buckets.length; i-- > 0;) {
                HashEntry e = buckets[i];
                while (e != null) {
                    final int index = comparator.hash(e.key) & mask;
                    newBuckets[index] = new HashEntry(e.key, e.hash, e.value,
                            newBuckets[index]);
                    e = e.next;
                }
            }
            buckets = newBuckets;
        } finally {
            lock.unlock();
        }
    }


    public LispObject ENTRIES() {
        return getEntries();
    }

    // Returns a list of (key . value) pairs.        
    public LispObject getEntries() {
        // No need to take out a read lock, for the same reason as MAPHASH
        HashEntry[] b = buckets;
        LispObject list = NIL;
        for (int i = b.length; i-- > 0;) {
            HashEntry e = b[i];
            while (e != null) {
                list = new Cons(new Cons(e.key, e.value), list);
                e = e.next;
            }
        }
        return list;
    }

    public LispObject MAPHASH(LispObject function) {
        // Don't take out a read lock: it can't be upgraded to a write
        // lock, which would block the scenario where put() is called to
        // set the value of the current entry

        HashEntry[] b = buckets;
        for (int i = b.length; i-- > 0;) {
            HashEntry e = b[i];
            while (e != null) {
                function.execute(e.key, e.value);
                e = e.next;
            }
        }
        return NIL;
    }

    protected static class Comparator {

        Symbol getTest() {
            return Symbol.EQ;
        }

        boolean keysEqual(LispObject key1, LispObject key2) {
            return key1 == key2;
        }

        int hash(LispObject key) {
            return key.sxhash();
        }
    }

    protected static class EqlComparator extends Comparator {

        @Override
        Symbol getTest() {
            return Symbol.EQL;
        }

        @Override
        boolean keysEqual(LispObject key1, LispObject key2) {
            return key1.eql(key2);
        }
    }

    protected static class EqualComparator extends Comparator {

        @Override
        Symbol getTest() {
            return Symbol.EQUAL;
        }

        @Override
        boolean keysEqual(LispObject key1, LispObject key2) {
            return key1.equal(key2);
        }
    }

    protected static class EqualpComparator extends Comparator {

        @Override
        Symbol getTest() {
            return Symbol.EQUALP;
        }

        @Override
        boolean keysEqual(LispObject key1, LispObject key2) {
            return key1.equalp(key2);
        }

        @Override
        int hash(LispObject key) {
            return key.psxhash();
        }
    }

    protected static class HashEntry {

        LispObject key;
        int hash;
        volatile LispObject value;
        HashEntry next;

        HashEntry(LispObject key, int hash, LispObject value, HashEntry next) {
            this.key = key;
            this.hash = hash;
            this.value = value;
            this.next = next;
        }
    }

    // For EQUALP hash tables.
    @Override
    public int psxhash() {
        long result = 2062775257; // Chosen at random.
        result = mix(result, count);
        result = mix(result, getTest().sxhash());
        return (int) (result & 0x7fffffff);
    }
}
