package eu.dnetlib.dlms.jdbc;

import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.NClob;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.Ref;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.RowId;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Calendar;

import eu.dnetlib.dlms.jdbc.server.AbstractDOLEngine;
import eu.dnetlib.dlms.lowlevel.objects.Atom;
import eu.dnetlib.dlms.lowlevel.objects.LLDigitalObject;
import eu.dnetlib.dlms.lowlevel.objects.Relation;
import eu.dnetlib.dlms.lowlevel.objects.structures.BooleanBasicValue;
import eu.dnetlib.dlms.lowlevel.objects.structures.DateBasicValue;
import eu.dnetlib.dlms.lowlevel.objects.structures.IntBasicValue;
import eu.dnetlib.dlms.lowlevel.objects.structures.StringBasicValue;
import eu.dnetlib.dlms.lowlevel.objects.structures.Structure;

/**
 * PreparedStatement implementation.
 * 
 * @author lexis
 */
public class DOLPreparedStatement extends DOLStatement implements PreparedStatement {

	/** Parameters of the string to be executed. */
	private ParamInfo[] parameters;
	/** ParameterMetadata instance that holds info about the parameter placeholders' properties and types. */
	private DOLParameterMetadata parameterMetaData;

	/**
	 * Constructor.
	 * 
	 * @param connection
	 *            DorotyConnection instance
	 * @throws SQLException
	 *             Constructor Exception
	 */
	public DOLPreparedStatement(final DorotyConnection connection) throws SQLException {
		super(connection);
		this.parameterMetaData = new DOLParameterMetadata(this);
	}

	/**
	 * Constructor.
	 * 
	 * @param connection
	 *            a DorotyConnection instance
	 * @param dol
	 *            a DOL statement that may contain one or more '?' IN parameter placeholders
	 * @throws SQLException
	 *             Constructor Exception
	 */
	public DOLPreparedStatement(final DorotyConnection connection, final String dol) throws SQLException {
		this(connection);
		this.setDolString(dol);
		this.parameters = new ParamInfo[this.countNumberOfParameters()];
	}

	public ParamInfo[] getParameters() {
		return this.parameters;
	}

