/*
 * MiningMart Version 1.0
 * 
 * Copyright (C) 2006 Martin Scholz, Timm Euler, 
 *                    Daniel Hakenjos, Katharina Morik
 *
 * Contact: miningmart@ls8.cs.uni-dortmund.de
 *
 * A list of contributing developers (other than the copyright 
 * holders) can be found at
 * http://mmart.cs.uni-dortmund.de/downloads/download.html
 * 
 * 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 2
 * of the License, or (at your option) any later version.
 * 
 * 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.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program, see the file MM_HOME/LICENSE; if not, write
 * to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
 * Floor, Boston, MA 02110-1301, USA.
 */
package edu.udo.cs.miningmart.compiler.utils;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Iterator;
import java.util.Random;
import java.util.Vector;

import edu.udo.cs.miningmart.db.CompilerDatabaseService;
import edu.udo.cs.miningmart.db.DB;
import edu.udo.cs.miningmart.exception.DbConnectionClosed;
import edu.udo.cs.miningmart.exception.M4CompilerError;
import edu.udo.cs.miningmart.exception.M4Exception;
import edu.udo.cs.miningmart.m4.Column;
import edu.udo.cs.miningmart.m4.Columnset;
import edu.udo.cs.miningmart.m4.PrimaryKey;
import edu.udo.cs.miningmart.m4.RelationalDatatypes;


/**
 * @author Martin Scholz
 * @version $Id: Sampling.java,v 1.5 2006/04/11 14:10:18 euler Exp $
 */
abstract class Sampling {

	// After how many inserts into the temporary table should all changes be commited?
	public static final long COMMIT_LIMIT = 10000;

	// Specifies tables/views and attributes we are operating on:
	private final Columnset sourceCs;
	private final String    tempTable;

	// If set to a value different from <null> this Collection of attribute names
	// determines the columns to be contained in the output columnset:
	private final Collection selectedColumns;	
	
	private final long   rowcount; // number of rows in the source columnset
	private final Random random;   // the source of random bits		
	private final CompilerDatabaseService db; // an instance of class DB to access the database
	
	// need to materialize input views for Postgres:
	protected boolean materializedInput;
	protected boolean usingPostgres;
	protected boolean usingOracle;
	private String  inputTableName;
	private static String SUFFIX_FOR_MATERIALIZED_INPUT = "_MATERIALIZED_FOR_SAMPLING";
	protected Vector allRowIds;	
	protected String[] thePrimaryKey; // [0] for name of primary key attribute, [1] for its datatype name
	
	protected String numericDatatypeName, rowIdentifierName;

	protected Sampling(Columnset sourceCs, Collection selectedColumns, String tempTable, Long rowcount, Long seed, CompilerDatabaseService db)
			throws M4CompilerError
	{
		this.sourceCs = sourceCs;
		this.db = db;
		this.inputTableName = this.sourceCs.getSchemaPlusName();
		this.usingPostgres = (this.db.getBusinessDbms() == DB.POSTGRES);
		this.usingOracle = (this.db.getBusinessDbms() == DB.ORACLE);
		this.numericDatatypeName = this.db.getNameOfNumericDatatype();
		this.rowIdentifierName = this.db.getUniqueRowIdentifier(true);
		this.thePrimaryKey = this.findPrimaryKeyAttribute();
		
		this.selectedColumns = selectedColumns;
		this.tempTable = tempTable;
		this.allRowIds = new Vector();
		this.rowcount = calculateRowCount(rowcount, this.sourceCs, this.db, this.usingPostgres, this.allRowIds, this.inputTableName);
		this.random =
			(seed == null ? new Random() : new Random(seed.longValue()));

	}

