/*
 * Copyright (C) 1999-2004 <a href="mailto:mandarax@jbdietrich.com">Jens Dietrich</a>
 *
 * 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 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.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package org.mandarax.sql;

import java.sql.*;
import javax.sql.DataSource;
import org.mandarax.util.logging.LogCategories;
import org.mandarax.kernel.*;
import org.mandarax.util.ClauseIterator;
import org.mandarax.util.PredicateUtils;

/**
 * Implementation of a predicate based on a SQL query.
 * The query defines the structure of the predicate, SQL types are mapped via
 * a DBMapping object to java types. The query does not have a WHERE clause,
 * since the where clause would only define the extension (= the rows in the result set),
 * not the structure itself. The where clause comes in when clause sets are defined
 * on top of the predicate.
 * <p>
 * As from version 1.7.1, SQLPredicates can be performed (@see org.mandarax.kernel.SemanticsSupport).
 * @see java.sql.ResultSet
 * @see org.mandarax.sql.SQLTypeMapping
 * @author <A HREF="mailto:mandarax@jbdietrich.com">Jens Dietrich</A>
 * @version 3.3.1 <9 August 2004>
 * @since 1.5
 */
public class SQLPredicate implements Predicate, LogCategories {

	private String name = null;
	private DataSource dataSource = null;
	private String query = null;
	private SQLTypeMapping typeMapping = new DefaultTypeMapping();
	private Class[] structure = null;
	private String[] slotNames = null;

	/**
	 * Constructor.
	 */
	public SQLPredicate() {
		super();
	}

	/**
	 * Get the structure of the predicate, i.e. the types associated with the columns
	 * in the result set of the query. We obtain the information by issuing a query,
	 * therefore the database must be 'live'.
	 */
	public void fetchStructure() throws SQLException {
		if (dataSource != null) {
			Connection con = dataSource.getConnection();
			Statement stmt = con.createStatement();
			java.sql.ResultSet rs = stmt.executeQuery(query);
			ResultSetMetaData metaData = rs.getMetaData();
			int cols = metaData.getColumnCount();
			Class[] clazzes = new Class[cols];

			for (int i = 1; i <= cols; i++) {
				try {
					if (typeMapping == null) {
						clazzes[i - 1] = Class.forName(metaData.getColumnClassName(i));
					}
					else {
						clazzes[i - 1] = typeMapping.getType(metaData.getColumnType(i), i);
					}
				}
				catch (ClassNotFoundException x) {
					con.close();

					throw new SQLException("Result set meta data returns unknown java type " + metaData.getColumnClassName(i) + " at position " + i);
				}
			}

			structure = clazzes;
		}
	}

	/**
	 * Set the data source.
	 * @return a data source.
	 */
	public DataSource getDataSource() {
		return dataSource;
	}

	/**
	 * Get the name of the predicate.
	 * @return the name of the predicate
	 */
	public String getName() {
		return name;
	}

	/**
	 * Get the query string.
	 * @return a query
	 */
	public String getQuery() {
		return query;
	}

	/**
	 * Get the type structure of the object, e.g. the types of terms
	 * that can be used with this constructor.
	 * @return an array of classes
	 */
	public java.lang.Class[] getStructure() {
		if (structure == null) {
			try {
				fetchStructure();
			}
			catch (SQLException x) {
				LOG_SQL.error("Error fetching structure from meta data, perhaps the JDBC driver does not support this", x);
			}
		}

		return structure;
	}

	/**
	 * Set the structure of the predicates, i.e. the types of the terms.
	 * Should be handled with care since it must be consistent with the type mapping
	 * used and the (SQL) type and number of columns in the result set.
	 * @param an array of classes
	 */
	public void setStructure(Class[] struct) {
		structure = struct;
	}

	/**
	 * Set the type mapping used.
	 * @param map a type mapping
	 */
	public void setTypeMapping(SQLTypeMapping map) {
		typeMapping = map;
	}

	/**
	 * Get the new type mapping.
	 * @return a type mapping
	 */
	public SQLTypeMapping getTypeMapping() {
		return typeMapping;
	}

