/*
 * Copyright Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags and
 * the COPYRIGHT.txt file distributed with this work.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.teiid.jdbc;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.net.URL;
import java.nio.charset.Charset;
import java.sql.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.regex.Matcher;

import javax.sql.rowset.serial.SerialArray;

import org.teiid.client.RequestMessage;
import org.teiid.client.RequestMessage.ResultsMode;
import org.teiid.client.RequestMessage.StatementType;
import org.teiid.client.metadata.MetadataResult;
import org.teiid.client.util.ResultsFuture;
import org.teiid.core.TeiidComponentException;
import org.teiid.core.TeiidProcessingException;
import org.teiid.core.types.ArrayImpl;
import org.teiid.core.types.BlobImpl;
import org.teiid.core.types.ClobImpl;
import org.teiid.core.types.DataTypeManager;
import org.teiid.core.types.InputStreamFactory;
import org.teiid.core.types.JDBCSQLTypeInfo;
import org.teiid.core.types.Streamable;
import org.teiid.core.util.MultiArrayOutputStream;
import org.teiid.core.util.ReaderInputStream;
import org.teiid.core.util.SqlUtil;
import org.teiid.core.util.TimestampWithTimezone;
import org.teiid.netty.handler.codec.serialization.CompactObjectOutputStream;


/**
 * <p> Instances of PreparedStatement contain a SQL statement that has already been
 * compiled.  The SQL statement contained in a PreparedStatement object may have
 * one or more IN parameters. An IN parameter is a parameter whose value is not
 * specified when a SQL statement is created. Instead the statement has a placeholder
 * for each IN parameter.</p>
 * <p> The MMPreparedStatement object wraps the server's PreparedStatement object.
 * The methods in this class are used to set the IN parameters on a server's
 * preparedstatement object.</p>
 */

public class PreparedStatementImpl extends StatementImpl implements TeiidPreparedStatement {
	// sql, which this prepared statement is operating on
    protected String prepareSql;

    //map that holds parameter index to values for prepared statements
    private Map<Integer, Object> parameterMap;
    TreeMap<String, Integer> paramsByName;
    
    //a list of map that holds parameter index to values for prepared statements
    protected List<List<Object>> batchParameterList;

    // metadata
	private MetadataResult metadataResults;
    private ResultSetMetaData metadata;
    private ParameterMetaDataImpl parameterMetaData;
    
    private Calendar serverCalendar;
    private Object command;
    
    private boolean autoGeneratedKeys;

    /**
     * <p>MMPreparedStatement constructor.
     * @param Driver's connection object.
     * @param String object representing the prepared statement
     */
    PreparedStatementImpl(ConnectionImpl connection, String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        super(connection, resultSetType, resultSetConcurrency);

        if (sql == null) {
        	throw new TeiidSQLException(JDBCPlugin.Util.getString("MMPreparedStatement.Err_prep_sql")); //$NON-NLS-1$
        }
        this.prepareSql = sql;

        TimeZone timezone = connection.getServerConnection().getLogonResult().getTimeZone();

        if (timezone != null && !timezone.hasSameRules(getDefaultCalendar().getTimeZone())) {
        	this.serverCalendar = Calendar.getInstance(timezone);
        }        
    }
    
    public void setAutoGeneratedKeys(boolean getAutoGeneratedKeys) {
		this.autoGeneratedKeys = getAutoGeneratedKeys;
	}

    /**
     * <p>Adds a set of parameters to this PreparedStatement object's list of commands
     * to be sent to the database for execution as a batch.
     * @throws SQLException if there is an error
     */
    public void addBatch() throws SQLException {
        checkStatement();
    	if(batchParameterList == null){
    		batchParameterList = new ArrayList<List<Object>>();
		}
    	batchParameterList.add(getParameterValues());
    }

    /**
     * Makes the set of commands in the current batch empty.
     *
     * @throws SQLException if a database access error occurs or the
     * driver does not support batch statements
     */
    public void clearBatch() throws SQLException {
    	if (batchParameterList != null ) {
    		batchParameterList.clear();
    	}
    }

