package de.dassit.dbwrapper;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

/**
 * Abstract base class for database entries
 * 
 * @author slederer@dass-it.de
 * 
 */
abstract public class Entry {
	/**
	 * indicates if this object was loaded from the database and therefore
	 * requires an update instead of an insert on save()
	 */
	protected boolean update;
	/**
	 * value of the primary key for this object
	 */
	protected String key;

	/**
	 * name of the table that stores our objects
	 */
	protected String tableName;
	/**
	 * name of the primary key column
	 */
	protected String keyName;
	/**
	 * name of the column which contains an auto-incrementing id and therefore
	 * must be empty on insert
	 */
	protected String autoIdField;
	/**
	 * an array containing the column names of the table, in the correct order
	 */
	protected String[] fieldNames = {};

	/**
	 * regex to check for invalid characters in string fields
	 */
	protected static String invalidCharacters = ".*[\"';<>].*";

	/**
	 * initialize an object from a <code>ResultSet</code> object.
	 * 
	 * @param queryResult
	 *            a <code>ResultSet</code> object which must contain exactly one
	 *            row
	 * @throws SQLException
	 */

	public abstract void initFromQuery(ResultSet queryResult)
			throws SQLException;

	/**
	 * return a new object, cast to <code>Entry</code>, which was initialized
	 * from a <code>ResultSet</code>
	 * 
	 * @param rs
	 *            a <code>ResultSet</code> object which must contain exactly one
	 *            row
	 * @return the new object
	 * @throws SQLException
	 */
	public abstract Entry newFromQuery(ResultSet rs) throws SQLException;

	/**
	 * make sure that no fields contain SQL metacharacters
	 * 
	 * @throws SQLException
	 */
	public abstract void validate() throws SQLException;

	/**
	 * Create an update statement as a string which updates a single column.
	 * 
	 * @param field
	 *            column name
	 * @param value
	 *            the new value
	 * @param key
	 *            primary key of the object/row to be updated
	 * @return the statement as a string
	 */
	public String genericUpdateOne(String field, String value, String key) {
		return "UPDATE " + tableName + " SET " + sqlStringValue(value, field)
				+ " WHERE " + sqlStringValue(key, keyName);
	}

	/**
	 * Create an update statement as a string which updates all columns.
	 * 
	 * @param key
	 *            primary key of the object/row to be updated
	 * @param values
	 *            an array of <code>Object</code> with the corresponding values
	 *            for all columns
	 * @return the statement as a string
	 */
	public String genericUpdate(String key, Object... values) {
		int index = 0;
		StringBuilder sql = new StringBuilder("UPDATE " + tableName + " SET ");
		for (Object o : values) {
			if (index > 0) {
				sql.append(',');
			}

			if (o == null) {
				sql.append(fieldNames[index] + "=NULL");
			} else if (o instanceof Number || o instanceof Boolean) {
				sql.append(fieldNames[index]);
				sql.append('=');
				sql.append(o.toString());
			} else {
				sql.append(sqlStringValue(o.toString(), fieldNames[index]));
			}
			index++;
		}

		sql.append(getWhereClause(key));

		return sql.toString();
	}

	/**
	 * Create a select statement which explicitly states all column names. Does
	 * not contain a WHERE clause.
	 * 
	 * @return the statement as a string
	 */
	public String genericSelect() {
		int index = 0;
		StringBuilder sql = new StringBuilder("SELECT ");
		for (String n : fieldNames) {
			if (index > 0) {
				sql.append(',');
			}
			sql.append(n);
			index++;
		}

		sql.append(" FROM ");
		sql.append(tableName);

		return sql.toString();
	}