	/**
	 * Perform the predicate using an array of terms as parameters.
	 * If the terms can be resolved, we iterate over the result set and look for matching records.
	 * If such a record is found, Boolean.TRUE is returned. Boolean.FALSE is returned otherwise. 
	 * Note that this operation can be slow! @todo: Figure out performance improvments by adding a WHERE clause.
	 * @return the result of the perform operation
	 * @param parameter an array of terms
	 * @param session a session object
	 * @throws java.lang.UnsupportedOperationException thrown if this constructor does not have a sematics and this operation cannot be supported
	 * @throws java.lang.IllegalArgumentException
	 */
	public Object perform(org.mandarax.kernel.Term[] parameter,Session session) throws UnsupportedOperationException, IllegalArgumentException {

		Object[] objects = new Object[parameter.length];
		for (int i = 0; i < parameter.length; i++) {
			objects[i] = parameter[i].resolve(session);
		}

		try {
			SQLClauseSet cs = new SQLClauseSet(this, SQLClauseSet.NO_CACHE);
			Term[] terms = null;
			ConstantTerm t = null;
			int i = 0;
			for (ClauseIterator clauses = cs.clauses(); clauses.hasMoreClauses();) {
				Fact f = (Fact) clauses.nextClause();
				// compare
				terms = f.getTerms();
				boolean result = true;
				i = 0;
				while (result && i < terms.length) {
					t = (ConstantTerm) terms[i];
					if (!objects[i].equals(t.getObject()))
						result = false;
					i = i + 1;
				}
				if (result)
					return Boolean.TRUE;
			}
		}
		catch (Exception x) {
			LOG_SQL.debug("Exception evaluating SQL predicate", x);
			throw new IllegalArgumentException("Cannot evaluate predicate with these set of parameters");
		}
		return Boolean.FALSE;

	}

	/**
	 * Set the data source.
	 * @param ds a data source.
	 */
	public void setDataSource(DataSource ds) {
		dataSource = ds;
	}

	/**
	 * Set the query.
	 * @param q a query string (a SELECT statement <em>without</em> an WHERE clause
	 */
	public void setQuery(String q) {
		query = q;
	}

	/**
	 * Constructor.
	 * @param n a name
	 * @param q a query
	 * @param ds a data source
	 */
	public SQLPredicate(String n, String q, DataSource ds) {
		super();

		name = n;
		query = q;
		dataSource = ds;
	}

	/**
	 * Constructor with a given connection manager.
	 * 
	 * @param n a name
	 * @param q a query
	 * @param conMan the given connection manager.
	 */
	public SQLPredicate(String n, String q, SQLConnectionManager conMan) {
		this (n, q, conMan.getDataSource ());
	}

	/**
	 * Set the name.
	 * @param n a name
	 */
	public void setName(String n) {
		name = n;
	}

	/**
	 * Convert the object to a string.
	 * @return a string
	 */
	public String toString() {
		return (name == null) ? super.toString() : name;
	}

	/**
	 * Indicates whether the object (usually a term or a clause set) can be performed
	 * using the java semantics.
	 * @return false
	 */
	public boolean isExecutable() {
		return true;
	}

	/**
	 * Compare objects.
	 * @param obj another object
	 * @return a boolean
	 */
	public boolean equals(Object obj) {
		if ((obj != null) && (obj instanceof SQLPredicate)) {
			SQLPredicate p = (SQLPredicate) obj;
			boolean result = true;

			// compare query, name and data source
			// bugfix in 1.8 - thanks to chenjb@gsta.com
			result = (name == null) ? (p.name == null) : name.equals(p.name);
			result = result && ((dataSource == null) ? (p.dataSource == null) : dataSource.equals(p.dataSource));
			result = result && ((query == null) ? (p.query == null) : query.equals(p.query));
			result = result && ((typeMapping == null) ? (p.typeMapping == null) : typeMapping.equals(p.typeMapping));

			Class[] c1 = p.structure;
			Class[] c2 = structure;

			if (c1 == null) {
				result = c2 == null;
			}
			else {
				result = result && (c1.length == c2.length);

				for (int i = 0; i < c1.length; i++) {
					result = result && (c1[i] == c2[i]);
				}
			}

			return result;
		}
		else {
			return false;
		}
	}

	/**
	 * Get the hashcode of the object.
	 * @return the hash code of the object
	 */
	public int hashCode() {
		return ((name == null) ? 0 : name.hashCode()) ^ ((query == null) ? 0 : query.hashCode()) ^ ((dataSource == null) ? 0 : dataSource.hashCode());
	}
	/**
	 * Get the slot names.
	 * @return an array of strings, the length of the array is the same as
	 * the length of the array of terms (the structure of the predicate)
	 */
	public String[] getSlotNames() {
		if (slotNames==null) {
			slotNames = new String[getStructure().length];
			for (int i=0;i<slotNames.length;i++) slotNames[i] = PredicateUtils.getSlotName(this,i); 
		}
		return slotNames;
	}
	/**
	 * Set the slot names.
	 * @param names an array of strings, the length of the array is the same as
	 * the length of the array of terms (the structure of the predicate)
	 */
	public void setSlotNames(String[] names) {
		if (names!=null && names.length!=getStructure().length) throw new IllegalArgumentException("Number of slot names and number of slots must match - cannot set slot names for predicate " + this);
		this.slotNames = names;
	}
	/**
	 * Indicates whether the slot names can be modified.
	 * @return a boolean
	 */
	public boolean slotNamesCanBeEdited() {
		return true;
	}
}
