/* 
 * E-XML Library:  For XML, XML-RPC, HTTP, and related.
 * Copyright (C) 2002-2008  Elias Ross
 * 
 * genman@noderunner.net
 * http://noderunner.net/~genman
 * 
 * 1025 NE 73RD ST
 * SEATTLE WA 98115
 * USA
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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.
 * 
 * $Id$
 */

package net.noderunner.exml;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 * This string pool holds instances of <code>String</code> objects, 
 * which makes it easy to re-use an existing String instance if a character
 * sequence was found previously.
 * For parsing, there is almost always a small set of different element and
 * attribute names, in comparison to the number of tags appearing in a given
 * document, so this pool should not grow very large.  The number of buckets
 * will grow to at least half the number of entries within.  For different
 * kinds of text documents, then, it makes sense to use a completely new
 * instance of this object.
 * <p>
 * To regulate the creation of large numbers of spurious String objects
 * that are no longer neccesary, internally the references are made
 * through the use of <code>java.lang.ref.SoftReference</code>
 * instances.
 * </p>
 * <p>
 * Data is maintained as a basic hash-table, with the buckets as
 * linked-list instances.
 * </p>
 * <p>
 * This class is not thread safe.
 * </p>
 */
public class StringPool
	extends AbstractSet<String>
{

	private static class SoftString extends SoftReference<String> {
		private char chars[];
		private int hashCode;

		private SoftString(String s, char chars[], int hashCode, ReferenceQueue<String> q) {
			super(s, q);
			this.hashCode = hashCode;
			this.chars = chars;
		}

		/** Returns a debug string */
		@Override
		public String toString() {
			if (chars == null)
				return "SoftString null";
			int len = chars.length;
			if (len > 40)
				len = 40;
			return "SoftString " + new String(chars, 0, len) + " len=" + chars.length;
		}

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

		@Override
		public boolean equals(Object o) {
			throw new UnsupportedOperationException();
		}

		/**
		 * It is important that clear() is called to remove the
		 * character array reference.  From what I can tell from
		 * the heap manager, a SoftReference object holds a
		 * <code>next</code> pointer and thus any memory held
		 * here may not be garbage collected. 
		 */
		@Override
		public void clear() {
			super.clear();
			chars = null;
		}
	}

	private final class Entry {
		private SoftString softString;
		private Entry next;

		public Entry(Entry other) {
			softString = other.softString;
			next = null;
		}
		
		public Entry(String s, int hashCode) {
			char chars[] = s.toCharArray();
			softString = new SoftString(s, chars, hashCode, refQueue);
			next = null;
		}

		public Entry(String s, char chars[], int hashCode) {
			softString = new SoftString(s, chars, hashCode, refQueue);
			next = null;
		}

		public boolean equals(char buf[], int off, int len, int hashCode) {
			if (softString.hashCode != hashCode || softString.chars == null || 
				softString.chars.length != len)
					return false;
			for (int i = 0; i < len; i++)
				if (softString.chars[i] != buf[i + off])
					return false;
			return true;
		}

		@Override
		public boolean equals(Object other) {
			// this is coming from the remove() method most likely
			if (other instanceof SoftString) {
				return softString == other;
			}
			// this is coming from intern
			Object string = softString.get();
			if (string == null) {
				return false;
			}
			return string.equals(other.toString());
		}

		private String recreate() {
			char c[] = softString.chars;
			int hc = softString.hashCode;
			String s = new String(c);
			softString = new SoftString(s, c, hc, refQueue);
			return s;
		}

		@Override
		public String toString() {
			Object string = softString.get();
			if (string == null) {
				return recreate();
			}
			return (String)string;
		}

		@Override
		public int hashCode() {
			return softString.hashCode;
		}
	}
	
	private Entry pool[];
	private int numEntries = 0;
	private ReferenceQueue<String> refQueue;

	/**
	 * Constructs a <code>StringPool</code> with 128 buckets.
	 */
	public StringPool() {
		this(128);
	}

	/**
	 * Constructs a <code>StringPool</code> with a specific size.
	 * @param size number of buckets
	 */
	public StringPool(int size) {
		pool = new Entry[size];
		refQueue = new ReferenceQueue<String>();
	}

	/**
	 * Returns a canonical representation of a <code>String</code>. 
	 * For all String instances <code>a</code> and </code>b</code>
	 * where <code>a.equals(b)</code> is true, <code>itern(a) ==
	 * intern(b)</code> will be true.
	 */
	public String intern(final String s) {
		rehash();
		int hashCode = s.hashCode();
		int index = index(hashCode, pool.length);
		Entry e = pool[index];
		if (e == null) { // new entry
			String is = s.intern();
			pool[index] = new Entry(is, hashCode);
			numEntries++;
			return is;
		}
		while (true) {
			if (e.equals(s))
				return e.toString();
			if (e.next == null) { // new entry
                                String is = s.intern();
				e.next = new Entry(is, hashCode);
				numEntries++;
				return is;
			}
			e = e.next;
		}
	}

	private static char[] charArray(char buf[], int off, int len) {
		char chars[] = new char[len];
		System.arraycopy(buf, off, chars, 0, len);
		return chars;
	}
	
	/**
	 * Returns a <code>String</code> representation of a character array
	 * slice.
	 */
	public String intern(char buf[], int off, int len) {
		rehash();
		int hashCode = hashCode(buf, off, len);
		int index = index(hashCode, pool.length);
		Entry e = pool[index];
		if (e == null) { // new entry
			char[] chars = charArray(buf, off, len);
			String s = new String(chars).intern();
			e = new Entry(s, chars, hashCode);
			pool[index] = e;
			numEntries++;
			return s;
		}
		while (true) {
			if (e.equals(buf, off, len, hashCode)) {
				return e.toString();
			}
			if (e.next == null) { // new entry
				char[] chars = charArray(buf, off, len);
				String s = new String(chars).intern();
				e.next = new Entry(s, chars, hashCode);
				numEntries++;
				return s;
			}
			e = e.next;
		}
	}

	/**
	 * Returns the index of the bucket to use.
	 */
	private int index(int hash, int len) {
		return (hash & 0x7FFFFFFF) % len;
	}

	/**
	 * Removes unused entries.
	 */
	private void removeSoftReferences() {
		Reference ref;
		while ((ref = refQueue.poll()) != null) {
			remove(ref);
			ref.clear();
		}
	}

	/**
	 * Expands the pool if neccessary.
	 */
	private void rehash() {
		if (numEntries / 2 < pool.length)
			return;
		removeSoftReferences();
		if (numEntries / 2 < pool.length)
			return;

		Entry newPool[] = new Entry[pool.length * 2];
		Iterator<Entry> i = entryIterator();
		while (i.hasNext()) {
			Entry e = i.next();
			int newIndex = index(e.hashCode(), newPool.length);
			Entry e2 = newPool[newIndex];
			if (e2 == null) {
				newPool[newIndex] = new Entry(e);
			} else {
				while (e2.next != null)
					e2 = e2.next;
				e2.next = new Entry(e);
			}
		}
		pool = newPool;
	}

	private class EntryIterator
		implements Iterator<Entry>
	{
		private int index;
		private Entry e;
		public EntryIterator() {
		}
		public boolean hasNext() {
			if (e != null)
				return true;
			while (index < pool.length) {
				if (pool[index] != null) {
					e = pool[index];
					index++;
					return true;
				}
				index++;
			}
			return false;
		}
		public Entry next() {
			if (e == null) {
				if (!hasNext())
					throw new NoSuchElementException("No more pool elements");
			}
			Entry orig = e;
			e = e.next;
			return orig;
		}
		public void remove()
		{
			throw new UnsupportedOperationException("Cannot remove using iterator");
		}
	}

	private class StringIterator implements Iterator<String>
	{
		private EntryIterator ei = new EntryIterator();
		public boolean hasNext() {
			return ei.hasNext();
		}
		public String next() {
		    Entry e = ei.next();
		    return e.toString();
		}
		public void remove()
		{
			ei.remove();
		}
	}

	/**
	 * Returns the number of used buckets.
	 * This is used for hash performance estimation.
	 */
	int buckets() {
		int count = 0;
		for (int i = 0; i < pool.length; i++) {
			if (pool[i] != null && pool[i].next != null)
				count++;
		}
		return count;
	}

	/**
	 * Returns an iterator of the strings.
	 * 
	 * Note that the {@link Iterator#remove()} operation is not supported.
	 */
	@Override
	public Iterator<String> iterator() {
		return new StringIterator();
	}
	
	private Iterator<Entry> entryIterator() {
		return new EntryIterator();
	}

	/**
	 * Returns the number of strings in this pool.
	 */
	@Override
	public int size() {
		return numEntries;
	}

	/**
	 * Adds a string to this pool.
	 * Always returns true, even if this string previously existed.
	 *
	 * @param o must be a <code>java.lang.String</code> instance
	 */
	@Override
	public boolean add(String o) {
		intern(o);
		return true;
	}

	/**
	 * Removes a <code>String</code> or <code>SoftString</code> from
	 * the pool.
	 * Returns true if modification was done.
	 */
	@Override
	public boolean remove(Object o) {
		int index = index(o.hashCode(), pool.length);
		Entry e = pool[index];
		if (e == null)
			return false;
		if (e.equals(o)) {
			pool[index] = e.next;
			numEntries--;
			return true;
		}
		while (e.next != null) {
			if (e.next.equals(o)) {
				e = e.next.next;
				numEntries--;
				return true;
			}
			e = e.next;
		}
		return false;
	}

	/**
	 * Tests if this object is present in the pool.
	 */
	@Override
	public boolean contains(Object o) {
		int index = index(o.hashCode(), pool.length);
		Entry e = pool[index];
		while (e != null) {
			if (e.equals(o))
				return true;
			e = e.next;
		}
		return false;
	}

	/**
	 * Adds a collection of strings to this pool.
	 * Always returns true, even if additional entries were created.
	 */
	@Override
	public boolean addAll(Collection<? extends String> c) {
		Iterator<? extends String> i = c.iterator();
		while (i.hasNext())
			intern(i.next().toString());
		return true;
	}

	/**
	 * Computes a hash code of a character array slice using the same
	 * algorithm as described in the Java documentation for
	 * <code>java.lang.String</code>.
	 */
	static int hashCode(char val[], int off, int len) {
		int h = 0;
		for (int i = 0; i < len; i++)
			h = 31*h + val[off++];
		return h;
	}

}