	/**
	 * Create an insert statement as a string, using the supplied values.
	 * 
	 * @param values
	 *            an array of <code>Object</code> with the corresponding values
	 *            for all columns
	 * @return the statement as a string
	 */
	public String genericInsert(Object... values) {
		int index;
		StringBuilder sql = new StringBuilder("INSERT INTO " + tableName + " (");

		boolean first = true;

		for (index = 0; index < values.length; index++) {
			if (autoIdField != null && fieldNames[index].equals(autoIdField)) {
				continue;
			}

			if (!first) {
				sql.append(',');
			} else {
				first = false;
			}

			sql.append(fieldNames[index]);
		}
		sql.append(") VALUES (");

		first = true;
		index = 0;
		for (Object o : values) {
			if (autoIdField != null && fieldNames[index].equals(autoIdField)) {
				index++;
				continue;
			}

			if (!first) {
				sql.append(',');
			} else {
				first = false;
			}

			if (o == null) {
				sql.append("NULL");
			} else if (o instanceof Number || o instanceof Boolean) {
				sql.append(o.toString());
			} else {
				sql.append(sqlStringValue(o.toString()));
			}
			index++;
		}
		sql.append(')');
		return sql.toString();
	}

	/**
	 * Check if a string contains SQL metacharacters. Throws an exception if it
	 * does.
	 * 
	 * @param s
	 *            the string
	 * @throws SQLException
	 */
	public void verifySqlString(String s) throws SQLException {
		if (s == null)
			return;
		if (s.matches(invalidCharacters)) {
			throw new SQLException("invalid string value " + s);
		}
	}

	/**
	 * create an SQL string value, including quoting characters, as a string
	 * 
	 * @param s
	 *            value
	 * @return string with the quoted value
	 */
	public String sqlStringValue(String s) {
		if (s == null)
			return "null";
		return "'" + s + "'";
	}

	/**
	 * creates an SQL fragment of the form <code>name='value'</code>
	 * 
	 * 
	 * @param s
	 *            a string value. if the value is <code>null</code>, the SQL
	 *            value will be <code>NULL</code>.
	 * @param name
	 *            a column name
	 * @return
	 */
	public String sqlStringValue(String s, String name) {
		if (s == null)
			return name + "=null";
		return name + "='" + s + "'";
	}

	public String sqlStringValueComma(String s) {
		return sqlStringValue(s) + ",";
	}

	public String sqlStringValueComma(String s, String name) {
		return sqlStringValue(s, name) + ",";
	}

	public String getSelectStatement() {
		return genericSelect();
	}

	public String getWhereClause(String key) {
		return " WHERE " + keyName + "='" + key + "'";
	}

	public String getInsertStatement() {
		return genericInsert(getValues());
	}

	/**
	 * Return all field values as an <code>Object</code> array. Must be in the
	 * same order as in <code>fieldNames</code>.
	 * 
	 * @return array of <code>Object</code>
	 */
	protected Object[] getValues() {
		Object[] a = {};
		return a;
	}

	public String getUpdateStatement() {
		return genericUpdate(key, getValues());
	}

	public String getCreateStatement() {
		return "CREATE TABLE ...)";
	}

	public String getDeleteStatement() {
		return "DELETE FROM " + tableName + getWhereClause(key);
	}

	public String getRenameStatement(String newName) {
		return "UPDATE " + tableName + " SET "
				+ sqlStringValue(newName, keyName) + getWhereClause(key);
	}

	public String getTableName() {
		return tableName;
	}

	public void setTableName(String tableName) {
		this.tableName = tableName;
	}

	/**
	 * Initialize an empty object from the database.
	 * 
	 * @param key
	 *            the primary key of the row to be loaded
	 * @return <code>true</code>if the object was found, <code>false</code>
	 *         otherwise
	 * @throws SQLException
	 */
	public boolean load(String key) throws SQLException {
		boolean result = false;
		verifySqlString(key);
		final String sql = getSelectStatement() + " " + getWhereClause(key);
		DbTool.debug(sql);
		Statement s = getStatement();
		ResultSet rs = null;

		try {
			rs = s.executeQuery(sql);
			if (rs.next()) {
				initFromQuery(rs);
				update = true;
				result = true;
				this.key = key;
			}
		} finally {
			releaseResultSet(rs);
			releaseStatement(s);
		}
		return result;
	}