	public void setParameterMetaData(final DOLParameterMetadata paramMetadata) {
		this.parameterMetaData = paramMetadata;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#addBatch()
	 */
	public void addBatch() throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#clearParameters()
	 */
	public void clearParameters() throws SQLException {
		if (this.isClosed())
			throw new SQLException();
		this.parameters = new ParamInfo[this.parameters.length];
		this.parameterMetaData = null;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#execute()
	 */
	public boolean execute() throws SQLException {
		if (this.isClosed()) {
			throw new SQLException("Cannot call execute on a closed PreparedStatement");
		}
		boolean hasResult = false;
		if (this.allParametersSet()) {
			AbstractDOLEngine eng = this.getConnection().getDolEngine();
			this.setDolExecuter(eng.execute(this.getDolString(), this.parameters));
			hasResult = this.getDolExecuter().isResultSetExpected();
			if (hasResult) {
				DOLResultSet rs = this.getResultSetFactory().createDOLResultSet(this, this.getDolExecuter().getColumns());
				this.setResultSet(rs);
			} else
				this.setResultSet(null);
		} else {
			throw new SQLException("Missing parameter(s)");
		}
		return hasResult;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#executeQuery()
	 */
	public ResultSet executeQuery() throws SQLException {
		if (this.getResultSet() != null)
			this.getResultSet().close();
		boolean hasResult = this.execute();
		if (!hasResult) {
			throw new SQLException("Called executeQuery on a DOLPreparedStatement that does not return a ResultSet");
		}
		return this.getResultSet();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#executeUpdate()
	 */
	public int executeUpdate() throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#getMetaData()
	 */
	public ResultSetMetaData getMetaData() throws SQLException {
		return this.getResultSet().getMetaData();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#getParameterMetaData()
	 */
	public ParameterMetaData getParameterMetaData() throws SQLException {
		return this.parameterMetaData;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setArray(int, java.sql.Array)
	 */
	public void setArray(final int parameterIndex, final Array x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setAsciiStream(int, java.io.InputStream)
	 */
	public void setAsciiStream(final int parameterIndex, final InputStream x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setAsciiStream(int, java.io.InputStream, int)
	 */
	public void setAsciiStream(final int parameterIndex, final InputStream x, final int length) throws SQLException {
		throw new SQLFeatureNotSupportedException();

	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setAsciiStream(int, java.io.InputStream, long)
	 */
	public void setAsciiStream(final int parameterIndex, final InputStream x, final long length) throws SQLException {
		throw new SQLFeatureNotSupportedException();

	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setBigDecimal(int, java.math.BigDecimal)
	 */
	public void setBigDecimal(final int parameterIndex, final BigDecimal x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setBinaryStream(int, java.io.InputStream)
	 */
	public void setBinaryStream(final int parameterIndex, final InputStream x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setBinaryStream(int, java.io.InputStream, int)
	 */
	public void setBinaryStream(final int parameterIndex, final InputStream x, final int length) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setBinaryStream(int, java.io.InputStream, long)
	 */
	public void setBinaryStream(final int parameterIndex, final InputStream x, final long length) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setBlob(int, java.sql.Blob)
	 */
	public void setBlob(final int parameterIndex, final Blob x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setBlob(int, java.io.InputStream)
	 */
	public void setBlob(final int parameterIndex, final InputStream x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setBlob(int, java.io.InputStream, long)
	 */
	public void setBlob(final int parameterIndex, final InputStream x, final long length) throws SQLException {
		throw new SQLFeatureNotSupportedException();

	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setBoolean(int, boolean)
	 */
	public void setBoolean(final int parameterIndex, final boolean x) throws SQLException {
		this.setObjectBase(parameterIndex, new Boolean(x), DorotyObjectEnum.std_boolean);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setByte(int, byte)
	 */

	public void setByte(final int parameterIndex, final byte x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setBytes(int, byte[])
	 */

	public void setBytes(final int parameterIndex, final byte[] x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setCharacterStream(int, java.io.Reader)
	 */
	public void setCharacterStream(final int parameterIndex, final Reader x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setCharacterStream(int, java.io.Reader, int)
	 */
	public void setCharacterStream(final int parameterIndex, final Reader x, final int length) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setCharacterStream(int, java.io.Reader, long)
	 */
	public void setCharacterStream(final int parameterIndex, final Reader x, final long length) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setClob(int, java.sql.Clob)
	 */
	public void setClob(final int parameterIndex, final Clob x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setClob(int, java.io.Reader)
	 */
	public void setClob(final int parameterIndex, final Reader x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setClob(int, java.io.Reader, long)
	 */
	public void setClob(final int parameterIndex, final Reader x, final long length) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setDate(int, java.sql.Date)
	 */
	public void setDate(final int parameterIndex, final Date x) throws SQLException {
		this.setObjectBase(parameterIndex, x, DorotyObjectEnum.std_date);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setDate(int, java.sql.Date, java.util.Calendar)
	 */
	public void setDate(final int parameterIndex, final Date x, final Calendar cal) throws SQLException {
		//TODO: what to do with the calendar??
		this.setObjectBase(parameterIndex, x, DorotyObjectEnum.std_date);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setDouble(int, double)
	 */
	public void setDouble(final int parameterIndex, final double x) throws SQLException {
		this.setObjectBase(parameterIndex, x, DorotyObjectEnum.std_double);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setFloat(int, float)
	 */
	public void setFloat(final int parameterIndex, final float x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setInt(int, int)
	 */
	public void setInt(final int parameterIndex, final int x) throws SQLException {
		this.setObjectBase(parameterIndex, x, DorotyObjectEnum.std_int);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setLong(int, long)
	 */
	public void setLong(final int parameterIndex, final long x) throws SQLException {
		this.setObjectBase(parameterIndex, x, DorotyObjectEnum.std_long);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setNCharacterStream(int, java.io.Reader)
	 */
	public void setNCharacterStream(final int parameterIndex, final Reader x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setNCharacterStream(int, java.io.Reader, long)
	 */
	public void setNCharacterStream(final int parameterIndex, final Reader x, final long length) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setNClob(int, java.sql.NClob)
	 */
	public void setNClob(final int parameterIndex, final NClob x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setNClob(int, java.io.Reader)
	 */
	public void setNClob(final int parameterIndex, final Reader x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setNClob(int, java.io.Reader, long)
	 */
	public void setNClob(final int parameterIndex, final Reader x, final long length) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setNString(int, java.lang.String)
	 */
	public void setNString(final int parameterIndex, final String x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setNull(int, int)
	 */
	public void setNull(final int parameterIndex, final int sqlType) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setNull(int, int, java.lang.String)
	 */
	public void setNull(final int parameterIndex, final int sqlType, final String typeName) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setObject(int, java.lang.Object)
	 */
	public void setObject(final int parameterIndex, final Object x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc} The value of targetType must match one of the DorotyCode values.
	 * 
	 * @see java.sql.PreparedStatement#setObject(int, java.lang.Object, int)
	 */
	public void setObject(final int parameterIndex, final Object x, final int targetType) throws SQLException {
		this.setObjectBase(parameterIndex, x, DorotyObjectEnum.getEnumFromCode(targetType));
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setObject(int, java.lang.Object, int, int)
	 */
	public void setObject(final int parameterIndex, final Object x, final int targetSqlType, final int scaleOrLength) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setRef(int, java.sql.Ref)
	 */
	public void setRef(final int parameterIndex, final Ref x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setRowId(int, java.sql.RowId)
	 */
	public void setRowId(final int parameterIndex, final RowId x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setSQLXML(int, java.sql.SQLXML)
	 */
	public void setSQLXML(final int parameterIndex, final SQLXML x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setShort(int, short)
	 */
	public void setShort(final int parameterIndex, final short x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setString(int, java.lang.String)
	 */
	public void setString(final int parameterIndex, final String x) throws SQLException {
		this.setObjectBase(parameterIndex, x, DorotyObjectEnum.std_string);
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setTime(int, java.sql.Time)
	 */
	public void setTime(final int parameterIndex, final Time x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setTime(int, java.sql.Time, java.util.Calendar)
	 */
	public void setTime(final int parameterIndex, final Time x, final Calendar cal) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setTimestamp(int, java.sql.Timestamp)
	 */
	public void setTimestamp(final int parameterIndex, final Timestamp x) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setTimestamp(int, java.sql.Timestamp, java.util.Calendar)
	 */
	public void setTimestamp(final int parameterIndex, final Timestamp x, final Calendar cal) throws SQLException {
		throw new SQLFeatureNotSupportedException();
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setURL(int, java.net.URL)
	 */
	public void setURL(final int parameterIndex, final URL x) throws SQLException {
		this.setObjectBase(parameterIndex, x, DorotyObjectEnum.std_url);

	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.PreparedStatement#setUnicodeStream(int, java.io.InputStream, int)
	 */
	public void setUnicodeStream(final int parameterIndex, final InputStream x, final int length) throws SQLException {
		throw new SQLFeatureNotSupportedException();

	}

	/**
	 * {@inheritDoc}
	 * 
	 * @see java.sql.Statement#close()
	 */
	@Override
	public void close() throws SQLException {
		super.close();
		this.parameters = null;
		this.parameterMetaData = null;
	}

	//**********************************Utility*****************************************************//

	/**
	 * Sets the info for the parameter at the given position of this PreparedStatement.
	 * 
	 * @param parameterIndex
	 *            index of the parameter
	 * @param x
	 *            value of the parameter
	 * @param dorotyType
	 *            type of the parameter
	 * @throws SQLException
	 *             if the given type does not match the type of x or if it is not supported.
	 */
	void setObjectBase(final int parameterIndex, final Object x, final DorotyObjectEnum dorotyType) throws SQLException {
		ParamInfo paramInfo = new ParamInfo();
		paramInfo.setType(dorotyType);
		try {
			switch (dorotyType) {
				case d_boolean:
					BooleanBasicValue boolValue = (BooleanBasicValue) x;
					paramInfo.setValue(boolValue);
					break;
				case std_boolean:
					boolean b = ((Boolean) x).booleanValue();
					paramInfo.setValue(b);
					break;
				case d_date:
					DateBasicValue dateValue = (DateBasicValue) x;
					paramInfo.setValue(dateValue);
					break;
				case std_date:
					Date date = (Date) x;
					paramInfo.setValue(date);
					break;
				case d_int:
					IntBasicValue intValue = (IntBasicValue) x;
					paramInfo.setValue(intValue);
					break;
				case std_int:
					int v = ((Integer) x).intValue();
					paramInfo.setValue(v);
					break;
				case d_string:
					StringBasicValue strValue = (StringBasicValue) x;
					paramInfo.setValue(strValue);
					break;
				case std_string:
					String str = (String) x;
					paramInfo.setValue(str);
					break;
				case std_double:
					double d = ((Double) x).doubleValue();
					paramInfo.setValue(d);
					break;
				case std_long:
					long l = ((Long) x).longValue();
					paramInfo.setValue(l);
					break;
				case std_url:
					URL url = (URL) x;
					paramInfo.setValue(url);
					break;
				case atom:
					Atom atom = (Atom) x;
					paramInfo.setValue(atom);
					break;
				case object:
					LLDigitalObject obj = (LLDigitalObject) x;
					paramInfo.setValue(obj);
					break;
				case relation:
					Relation rel = (Relation) x;
					paramInfo.setValue(rel);
					break;
				case structure:
					Structure s = (Structure) x;
					paramInfo.setValue(s);
					break;
				default:
					throw new SQLFeatureNotSupportedException("Unknwon Type");
			}
			this.setParameter(parameterIndex, paramInfo);
		} catch (ClassCastException cce) {
			throw new SQLException("Type mismatch for parameter at index " + parameterIndex);
		}

	}

	/**
	 * Sets the Object x as the parameter at the parameterIndex index in the parameters array.
	 * 
	 * @param parameterIndex
	 *            index of the parameter to set. Allowed values go from 1 to this.parameters.length.
	 * @param x
	 *            the parameter value
	 * @throws SQLException
	 *             if the connection is closed or the index is not in the range [1,parameters.length]
	 */
	private void setParameter(final int parameterIndex, final ParamInfo x) throws SQLException {
		if (this.isClosed() || !this.validIndexForParameters(parameterIndex)) {
			throw new SQLException();
		}
		this.parameters[parameterIndex - 1] = x;
	}

	/**
	 * Checks if every parameter has been set.
	 * 
	 * @return true if all parameters are set, false otherwise.
	 */
	private boolean allParametersSet() {
		for (ParamInfo p : this.parameters) {
			if (p == null)
				return false;
		}
		return true;
	}

	/**
	 * Checks if the given index is in the range of the valid parameters indexes [1,parameters.length].
	 * 
	 * @param index
	 *            int value
	 * @return true if index belongs to the range [1,parameters.length].
	 */
	private boolean validIndexForParameters(final int index) {
		return (index >= 1 && index <= this.parameters.length);
	}

	/**
	 * Counts the number of the parameter of the dol string.
	 * 
	 * @return the number of '?' char in this.dolString
	 */
	private int countNumberOfParameters() {
		char[] sqlChars = this.getDolString().toCharArray();
		int numParams = 0;
		for (char c : sqlChars) {
			if (c == '?')
				numParams++;
		}
		return numParams;
	}

	//*************************************************************************************************//
}
