/**
 * <p>
 * Copyright © 2009-2010, Bruce-Robert Pocock
 * </p>
 * <p>
 * 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 3 of the License, or (at
 * your option) any later version.
 * </p>
 * <p>
 * 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.
 * </p>
 * <p>
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 * </p>
 * 
 * @author brpocock
 */

package org.starhope.appius.sql;

import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import org.json.JSONException;
import org.json.JSONObject;
import org.starhope.appius.game.AppiusClaudiusCaecus;
import org.starhope.appius.util.AppiusConfig;
import org.starhope.util.LibMisc;

/**
 * 
 * This is a class for infrequently-changed objects that are enumerated
 * types referenced by integer ID columns. Things like the set of
 * available avatar classes fall into this category.
 * 
 * @author brpocock
 * 
 */
public abstract class SQLPeerEnum extends SQLPeerDatum {

	/**
	 * enumeration of all possible values
	 */
	protected final static ConcurrentHashMap <Class <? extends SQLPeerEnum>, ConcurrentHashMap <Integer, String>> enumeration = new ConcurrentHashMap <Class <? extends SQLPeerEnum>, ConcurrentHashMap <Integer, String>> ();

	/**
	 * Index of which classes have been cached already. A value of -1
	 * means that the class is being cached presently; zero or more
	 * represents some state of completion, no longer requiring
	 * precaching.
	 */
	private static final ConcurrentHashMap <Class <? extends SQLPeerEnum>, Integer> hasCached = new ConcurrentHashMap <Class <? extends SQLPeerEnum>, Integer> ();

	/**
	 * WRITEME: document this field (brpocock, Dec 11, 2009)
	 * 
	 * knownChildren (SQLPeerEnum)
	 */
	private static HashSet <Class <? extends SQLPeerEnum>> knownChildren = new HashSet <Class <? extends SQLPeerEnum>> ();

	/**
	 * TODO: document this field (brpocock, Oct 13, 2009)
	 * 
	 * serialVersionUID (long)
	 */
	private static final long serialVersionUID = 8901251122444872319L;

	/**
	 * TODO: document this method (brpocock, Dec 11, 2009)
	 * 
	 */
	public synchronized static void doRealCacheResetStatic () {
		SQLPeerEnum.enumeration.clear ();
		SQLPeerEnum.hasCached.clear ();
	}

	/**
	 * TODO: document this method (brpocock, Nov 19, 2009)
	 * 
	 * @param klass WRITEME
	 * @param id WRITEME
	 * @return WRITEME
	 */
	public static SQLPeerEnum get (
			final Class <? extends SQLPeerEnum> klass, final int id) {
		SQLPeerEnum o = null;
		try {
			o = klass.newInstance ();
		} catch (final InstantiationException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		} catch (final IllegalAccessException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		}
		o.set (id);
		return klass.cast (o);
	}

	/**
	 * TODO: document this method (brpocock, Nov 19, 2009)
	 * 
	 * @param klass WRITEME WRITEME
	 * @param str WRITEME WRITEME
	 * @return WRITEME WRITEME
	 */
	public static SQLPeerEnum get (
			final Class <? extends SQLPeerEnum> klass, final String str) {
		SQLPeerEnum o = null;
		try {
			o = klass.newInstance ();
		} catch (final InstantiationException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		} catch (final IllegalAccessException e) {
			throw AppiusClaudiusCaecus.fatalBug (e);
		}
		o.set (str);
		return klass.cast (o);
	}

	/**
	 * TODO: document this method (brpocock, Dec 11, 2009)
	 * 
	 */
	public static void invalidateCache () {
		SQLPeerEnum.doRealCacheResetStatic ();
	}

