/*
 * $Id: AbstractTableMap.java 38 2012-01-04 22:44:15Z andre@naef.com $
 * See LICENSE.txt for license terms.
 */

package com.naef.jnlua.util;

import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import com.naef.jnlua.LuaState;
import com.naef.jnlua.LuaValueProxy;

/**
 * Abstract map implementation backed by a Lua table.
 */
public abstract class AbstractTableMap<K> extends AbstractMap<K, Object>
		implements LuaValueProxy {
	// -- State
	private Set<Map.Entry<K, Object>> entrySet;

	// -- Construction
	/**
	 * Creates a new instance.
	 */
	public AbstractTableMap() {
	}

	// -- Map methods
	@Override
	public Set<Map.Entry<K, Object>> entrySet() {
		if (entrySet == null) {
			entrySet = new EntrySet();
		}
		return entrySet;
	}

	@Override
	public boolean isEmpty() {
		return entrySet().isEmpty();
	}

	@Override
	public boolean containsKey(Object key) {
		checkKey(key);
		LuaState luaState = getLuaState();
		synchronized (luaState) {
			pushValue();
			luaState.pushJavaObject(key);
			luaState.getTable(-2);
			try {
				return !luaState.isNil(-1);
			} finally {
				luaState.pop(2);
			}
		}
	}

	@Override
	public Object get(Object key) {
		checkKey(key);
		LuaState luaState = getLuaState();
		synchronized (luaState) {
			pushValue();
			luaState.pushJavaObject(key);
			luaState.getTable(-2);
			try {
				return luaState.toJavaObject(-1, Object.class);
			} finally {
				luaState.pop(2);
			}
		}
	}

	@Override
	public Object put(K key, Object value) {
		checkKey(key);
		LuaState luaState = getLuaState();
		synchronized (luaState) {
			Object oldValue = get(key);
			pushValue();
			luaState.pushJavaObject(key);
			luaState.pushJavaObject(value);
			luaState.setTable(-3);
			luaState.pop(1);
			return oldValue;
		}
	}

	@Override
	public Object remove(Object key) {
		checkKey(key);
		LuaState luaState = getLuaState();
		synchronized (luaState) {
			Object oldValue = get(key);
			pushValue();
			luaState.pushJavaObject(key);
			luaState.pushNil();
			luaState.setTable(-3);
			luaState.pop(1);
			return oldValue;
		}
	}

	// -- Protected methods
	/**
	 * Checks a key for validity. If the key is not valid, the method throws an
	 * appropriate runtime exception. The method is invoked for all input keys.
	 * 
	 * <p>
	 * This implementation checks that the key is not <code>null</code>. Lua
	 * does not allow <code>nil</code> as a table key. Subclasses may implement
	 * more restrictive checks.
	 * </p>
	 * 
	 * @param key
	 *            the key
	 * @throws NullPointerException
	 *             if the key is <code>null</code>
	 */
	protected void checkKey(Object key) {
		if (key == null) {
			throw new NullPointerException("key must not be null");
		}
	}

	/**
	 * Indicates if this table map filters keys from the Lua table. If the
	 * method returns <code>true</code>, the table map invokes
	 * {@link #acceptKey(int)} on each key retrieved from the underlying table
	 * to determine whether the key is accepted or rejected.
	 * 
	 * <p>
	 * This implementation returns <code>false</code>. Subclasses may override
	 * the method alongside {@link #acceptKey(int)} to implement key filtering.
	 * </p>
	 * 
	 * @return whether this table map filters keys from the Lua table
	 */
	protected boolean filterKeys() {
		return false;
	}

	/**
	 * Accepts or rejects a key from the Lua table. Only table keys that are
	 * accepted are processed. The method allows subclasses to filter the Lua
	 * table. The method is called only if {@link #filterKeys()} returns
	 * <code>true</code>.
	 * 
	 * <p>
	 * This implementation returns <code>true</code> regardless of the input,
	 * thus accepting all keys. Subclasses may override the method alongside
	 * {@link #filterKeys()} to implement key filtering.
	 * </p>
	 * 
	 * @param index
	 *            the stack index containing the candidate key
	 * @return whether the key is accepted
	 */
	protected boolean acceptKey(int index) {
		return true;
	}

	/**
	 * Converts the key at the specified stack index to a Java object. If this
	 * table maps performs key filtering, the method is invoked only for keys it
	 * has accepted.
	 * 
	 * @param index
	 *            the stack index containing the key
	 * @return the Java object representing the key
	 * @see #filterKeys()
	 * @see #acceptKey(int)
	 */
	protected abstract K convertKey(int index);

	// -- Nested types
	/**
	 * Lua table entry set.
	 */
	private class EntrySet extends AbstractSet<Map.Entry<K, Object>> {
		// -- Set methods
		@Override
		public Iterator<Map.Entry<K, Object>> iterator() {
			return new EntryIterator();
		}

		@Override
		public boolean isEmpty() {
			LuaState luaState = getLuaState();
			synchronized (luaState) {
				pushValue();
				luaState.pushNil();
				while (luaState.next(-2)) {
					if (!filterKeys() || acceptKey(-2)) {
						luaState.pop(3);
						return false;
					}
				}
				luaState.pop(1);
				return true;
			}
		}

		@Override
		public int size() {
			LuaState luaState = getLuaState();
			synchronized (luaState) {
				int count = 0;
				pushValue();
				if (filterKeys()) {
					luaState.pushNil();
					while (luaState.next(-2)) {
						if (acceptKey(-2)) {
							count++;
						}
						luaState.pop(1);
					}
				} else {
					count = luaState.tableSize(-1);
				}
				luaState.pop(1);
				return count;
			}
		}

		@Override
		public boolean contains(Object object) {
			checkKey(object);
			if (!(object instanceof AbstractTableMap<?>.Entry)) {
				return false;
			}
			@SuppressWarnings("unchecked")
			Entry luaTableEntry = (Entry) object;
			if (luaTableEntry.getLuaState() != getLuaState()) {
				return false;
			}
			return containsKey(luaTableEntry.key);
		}

		@Override
		public boolean remove(Object object) {
			if (!(object instanceof AbstractTableMap<?>.Entry)) {
				return false;
			}
			@SuppressWarnings("unchecked")
			Entry luaTableEntry = (Entry) object;
			if (luaTableEntry.getLuaState() != getLuaState()) {
				return false;
			}
			LuaState luaState = getLuaState();
			synchronized (luaState) {
				pushValue();
				luaState.pushJavaObject(object);
				luaState.getTable(-2);
				boolean contains = !luaState.isNil(-1);
				luaState.pop(1);
				if (contains) {
					luaState.pushJavaObject(object);
					luaState.pushNil();
					luaState.setTable(-3);
				}
				luaState.pop(1);
				return contains;
			}
		}
	}

	/**
	 * Lua table iterator.
	 */
	private class EntryIterator implements Iterator<Map.Entry<K, Object>> {
		// -- State
		private K key;

		// -- Iterator methods
		@Override
		public boolean hasNext() {
			LuaState luaState = getLuaState();
			synchronized (luaState) {
				pushValue();
				luaState.pushJavaObject(key);
				while (luaState.next(-2)) {
					if (!filterKeys() || acceptKey(-2)) {
						luaState.pop(3);
						return true;
					}
				}
				luaState.pop(1);
				return false;
			}
		}

		@Override
		public Map.Entry<K, Object> next() {
			LuaState luaState = getLuaState();
			synchronized (luaState) {
				pushValue();
				luaState.pushJavaObject(key);
				while (luaState.next(-2)) {
					if (!filterKeys() || acceptKey(-2)) {
						key = convertKey(-2);
						luaState.pop(3);
						return new Entry(key);
					}
				}
				luaState.pop(1);
				throw new NoSuchElementException();
			}
		}

		@Override
		public void remove() {
			LuaState luaState = getLuaState();
			synchronized (luaState) {
				pushValue();
				luaState.pushJavaObject(key);
				luaState.pushNil();
				luaState.setTable(-3);
				luaState.pop(1);
			}
		}
	}

	/**
	 * Bindings entry.
	 */
	private class Entry implements Map.Entry<K, Object> {
		// -- State
		private K key;

		// -- Construction
		/**
		 * Creates a new instance.
		 */
		public Entry(K key) {
			this.key = key;
		}

		// -- Map.Entry methods
		@Override
		public K getKey() {
			return key;
		}

		@Override
		public Object getValue() {
			return get(key);
		}

		@Override
		public Object setValue(Object value) {
			return put(key, value);
		}

		// -- Object methods
		@Override
		public boolean equals(Object obj) {
			if (!(obj instanceof AbstractTableMap<?>.Entry)) {
				return false;
			}
			@SuppressWarnings("unchecked")
			Entry other = (Entry) obj;
			return getLuaState() == other.getLuaState()
					&& key.equals(other.key);
		}

		@Override
		public int hashCode() {
			return getLuaState().hashCode() * 65599 + key.hashCode();
		}

		@Override
		public String toString() {
			return key.toString();
		}

		// -- Private methods
		/**
		 * Returns the Lua script engine.
		 */
		private LuaState getLuaState() {
			return AbstractTableMap.this.getLuaState();
		}
	}
}