	/** Helper method of the constructor. */
	private long calculateRowCount(Long rowcount, Columnset cs, DB db, boolean usingPostgres, Vector allRowIds, String inputTableName)
		throws M4CompilerError
	{
		try {
			if (rowcount == null) {
				String selectWhat = "";
				if (this.usingOracle) {
					selectWhat = "COUNT(*)";
				}
				else {
					// for Postgres we have to remember the row identifiers
					// because they do not start with 1 like in Oracle, but are arbitrary 
					// selectWhat = this.db.getUniqueRowIdentifier(true);
					selectWhat = this.thePrimaryKey[0];
				}
				
				// find number of rows by a direct database query:
				String query = "SELECT " + selectWhat + " FROM " + this.inputTableName;
				ResultSet rs = db.executeBusinessSqlRead(query);
				if (this.usingOracle) {
					if (rs.next()) {
						rowcount = new Long(rs.getLong(1));
					}
					else { 
						rowcount = null; 
					}
				}
				else {
					this.allRowIds = new Vector();			
					while (rs.next()) {
						this.allRowIds.add(new Long(rs.getLong(1)));
						this.allRowIds.trimToSize();
					}
					rowcount = new Long(this.allRowIds.size());		
				}
				String readNumberOfRows = "SELECT COUNT(*) FROM " + this.inputTableName;
				rowcount = this.db.executeBusinessSingleValueSqlReadL(readNumberOfRows);
				// If the number of rows is still unknown we cannot proceed:
				if (rowcount == null) {
					throw new M4CompilerError(
						"Constructor of DrawSample: "
						+ "Could not determine number of rows for Columnset "
						+ cs.getName()
					);	
				}
			}
		}
		catch (SQLException e) {
			throw new M4CompilerError(
				"miningmart.compiler.utils.DrawSample:\n"
				+ e.getMessage());
		}
		
		return rowcount.longValue();
	}

	/** 
	 * @return a comma separated list of the source columnset's attribute
	 * names &quot;registered&quot; as M4 columns.
	 * */
	public String getSourceAttributes() throws M4CompilerError
	{
		try
		{
			Collection columns = this.getSourceTableColumns();
			StringBuffer sb = new StringBuffer();
			Iterator it = columns.iterator();
			L : while (it.hasNext())
			{
				Column theCol = (Column) it.next();
				String colName = theCol.getName().toUpperCase();
				if ((this.selectedColumns != null
					&& !this.selectedColumns.contains(colName))
					|| (theCol.getTheBaseAttribute() == null))
				{
					continue L;
				}
				sb.append(colName + ", ");
			}
			int le = sb.length();
			if (le >= 2)
			{
				sb.delete(le - 2, le);
			}
			return sb.toString();
		}
   		catch (M4Exception m4e)
   		{   throw new M4CompilerError("M4 interface error in Sampling: " + m4e.getMessage());  } 
	}

	/** 
	 * @return the list of attributes &quot;registered&quot; as columns,
	 * in the format necessary for an SQL <code>SELECT</code> statement.
	 * If columns are &quot;virtual&quot;, then the SQL definition followed
	 * by the name is returned.
	 * */
	public String getSourceAttributeDefinitions() throws M4CompilerError
	{
		try
		{
			Collection columns = this.getSourceTableColumns();
			StringBuffer sb = new StringBuffer();
			Iterator it = columns.iterator();
			L : while (it.hasNext())
			{
				Column theColumn = (Column) it.next();

				// Skip this Column, if we have an explicit list of selected Columns,
				// not containing the current one, or if the Column is not connected to
				// a BaseAttribute:
				if ((this.selectedColumns != null
					&& !this.selectedColumns.contains(
						theColumn.getName().toUpperCase()))
					|| (theColumn.getTheBaseAttribute() == null))
				{
					continue L;
				}

				String sql = theColumn.getSQLDefinition();
				String name = theColumn.getName();
				if (sql == null || sql.equals(name) || this.materializedInput)
				{
					sb.append(name);
				}
				else
				{
					sb.append("(" + sql + ") AS " + name);
				}
				sb.append(", ");
			}
			int le = sb.length();
			if (le >= 2)
			{
				sb.delete(le - 2, le);
			}
			return sb.toString();
		}
   		catch (M4Exception m4e)
   		{   throw new M4CompilerError("M4 interface error in Sampling: " + m4e.getMessage());  } 
	}

	/** @return name of the source columnset */	
	public String getSourceTableName() {
		return this.inputTableName;
	}

	/** @return collection of columns of the source columnset */	
	public Collection getSourceTableColumns() throws M4CompilerError {
		try { 
			return this.sourceCs.getColumns();
		}
   		catch (M4Exception m4e)
   		{   throw new M4CompilerError("M4 interface error in Sampling: " + m4e.getMessage());  } 
	}

	/** @return name of the temporary table to use */	
	public String getTempTableName() {
		return this.tempTable;
	}

	/** @return number of rows of the source columnset */
	public long getRowCount() {	return this.rowcount; }

	/** @return a new uniformly distributed random double */
	public double getNextRandomDouble() {
		return (this.random.nextDouble());
	}