	/**
	 * TODO: document this method (brpocock, Dec 11, 2009)
	 * 
	 */
	public static void invalidateCaches () {
		for (final Class <? extends SQLPeerEnum> klass : SQLPeerEnum.knownChildren) {
			try {
				klass
				.getMethod ("invalidateCache",
						(Class <?> []) null).invoke (
								(Class <?>) null, (Object) null);
			} catch (final IllegalArgumentException e) {
				// Default catch action, report bug (brpocock, Dec 11,
				// 2009)
				AppiusClaudiusCaecus
				.reportBug (
						"Caught a IllegalArgumentException in invalidateCaches",
						e);
			} catch (final SecurityException e) {
				// Default catch action, report bug (brpocock, Dec 11,
				// 2009)
				AppiusClaudiusCaecus
				.reportBug (
						"Caught a SecurityException in invalidateCaches",
						e);
			} catch (final IllegalAccessException e) {
				// Default catch action, report bug (brpocock, Dec 11,
				// 2009)
				AppiusClaudiusCaecus
				.reportBug (
						"Caught a IllegalAccessException in invalidateCaches",
						e);
			} catch (final InvocationTargetException e) {
				// Default catch action, report bug (brpocock, Dec 11,
				// 2009)
				AppiusClaudiusCaecus
				.reportBug (
						"Caught a InvocationTargetException in invalidateCaches",
						e);
			} catch (final NoSuchMethodException e) {
				// Default catch action, report bug (brpocock, Dec 11,
				// 2009)
				AppiusClaudiusCaecus
				.reportBug (
						"Caught a NoSuchMethodException in invalidateCaches",
						e);
			}
		}
	}

	/**
	 * TODO: document this method (brpocock, Dec 11, 2009)
	 * 
	 * @param klass WRITEME
	 */
	private static void registerClass (
			final Class <? extends SQLPeerEnum> klass) {
		SQLPeerEnum.knownChildren.add (klass);
	}

	/**
	 * instance ID
	 */
	protected int instance;

	/**
	 * TODO
	 * 
	 * @param klass WRITEME
	 */
	public SQLPeerEnum (final Class <? extends SQLPeerEnum> klass) {
		super ();
		prepCache ();
		SQLPeerEnum.registerClass (klass);
		instance = -1;
	}

	/**
	 * TODO
	 * 
	 * @param klass WRITEME
	 * @param id the specific ID
	 */
	protected SQLPeerEnum (final Class <? extends SQLPeerEnum> klass,
			final int id) {
		super ();
		prepCache ();
		SQLPeerEnum.registerClass (klass);
		instance = id;
	}

	/**
	 * This method caches into the internal "enumeration" hashmap the
	 * results of an SQL query specific to this SQLPeerEnum class of
	 * object.
	 * 
	 * The ResultSet must have a long (probably INT UNSIGNED) in the
	 * first column of the results, and the string value for it in
	 * column 1.
	 * 
	 * @param set The ResultSet from the SQL query
	 * @throws SQLException if anything goes wrong from the query
	 */
	protected void cache (final ResultSet set) throws SQLException {
		while (set.next ()) {
			// AppiusClaudiusCaecus.blather (" CACHE "
			// + this.getClass ().getCanonicalName () + "\t"
			// + set.getInt (1) + ":" + set.getString (2));
			getEnumeration ().put (set.getInt (1), set.getString (2));
		}
	}

	/**
	 * Actually flush the cache for all SQLPeerEnums
	 */
	private synchronized void doRealCacheReset () {
		SQLPeerEnum.doRealCacheResetStatic ();
	}