    /**
     * <p>Clears the values set for the PreparedStatement object's IN parameters and
     * releases the resources used by those values. In general, parameter values
     * remain in force for repeated use of statement.
     * @throws SQLException if there is an error while clearing params
     */
    public void clearParameters() throws SQLException {
        checkStatement();
        //clear the parameters list on servers prepared statement object
        if(parameterMap != null){
            parameterMap.clear();
        }
    }
    
    @Override
    public boolean execute(String sql) throws SQLException {
    	String msg = JDBCPlugin.Util.getString("JDBC.Method_not_supported"); //$NON-NLS-1$
        throw new TeiidSQLException(msg);
    }
    
    @Override
    public void submitExecute(String sql, StatementCallback callback, RequestOptions options) throws TeiidSQLException {
    	String msg = JDBCPlugin.Util.getString("JDBC.Method_not_supported"); //$NON-NLS-1$
        throw new TeiidSQLException(msg);
    }
    
    @Override
    public ResultSet executeQuery(String sql) throws SQLException {
    	String msg = JDBCPlugin.Util.getString("JDBC.Method_not_supported"); //$NON-NLS-1$
        throw new TeiidSQLException(msg);
    }
    
    @Override
    public int executeUpdate(String sql) throws SQLException {
    	String msg = JDBCPlugin.Util.getString("JDBC.Method_not_supported"); //$NON-NLS-1$
        throw new TeiidSQLException(msg);
    }
    
    @Override
    public boolean execute(String sql, int autoGeneratedKeys)
    		throws SQLException {
    	String msg = JDBCPlugin.Util.getString("JDBC.Method_not_supported"); //$NON-NLS-1$
        throw new TeiidSQLException(msg);
    }
    
    @Override
    public int executeUpdate(String sql, int autoGeneratedKeys)
    		throws SQLException {
    	String msg = JDBCPlugin.Util.getString("JDBC.Method_not_supported"); //$NON-NLS-1$
        throw new TeiidSQLException(msg);
    }
    
    @Override
    public boolean execute(String sql, int[] columnIndexes) throws SQLException {
    	String msg = JDBCPlugin.Util.getString("JDBC.Method_not_supported"); //$NON-NLS-1$
        throw new TeiidSQLException(msg);
    }
    
    @Override
    public boolean execute(String sql, String[] columnNames)
    		throws SQLException {
    	String msg = JDBCPlugin.Util.getString("JDBC.Method_not_supported"); //$NON-NLS-1$
        throw new TeiidSQLException(msg);
    }
    
    @Override
    public int executeUpdate(String sql, int[] columnIndexes)
    		throws SQLException {
    	String msg = JDBCPlugin.Util.getString("JDBC.Method_not_supported"); //$NON-NLS-1$
        throw new TeiidSQLException(msg);
    }
    
    @Override
    public int executeUpdate(String sql, String[] columnNames)
    		throws SQLException {
    	String msg = JDBCPlugin.Util.getString("JDBC.Method_not_supported"); //$NON-NLS-1$
        throw new TeiidSQLException(msg);
    }
    
    @Override
    public void addBatch(String sql) throws SQLException {
    	String msg = JDBCPlugin.Util.getString("JDBC.Method_not_supported"); //$NON-NLS-1$
        throw new TeiidSQLException(msg);
    }
    
    @Override
    public void submitExecute(StatementCallback callback, RequestOptions options)
    		throws SQLException {
    	NonBlockingRowProcessor processor = new NonBlockingRowProcessor(this, callback);
    	submitExecute(ResultsMode.EITHER, options).addCompletionListener(processor);
    }
    
    public ResultsFuture<Boolean> submitExecute(ResultsMode mode, RequestOptions options) throws SQLException {
        return executeSql(new String[] {this.prepareSql}, false, mode, false, options, autoGeneratedKeys);
    }

	@Override
    public boolean execute() throws SQLException {
        executeSql(new String[] {this.prepareSql}, false, ResultsMode.EITHER, true, null, autoGeneratedKeys);
        return hasResultSet();
    }
    