	/**
	 * @param sql an sql string to be executed in the business database.
	 * @throws an <code>SQLException</code>, if the database operations fail.
	 * @throws an <code>DbConnectionClosed</code>, if the database
	 * connection has been closed after a request to stop the thread.
	 *  */	
	protected void dbWrite(String sql)
		throws SQLException, DbConnectionClosed
	{
			this.db.executeBusinessSqlWrite(sql);
	}

	/**
	 * Helper method to delete a table and to ignore a possible
	 * &quot;table does not exist&quot; exception.
	 * @see edu.udo.cs.miningmart.m4.core.utils.DB#dropBusinessTable(String)
	 * */
	protected void deleteTable(String tableName) throws M4CompilerError {
		try {
			this.db.dropBusinessTable(tableName);
		}
		catch (M4Exception m4e) {
			throw new M4CompilerError(m4e.getMessage());
		}
	}

	protected void commit() throws DbConnectionClosed, SQLException {
		this.db.commitBusinessTransactions();	
	}	
	
	private void createInputTable() throws M4CompilerError {
		this.inputTableName = this.inputTableName + SUFFIX_FOR_MATERIALIZED_INPUT;
		this.materializedInput = true;		
		try {
			String inputSelect = this.sourceCs.getCompleteSQLQuery();
			if (inputSelect.toUpperCase().indexOf(" FROM (SELECT") > -1) {
				inputSelect += " AS ZZZ";
			}
		    String sql = "CREATE TABLE " + this.inputTableName + " AS " + inputSelect;
			this.db.executeBusinessSqlWrite(sql);
		}
		catch (SQLException sqle) {
		    throw new M4CompilerError("Sampling: could not materialize input view for Postgres: " + sqle.getMessage());
		}
		catch (M4Exception m4e) {
		    throw new M4CompilerError("Sampling: could not materialize input view for Postgres: " + m4e.getMessage());
		}
	}
	
	// return a String[2]: [0]: primary key column name, [1]: its datatype name
	private String[] findPrimaryKeyAttribute() throws M4CompilerError {
		
		// 1) for Oracle, use ROWNUM
		if (this.usingOracle) {
			String[] keyAttribute = new String[2];
			keyAttribute[0] = this.db.getUniqueRowIdentifier(true);
			keyAttribute[1] = this.db.getDbNameOfM4Datatype(RelationalDatatypes.RELATIONAL_DATATYPE_NUMBER, 0, true);
			return keyAttribute;
		}	
		
		// 2) see if there is a primary key
		
		try {
			// Iterator it = this.sourceCs.getPrimaryKeys().iterator();
			PrimaryKey pk = this.sourceCs.getPrimaryKey();
			if (pk != null) {
				Collection pkCols = pk.getAllColumns(); 
			
				// there might be more than one column
				// that form the primary key together!
				// TODO: Here we take only the first one...
			
				Iterator it = pkCols.iterator();
				Column primKeyColumn = null;
				if (it.hasNext()) {
					primKeyColumn = (Column) it.next();
				}
				if (primKeyColumn != null) {
					String[] keyAttribute = new String[2];
					keyAttribute[0] = primKeyColumn.getName();
					keyAttribute[1] = primKeyColumn.getColumnDataTypeName();
					return keyAttribute;
				}	
			}
		}
		catch (M4Exception m4e) {
			throw new M4CompilerError("Sampling: M4 exception when determining primary key for Columnset '" + 
			                          this.sourceCs.getName() + "': " + m4e.getMessage());
		}
		
		// 3) otherwise decide whether to materialize the input view:
		
		// HACK: materialize the input view if it is a view and we work with Postgres:
		if ((this.sourceCs.getType().equals(Columnset.CS_TYPE_VIEW)) && 
		    this.usingPostgres) {
		    this.createInputTable();
		} 
		else {
			this.materializedInput = false;
		}// end of HACK
		
		// now the table row identifier works in all cases:
		String[] keyAttribute = new String[2];
		keyAttribute[0] = this.rowIdentifierName;
		keyAttribute[1] = this.numericDatatypeName;
		
		return keyAttribute;
	}
}
/*
 * Historie
 * --------
 *
 * $Log: Sampling.java,v $
 * Revision 1.5  2006/04/11 14:10:18  euler
 * Updated license text.
 *
 * Revision 1.4  2006/04/06 16:31:18  euler
 * Prepended license remark.
 *
 * Revision 1.3  2006/03/29 09:50:48  euler
 * Added installation robustness.
 *
 * Revision 1.2  2006/01/05 14:10:20  euler
 * Bugfixes
 *
 * Revision 1.1  2006/01/03 09:54:35  hakenjos
 * Initial version!
 *
 */