	/**
	 * 
	 * This is an overriding method.
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals (final Object o) {
		if (o instanceof SQLPeerEnum)
			return equals ((SQLPeerEnum) o);
		return false;
	}

	/**
	 * @see Object#equals(Object)
	 * @param o The other type
	 * @return true if the two types are equal
	 */
	public boolean equals (final SQLPeerEnum o) {
		return getCacheUniqueID ().equals (o.getCacheUniqueID ());
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.sql.SQLPeerDatum#getCacheUniqueID()
	 */
	@Override
	protected String getCacheUniqueID () {
		return this.getClass ().getCanonicalName () + "#"
		+ String.valueOf (instance);
	}

	/**
	 * @return the enumeration
	 */
	protected ConcurrentHashMap <Integer, String> getEnumeration () {
		if (!SQLPeerEnum.enumeration.containsKey (this.getClass ())) {
			SQLPeerEnum.enumeration.put (this.getClass (),
					new ConcurrentHashMap <Integer, String> ());
		}
		return SQLPeerEnum.enumeration.get (this.getClass ());

	}

	/**
	 * 
	 * WRITEME: document this method (brpocock, Aug 14, 2009)
	 * 
	 * @return WRITEME
	 */
	public int getID () {
		return instance;
	}

	/**
	 * 
	 * WRITEME: document this method (brpocock, Jul 8, 2009)
	 * 
	 * @param s the string value for which to search
	 * @return the ID (number) of the string value, or -1 if it's not
	 *         found
	 */
	public int getID (final String s) {
		for (final Entry <Integer, String> e : getEnumeration ()
				.entrySet ()) {
			if (e.getValue () == s)
				return e.getKey ();
		}
		return -1;
	}

	/**
	 * 
	 * WRITEME: document this method (brpocock, Aug 14, 2009)
	 * 
	 * @param connection WRITEME
	 * @return WRITEME
	 */
	protected abstract PreparedStatement getStatement (
			Connection connection);

	/**
	 * TODO: document this method (brpocock, Oct 13, 2009)
	 * 
	 * @return WRITEME
	 */
	public String getString () {
		return getEnumeration ().get (instance);
	}

	/**
	 * 
	 * WRITEME: document this method (brpocock, Jul 8, 2009)
	 * 
	 * @param id The value
	 * @return the string related to that value
	 */
	public String getString (final int id) {
		return SQLPeerEnum.enumeration.get (this.getClass ()).get (id);
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode () {
		return LibMisc.makeHashCode (getCacheUniqueID ());
	}

	/**
	 * 
	 * TODO: document this method (brpocock, Oct 13, 2009)
	 * 
	 */
	protected synchronized void prepCache () {
		// AppiusClaudiusCaecus.blather ("[prepCache]");

		int cacheLevel = -2;
		if (SQLPeerEnum.hasCached.containsKey (this.getClass ())) {
			cacheLevel = SQLPeerEnum.hasCached.get (this.getClass ());
		}

		if (cacheLevel >= 0)
			// AppiusClaudiusCaecus.blather ("#*#"
			// + getClass ().getCanonicalName () + "#*#");
			return;
		Connection con = null;
		PreparedStatement st = null;
		try {
			try {
				con = AppiusConfig.getDatabaseConnection ();
				st = getStatement (con);
			} catch (final SQLException e) {
				throw AppiusClaudiusCaecus.fatalBug (e);
			}
			try {
				if (st.execute ()) {
					cache (st.getResultSet ());
				} else
					throw AppiusClaudiusCaecus.fatalBug (new Exception (
							"Can't get enumerated type "
							+ this.getClass ()
							.getCanonicalName ()
							+ " from database"));
			} catch (final SQLException e) {
				throw AppiusClaudiusCaecus.fatalBug (e);
			}
		} finally {
			try {
				if (null != st) {
					st.close ();
				}
			} catch (final SQLException e) {
				AppiusClaudiusCaecus.reportBug (
						"Caught a SQLException closing Statement", e);
			}
			try {
				if (null != con) {
					con.close ();
				}
			} catch (final SQLException e) {
				AppiusClaudiusCaecus
				.reportBug (
						"Caught a SQLException closing Connection",
						e);
			}
		}

		SQLPeerEnum.hasCached.put (this.getClass (), 1);

		// AppiusClaudiusCaecus.blather ("[/prepCache]");
	}

	/**
	 * 
	 * TODO: document this method (brpocock, Oct 13, 2009)
	 * 
	 */
	public synchronized void resetCache () {
		doRealCacheReset ();
	}

	/**
	 * TODO: document this method (brpocock, Nov 19, 2009)
	 * 
	 * @param id WRITEME
	 */
	public void set (final int id) {
		instance = id;
	}

	/**
	 * TODO: document this method (brpocock, Nov 19, 2009)
	 * 
	 * @param str WRITEME
	 */
	public void set (final String str) {
		if (SQLPeerEnum.enumeration.size () == 0) {
			prepCache ();
		}
		for (final Entry <Integer, String> tuple : getEnumeration ()
				.entrySet ()) {
			if (tuple.getValue ().equals (str)) {
				instance = tuple.getKey ();
				return;
			}
		}
		AppiusClaudiusCaecus.reportBug ("Key not found: "
				+ this.getClass ().getCanonicalName () + ": “" + str
				+ "”");
		instance = -1;
	}

	/**
	 * 
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.sql.SQLPeerDatum#toJSON()
	 */
	@Override
	public JSONObject toJSON () {
		final JSONObject self = new JSONObject ();
		try {
			self.put ("id", instance);
		} catch (final JSONException e) {
			// Default catch action, report bug (brpocock, Aug 17, 2009)
			AppiusClaudiusCaecus.reportBug (e);
		}
		return self;
	}

	/**
	 * Create a stringified version of this enumeration. Usually the
	 * integer ID, a colon, and the string name, but might be overridden
	 * in a child class.
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString () {
		return String.valueOf (instance) + ":" + getString ();
	}
}