    @Override
    public int[] executeBatch() throws SQLException {
    	if (batchParameterList == null || batchParameterList.isEmpty()) {
   	     	return new int[0];
    	}
	   	try{
	   		//check to see if we need to split large batches
	   		int[] allUpdateCounts = null;
	   		if (batchParameterList.size() > 256 && !this.getConnection().getServerConnection().isLocal()) {
	   			MultiArrayOutputStream maos = new MultiArrayOutputStream(1 << 13);
	   			try {
	   				CompactObjectOutputStream coos = new CompactObjectOutputStream(maos);
		   			List<List<Object>> copy = new ArrayList<List<Object>>(this.batchParameterList);
		   			int start = 0;
		   			for (int i = 0; i < copy.size(); i++) {
		   				coos.writeObject(copy.get(i));
		   				coos.flush();
		   				//if we have have over .5 mb, then send that as a single message
		   				//TODO: we could use max message size from the server
		   				if (maos.getCount() > (1 << 19) && i + 1 < copy.size()) {
		   					this.batchParameterList = copy.subList(start, i + 1);
		   					start = i + 1;
		   					executeSql(new String[] {this.prepareSql}, true, ResultsMode.UPDATECOUNT, true, null);
		   					if (allUpdateCounts == null) {
		   						allUpdateCounts = this.updateCounts;
		   					} else {
		   						allUpdateCounts = concatArrays(allUpdateCounts, this.updateCounts);
		   					}
		   					coos.reset();
		   					maos.reset(0);
		   				}
		   			}
		   			if (allUpdateCounts != null) {
		   				if (start < copy.size()) {
			   				this.batchParameterList = copy.subList(start, copy.size());
			   				executeSql(new String[] {this.prepareSql}, true, ResultsMode.UPDATECOUNT, true, null);
	   						allUpdateCounts = concatArrays(allUpdateCounts, this.updateCounts);
		   				}
   						this.updateCounts = allUpdateCounts;
		   			}
	   			} catch (IOException e) {
	   				TeiidSQLException ex = TeiidSQLException.create(e);
	   				throw new BatchUpdateException(e.getMessage(), ex.getSQLState(), ex.getErrorCode(), allUpdateCounts==null?new int[0]:allUpdateCounts, e);
	   			} catch (BatchUpdateException e) {
	   				if (allUpdateCounts == null) {
	   					throw e;
	   				}
	   				allUpdateCounts = concatArrays(allUpdateCounts, e.getUpdateCounts());
	   				throw new BatchUpdateException(e.getMessage(), e.getSQLState(), e.getErrorCode(), allUpdateCounts, e);
	   			}
	   		}
	   		if (allUpdateCounts == null) {
	   			executeSql(new String[] {this.prepareSql}, true, ResultsMode.UPDATECOUNT, true, null);
	   		}
	   	}finally{
	   		batchParameterList.clear();
	   	}
	   	return this.updateCounts;
    }

	static int[] concatArrays(int[] array1, int[] array2) {
		int length = array1.length;
		array1 = Arrays.copyOf(array1, length + array2.length);
		System.arraycopy(array2, 0, array1, length, array2.length);
		return array1;
	}

	@Override
    public ResultSetImpl executeQuery() throws SQLException {
        executeSql(new String[] {this.prepareSql}, false, ResultsMode.RESULTSET, true, null, autoGeneratedKeys);
        return resultSet;
    }

	@Override
    public int executeUpdate() throws SQLException {
        executeSql(new String[] {this.prepareSql}, false, ResultsMode.UPDATECOUNT, true, null, autoGeneratedKeys);
        if (this.updateCounts == null) {
        	return 0;
        }
        if (this.updateCounts.length == 0) {
            return 0;
        }
        return this.updateCounts[0];
    }
    
    @Override
    protected RequestMessage createRequestMessage(String[] commands,
    		boolean isBatchedCommand, ResultsMode resultsMode) {
    	RequestMessage message = super.createRequestMessage(commands, false, resultsMode);
    	message.setStatementType(StatementType.PREPARED);
    	message.setParameterValues(isBatchedCommand?getParameterValuesList(): getParameterValues());
    	message.setBatchedUpdate(isBatchedCommand);
    	message.setCommand(this.command);
    	return message;
    }