	/**
	 * Save an object to the database. If the object was loaded from the
	 * database before with the {@link Entry#load(String)} method, an SQL UPDATE
	 * will be performed, otherwise an INSERT.
	 * 
	 * @throws SQLException
	 */
	public void save() throws SQLException {
		String sql;

		validate();

		if (update) {
			sql = getUpdateStatement();
		} else {
			sql = getInsertStatement();
		}

		Statement s = getStatement();
		DbTool.debug(sql);
		try {
			s.executeUpdate(sql);
			update = true;
		} finally {
			releaseStatement(s);
		}
	}

	/**
	 * Tries to save an entry while ignoring errors. *
	 * 
	 * @return true if successful, false if there was an exception during the
	 *         save operation
	 * @throws SQLException
	 */

	public boolean trySave() {
		try {
			save();
		} catch (SQLException ex) {
			return false;
		}
		return true;
	}

	/**
	 * returns a new {@link Statement} object from the default database
	 * connection.
	 * 
	 * @return new Statement object
	 * @throws SQLException
	 */
	protected Statement getStatement() throws SQLException {
		return DbTool.getDb().createStatement();
	}

	/**
	 * release resources of the {@link Statement} object created with
	 * {@link Entry#getStatement()}
	 * 
	 * @param s
	 * @throws SQLException
	 */
	protected void releaseStatement(Statement s) throws SQLException {
		if (s == null)
			return;

		Connection c = null;
		try {
			c = s.getConnection();
		} finally {
			try {
				s.close();
			} finally {
				c.close();
			}
		}
	}

	public void releaseResultSet(ResultSet rs) throws SQLException {
		if (rs != null)
			rs.close();
	}

	/**
	 * Delete this object from the database. The field that represents the
	 * primary key obviously needs to be set either by assignment or by using
	 * the {@link Entry#load(String)} method.
	 * 
	 * @throws SQLException
	 */
	public void delete() throws SQLException {
		String sql;

		validate();

		sql = getDeleteStatement();

		Statement s = getStatement();
		try {
			s.executeUpdate(sql);
		} finally {
			releaseStatement(s);
		}
	}

	/**
	 * Fetch all object from database
	 * 
	 * @return a <code>List</code> of <code>Entry</code> objects
	 * @throws SQLException
	 */
	public List<Entry> fetchAll() throws SQLException {
		return fetch("");
	}

	/**
	 * Fetch several objects from the database using a SQL WHERE clause.
	 * 
	 * @param where
	 *            a string containing the WHERE clause
	 * @return a <code>List</code> of <code>Entry</code> objects
	 * @throws SQLException
	 */
	public List<Entry> fetch(String where) throws SQLException {
		final String sql = getSelectStatement() + " " + where;
		return query(sql);
	}

	/**
	 * Execute an SQL query and create objects from the result.
	 * 
	 * @param sql
	 *            some select statement
	 * @return a <code>List</code> of <code>Entry</code> objects
	 * @throws SQLException
	 */
	public List<Entry> query(String sql) throws SQLException {
		List<Entry> result = new ArrayList<Entry>();

		Statement s = getStatement();
		ResultSet rs = null;

		try {
			rs = s.executeQuery(sql);
			while (rs.next()) {
				Entry e = newFromQuery(rs);
				e.key = rs.getString(e.keyName);
				e.update = true;
				result.add(e);
			}
		} finally {
			releaseResultSet(rs);
			releaseStatement(s);
		}
		return result;
	}

	public void createTable() throws SQLException {
		Statement s = getStatement();
		try {
			s.executeUpdate(getCreateStatement());
		} finally {
			releaseStatement(s);
		}
	}

	/**
	 * Rename an object in the database, altering the primary key column. The
	 * object must have been loaded before.
	 * 
	 * @param newName
	 *            the new name/primary key
	 * @throws SQLException
	 */
	public void rename(String newName) throws SQLException {
		String sql;

		verifySqlString(newName);
		validate();

		sql = getRenameStatement(newName);

		Statement s = getStatement();
		try {
			s.executeUpdate(sql);
		} finally {
			releaseStatement(s);
		}
	}
}