    public ResultSetMetaData getMetaData() throws SQLException {

        // check if the statement is open
        checkStatement();

        if(metadata == null) {
        	if (updateCounts != null) {
        		return null;
        	} else if(resultSet != null) {
                metadata = resultSet.getMetaData();
            } else {
				Matcher matcher = StatementImpl.SHOW_STATEMENT.matcher(prepareSql);
				if (matcher.matches()) {
					this.executeShow(matcher);
					metadata = this.resultSet.getMetaData();
					this.resultSet = null;
					return metadata;
				} 
				if (getMetadataResults().getColumnMetadata() == null || getMetadataResults().getColumnMetadata().length == 0) {
					return null;
				}
                MetadataProvider provider = new MetadataProvider(getMetadataResults().getColumnMetadata());
                metadata = new ResultSetMetaDataImpl(provider, this.getExecutionProperty(ExecutionProperties.JDBC4COLUMNNAMEANDLABELSEMANTICS));
            }
        }

        return metadata;
    }

	private MetadataResult getMetadataResults() throws SQLException {
		if (metadataResults == null) {
			if (StatementImpl.SET_STATEMENT.matcher(prepareSql).matches() 
					|| StatementImpl.TRANSACTION_STATEMENT.matcher(prepareSql).matches()
					|| StatementImpl.SHOW_STATEMENT.matcher(prepareSql).matches()
					|| StatementImpl.SET_CHARACTERISTIC_STATEMENT.matcher(prepareSql).matches()) {
				metadataResults = new MetadataResult();
			} else {
				try {
					metadataResults = this.getDQP().getMetadata(this.currentRequestID, prepareSql, Boolean.valueOf(getExecutionProperty(ExecutionProperties.ANSI_QUOTED_IDENTIFIERS)).booleanValue());
				} catch (TeiidComponentException e) {
					throw TeiidSQLException.create(e);
				} catch (TeiidProcessingException e) {
					throw TeiidSQLException.create(e);
				}
			}
		}
		return metadataResults;
	}

    public void setAsciiStream(int parameterIndex, java.io.InputStream in, int length) throws SQLException {
    	setAsciiStream(parameterIndex, in);
    }

    public void setBigDecimal (int parameterIndex, java.math.BigDecimal value) throws SQLException {
        setObject(parameterIndex, value);
    }

    public void setBinaryStream(int parameterIndex, java.io.InputStream in, int length) throws SQLException {
    	setBlob(parameterIndex, in);
    }

    public void setBlob (int parameterIndex, Blob x) throws SQLException {
        setObject(parameterIndex, x);
    }

    public void setBoolean (int parameterIndex, boolean value) throws SQLException {
        setObject(parameterIndex, Boolean.valueOf(value));
    }

    public void setByte(int parameterIndex, byte value) throws SQLException {
        setObject(parameterIndex, Byte.valueOf(value));
    }

    public void setBytes(int parameterIndex, byte bytes[]) throws SQLException {
    	setObject(parameterIndex, bytes);
    }

    public void setCharacterStream (int parameterIndex, java.io.Reader reader, int length) throws SQLException {
    	setCharacterStream(parameterIndex, reader);
    }

    public void setClob (int parameterIndex, Clob x) throws SQLException {
        setObject(parameterIndex, x);
    }

    public void setDate(int parameterIndex, java.sql.Date value) throws SQLException {
        setDate(parameterIndex, value, null);
    }
    
    public void setDate(int parameterIndex, java.sql.Date x ,java.util.Calendar cal) throws SQLException {
    	setDate(Integer.valueOf(parameterIndex), x, cal);
    }

    void setDate(Object parameterIndex, java.sql.Date x ,java.util.Calendar cal) throws SQLException {

        if (cal == null || x == null) {
            setObject(parameterIndex, x);
            return;
        }
                
        // set the parameter on the stored procedure
        setObject(parameterIndex, TimestampWithTimezone.createDate(x, cal.getTimeZone(), getDefaultCalendar()));
    }

    public void setDouble(int parameterIndex, double value) throws SQLException {
        setObject(parameterIndex, new Double(value));
    }

    public void setFloat(int parameterIndex, float value) throws SQLException {
        setObject(parameterIndex, new Float(value));
    }

    public void setInt(int parameterIndex, int value) throws SQLException {
        setObject(parameterIndex, Integer.valueOf(value));
    }

    public void setLong(int parameterIndex, long value) throws SQLException {
        setObject(parameterIndex, Long.valueOf(value));
    }

    public void setNull(int parameterIndex, int jdbcType) throws SQLException {
        setObject(parameterIndex, null);
    }

    public void setNull(int parameterIndex, int jdbcType, String typeName) throws SQLException {
        setObject(parameterIndex, null);
    }
    
    public void setObject (int parameterIndex, Object value, int targetJdbcType, int scale) throws SQLException {
    	setObject(Integer.valueOf(parameterIndex), value, targetJdbcType, scale);
    }

    void setObject (Object parameterIndex, Object value, int targetJdbcType, int scale) throws SQLException {

       if(value == null) {
            setObject(parameterIndex, null);
            return;
        }

       if(targetJdbcType != Types.DECIMAL || targetJdbcType != Types.NUMERIC) {
            setObject(parameterIndex, value, targetJdbcType);
        // Decimal and NUMERIC types correspond to java.math.BigDecimal
        } else {
            // transform the object to a BigDecimal
            BigDecimal bigDecimalObject = DataTypeTransformer.getBigDecimal(value);
            // set scale on the BigDecimal
            setObject(parameterIndex, bigDecimalObject.setScale(scale));
        }
    }
    
    public void setObject(int parameterIndex, Object value, int targetJdbcType) throws SQLException {
    	setObject(Integer.valueOf(parameterIndex), value, targetJdbcType);
    }

    void setObject(Object parameterIndex, Object value, int targetJdbcType) throws SQLException {
        Object targetObject = value;

        if(value == null) {
            setObject(parameterIndex, null);
            return;
        }

        // get the java class name for the given JDBC type
        String typeName = JDBCSQLTypeInfo.getTypeName(targetJdbcType);
        int typeCode = DataTypeManager.getTypeCode(DataTypeManager.getDataTypeClass(typeName));
        // transform the value to the target datatype
        switch (typeCode) {
        case DataTypeManager.DefaultTypeCodes.STRING:
        	targetObject = DataTypeTransformer.getString(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.CHAR:
        	targetObject = DataTypeTransformer.getCharacter(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.INTEGER:
        	targetObject = DataTypeTransformer.getInteger(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.BYTE:
        	targetObject = DataTypeTransformer.getByte(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.SHORT:
        	targetObject = DataTypeTransformer.getShort(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.LONG:
        	targetObject = DataTypeTransformer.getLong(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.FLOAT:
        	targetObject = DataTypeTransformer.getFloat(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.DOUBLE:
        	targetObject = DataTypeTransformer.getDouble(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.BOOLEAN:
        	targetObject = DataTypeTransformer.getBoolean(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.BIGDECIMAL:
        	targetObject = DataTypeTransformer.getBigDecimal(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.TIMESTAMP:
        	targetObject = DataTypeTransformer.getTimestamp(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.DATE:
        	targetObject = DataTypeTransformer.getDate(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.TIME:
        	targetObject = DataTypeTransformer.getTime(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.BLOB:
        	targetObject = DataTypeTransformer.getBlob(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.CLOB:
        	targetObject = DataTypeTransformer.getClob(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.XML:
        	targetObject = DataTypeTransformer.getSQLXML(value);
        	break;
        case DataTypeManager.DefaultTypeCodes.VARBINARY:
        	targetObject = DataTypeTransformer.getBytes(value);
        	break;
        }

        setObject(parameterIndex, targetObject);
    }
    
    public void setObject(int parameterIndex, Object value) throws SQLException {
    	setObject(Integer.valueOf(parameterIndex), value);
    }

    void setObject(Object parameterIndex, Object value) throws SQLException {
    	if (parameterIndex instanceof String) {
	    	String s = (String)parameterIndex;
			if (paramsByName == null) {
				paramsByName = new TreeMap<String, Integer>(String.CASE_INSENSITIVE_ORDER);
				ParameterMetaDataImpl pmdi = getParameterMetaData();
				for (int i = 1; i <= pmdi.getParameterCount(); i++) {
					String name = pmdi.getParameterName(i);
					paramsByName.put(name, i);
				}
			}
			parameterIndex = paramsByName.get(s);
			if (parameterIndex == null) {
				throw new TeiidSQLException(JDBCPlugin.Util.getString("MMCallableStatement.Param_not_found", s)); //$NON-NLS-1$
			}
    	}
		if ((Integer)parameterIndex < 1) {
			throw new TeiidSQLException(JDBCPlugin.Util.getString("MMPreparedStatement.Invalid_param_index")); //$NON-NLS-1$
		}

        if(parameterMap == null){
            parameterMap = new TreeMap<Integer, Object>();
        }
        
        if (serverCalendar != null && value instanceof java.util.Date) {
            value = TimestampWithTimezone.create((java.util.Date)value, getDefaultCalendar().getTimeZone(), serverCalendar, value.getClass());
        }
        parameterMap.put((Integer)parameterIndex, value);
    }

    public void setShort(int parameterIndex, short value) throws SQLException {
        setObject(parameterIndex, Short.valueOf(value));
    }

    public void setString(int parameterIndex, String value) throws SQLException {
        setObject(parameterIndex, value);
    }

    public void setTime(int parameterIndex, java.sql.Time value) throws SQLException {
        setTime(parameterIndex, value, null);
    }
    
    public void setTime(int parameterIndex, java.sql.Time x, java.util.Calendar cal) throws SQLException {
    	setTime(Integer.valueOf(parameterIndex), x, cal);
    }

    void setTime(Object parameterIndex, java.sql.Time x, java.util.Calendar cal) throws SQLException {

       if (cal == null || x == null) {
           setObject(parameterIndex, x);
           return;
       }
               
       // set the parameter on the stored procedure
       setObject(parameterIndex, TimestampWithTimezone.createTime(x, cal.getTimeZone(), getDefaultCalendar()));
    }

    public void setTimestamp(int parameterIndex, java.sql.Timestamp value) throws SQLException {
        setTimestamp(parameterIndex, value, null);
    }
    
    public void setTimestamp(int parameterIndex, java.sql.Timestamp x, java.util.Calendar cal) throws SQLException {
    	setTimestamp(Integer.valueOf(parameterIndex), x, cal);
    }

    void setTimestamp(Object parameterIndex, java.sql.Timestamp x, java.util.Calendar cal) throws SQLException {

        if (cal == null || x == null) {
            setObject(parameterIndex, x);
            return;
        }
                
        // set the parameter on the stored procedure
        setObject(parameterIndex, TimestampWithTimezone.createTimestamp(x, cal.getTimeZone(), getDefaultCalendar()));
    }

    public void setURL(int parameterIndex, URL x) throws SQLException {
        setObject(parameterIndex, x);
    }

    List<List<Object>> getParameterValuesList() {
    	if(batchParameterList == null || batchParameterList.isEmpty()){
    		return Collections.emptyList();
    	}
    	return new ArrayList<List<Object>>(batchParameterList);
    }
    
    List<Object> getParameterValues() {
        if(parameterMap == null || parameterMap.isEmpty()){
            return Collections.emptyList();
        }
        return new ArrayList<Object>(parameterMap.values());
    }

	public ParameterMetaDataImpl getParameterMetaData() throws SQLException {
		if (parameterMetaData == null) {
			//TODO: some of the base implementation of ResultSetMetadata could be on the MetadataProvider
			this.parameterMetaData = new ParameterMetaDataImpl(new ResultSetMetaDataImpl(new MetadataProvider(getMetadataResults().getParameterMetadata()), this.getExecutionProperty(ExecutionProperties.JDBC4COLUMNNAMEANDLABELSEMANTICS)));
		}
		return parameterMetaData;
	}

    /**
     * Exposed for unit testing 
     */
    void setServerCalendar(Calendar serverCalendar) {
        this.serverCalendar = serverCalendar;
    }

	public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException {
		setObject(parameterIndex, xmlObject);
	}

	public void setArray(int parameterIndex, Array x) throws SQLException {
		if (x instanceof ArrayImpl) {
			setObject(parameterIndex, x);
		} else {
			setObject(parameterIndex, new SerialArray(x));
		}
	}

	@Override
	public void setAsciiStream(int parameterIndex, final InputStream x) 
		throws SQLException {
		setAsciiStream(Integer.valueOf(parameterIndex), x);
	}

	void setAsciiStream(Object parameterIndex, final InputStream x)
			throws SQLException {
		if (x == null) {
			this.setObject(parameterIndex, null);
			return;
		}
		this.setObject(parameterIndex, new ClobImpl(new InputStreamFactory() { 
			@Override
			public InputStream getInputStream() throws IOException {
				return x;
			}
		}, -1));
	}

	public void setAsciiStream(int parameterIndex, InputStream x, long length)
			throws SQLException {
		setAsciiStream(parameterIndex, x);
	}

	public void setBinaryStream(int parameterIndex, InputStream x)
			throws SQLException {
		setBlob(parameterIndex, x);
	}

	public void setBinaryStream(int parameterIndex, InputStream x, long length)
			throws SQLException {
		setBinaryStream(parameterIndex, x);
	}

	public void setBlob(int parameterIndex, final InputStream inputStream)
	throws SQLException {
		setBlob(Integer.valueOf(parameterIndex), inputStream);
	}
	
	void setBlob(Object parameterIndex, final InputStream inputStream)
			throws SQLException {
		if (inputStream == null) {
			this.setObject(parameterIndex, null);
			return;
		}
		this.setObject(parameterIndex, new BlobImpl(new InputStreamFactory() {
			@Override
			public InputStream getInputStream() throws IOException {
				return inputStream;
			}
		}));
	}

	public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException {
		setBlob(parameterIndex, inputStream);
	}

	public void setCharacterStream(int parameterIndex, Reader reader)
			throws SQLException {
		setClob(parameterIndex, reader);
	}

	public void setCharacterStream(int parameterIndex, Reader reader,
			long length) throws SQLException {
		setCharacterStream(parameterIndex, reader);
	}
	
	public void setClob(int parameterIndex, final Reader reader) throws SQLException {
		setClob(Integer.valueOf(parameterIndex), reader);
	}

	void setClob(Object parameterIndex, final Reader reader) throws SQLException {
		if (reader == null) {
			this.setObject(parameterIndex, null);
			return;
		}
		final ReaderInputStream readerInputStream = new ReaderInputStream(reader, Charset.forName(Streamable.ENCODING));
		this.setObject(parameterIndex, new ClobImpl(new InputStreamFactory() {
			
			@Override
			public InputStream getInputStream() throws IOException {
				return readerInputStream;
			}
		}, -1));
	}

	public void setClob(int parameterIndex, Reader reader, long length)
			throws SQLException {
		setClob(parameterIndex, reader);
	}

	public void setNCharacterStream(int parameterIndex, Reader value)
			throws SQLException {
		setClob(parameterIndex, value);
	}

	public void setNCharacterStream(int parameterIndex, Reader value,
			long length) throws SQLException {
		setCharacterStream(parameterIndex, value);
	}

	public void setNClob(int parameterIndex, NClob value) throws SQLException {
		setObject(parameterIndex, value);
	}

	public void setNClob(int parameterIndex, Reader reader) throws SQLException {
		setClob(parameterIndex, reader);
	}

	public void setNClob(int parameterIndex, Reader reader, long length)
			throws SQLException {
		setClob(parameterIndex, reader);
	}

	public void setNString(int parameterIndex, String value)
			throws SQLException {
		setObject(parameterIndex, value);
	}

	public void setRef(int parameterIndex, Ref x) throws SQLException {
		throw SqlUtil.createFeatureNotSupportedException();
	}

	public void setRowId(int parameterIndex, RowId x) throws SQLException {
		throw SqlUtil.createFeatureNotSupportedException();
	}

	public void setUnicodeStream(int parameterIndex, InputStream x, int length)
			throws SQLException {
		throw SqlUtil.createFeatureNotSupportedException();
	}
	
	public void setCommand(Object command) {
		this.command = command;
	}
}
