package com.planet_ink.fakedb;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import com.planet_ink.fakedb.Backend.ComparableValue;
import com.planet_ink.fakedb.Backend.FakeTable;

/*
   Copyright 2001 Thomas Neumann
   Copyright 2004-2020 Bo Zimmerman

   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.
 */
public class Statement implements java.sql.Statement
{
	protected ResultSet	myResultSet						= null;
	protected boolean	closeStatementOnResultSetClose	= false;

	static protected void log(final String x)
	{
		System.err.println("Statement: " + x);
	}

	protected Connection	connection;
	public String			lastSQL	= "null";

	Statement(final Connection c)
	{
		connection = c;
	}

	@Override
	public java.sql.Connection getConnection()
	{
		return connection;
	}

	public Connection getFakeConnection()
	{
		return connection;
	}

	protected String split(String sql, final String[] token)
	{
		while (true)
		{
			if (sql.length() == 0)
			{
				token[0] = "";
				return "";
			}
			if (sql.charAt(0) == ' ')
			{
				sql = sql.substring(1);
				continue;
			}
			int index;
			for (index = 0; index < sql.length(); index++)
			{
				char c = sql.charAt(index);
				if (c == ' ')
				{
					break;
				}
				else
				if (c == '\'')
				{
					for (++index; index < sql.length(); index++)
					{
						c = sql.charAt(index);
						if (c == '\\')
							index++;
						else
						if (c == '\'')
							break;
					}
				}
			}
			if (index >= sql.length())
			{
				token[0] = sql;
				return "";
			}
			token[0] = sql.substring(0, index);
			return sql.substring(index + 1);
		}
	}

	protected String splitColumns(final String sql, final List<String> cols)
	{
		int s = 0;
		while ((sql.length() > 0) && (s < sql.length()))
		{
			if ((s < sql.length()) && ((sql.charAt(s) == ' ') || (sql.charAt(s) == '\t')))
				s++;
			if (s >= sql.length())
				return "";
			int e = s;
			while ((e < sql.length()) && (sql.charAt(e) != ' ') && (sql.charAt(e) != '\t') && (sql.charAt(e) != ','))
				e++;
			if (e >= sql.length()) // was whatever it was the last word.. done
				return sql.substring(s);
			final String word = sql.substring(s, e);
			cols.add(word);
			if (sql.charAt(e) != ',')
			{
				while ((e < sql.length()) && ((sql.charAt(e) == ' ') || (sql.charAt(e) == '\t')))
					e++;
			}
			if ((e >= sql.length()) || (sql.charAt(e) != ','))
				return sql.substring(e);
			while (sql.charAt(e) == ',')
				e++;
			s = e;
		}
		return "";
	}

	@Override
	public boolean isClosed() throws SQLException
	{
		return connection.isClosed();
	}

	@Override
	public void setPoolable(final boolean isPoolable)
	{
	}

	@Override
	public boolean isPoolable() throws SQLException
	{
		return false;
	}

	@Override
	public boolean isWrapperFor(final Class<?> arg0) throws SQLException
	{
		return false;
	}

	@Override
	public <T> T unwrap(final Class<T> arg0) throws SQLException
	{
		return null;
	}

	public String parseWhereClause(final String tableName, final String sql, List<Backend.FakeCondition> conditions) throws java.sql.SQLException
	{
		int s = 0;
		final String eow1 = " \t!=><";
		final java.util.Stack<List<Backend.FakeCondition>> parenStack = new java.util.Stack<List<Backend.FakeCondition>>();
		while (s < sql.length())
		{
			while ((s < sql.length()) && (sql.charAt(s) == ' ' || sql.charAt(s) == '\t'))
				s++;
			if (s >= sql.length())
				break;
			Backend.FakeCondition condition = null;
			if (sql.charAt(s) == '(')
			{
				condition = connection.getBackend().buildFakeCondition(tableName, null, null, null, false);
				conditions.add(condition);
				parenStack.push(conditions);
				condition.contains = new ArrayList<Backend.FakeCondition>();
				conditions = condition.contains;
				s++;
				continue;
			}
			else
			if (sql.charAt(s) == ')')
			{
				if (parenStack.size() == 0)
					throw new java.sql.SQLException("Unexpected end parenthesis " + sql);
				conditions = parenStack.pop();
				condition = conditions.get(conditions.size() - 1);
				s++;
			}
			else
			{
				int e = s;
				boolean unPrepared = false;
				while ((e < sql.length()) && (eow1.indexOf(sql.charAt(e)) < 0))
					e++;
				final String columnName = sql.substring(s, e);
				if (e >= sql.length())
					throw new java.sql.SQLException("Unexpected end of where clause in " + sql);
				s = e;
				while ((s < sql.length()) && (sql.charAt(s) == ' ' || sql.charAt(s) == '\t'))
					s++;
				e = s;
				String comparitor;
				if ((e < sql.length() - 5)
				&& (Character.toLowerCase(sql.charAt(e)) == 'l')
				&& (Character.toLowerCase(sql.charAt(e + 1)) == 'i')
				&& (Character.toLowerCase(sql.charAt(e + 2)) == 'k')
				&& (Character.toLowerCase(sql.charAt(e + 3)) == 'e')
				&& (Character.toLowerCase(sql.charAt(e + 4)) == ' '))
				{
					comparitor = "like";
					e += 5;
				}
				else
				if ((e < sql.length()) && (eow1.indexOf(sql.charAt(e)) > 0))
				{
					while ((e < sql.length()) && (eow1.indexOf(sql.charAt(e)) > 0))
						e++;
					comparitor = sql.substring(s, e).trim();
				}
				else
					throw new java.sql.SQLException("Illegal comparator " + sql);
				if (e >= sql.length() || comparitor.length() == 0)
					throw new java.sql.SQLException("Unexpected end of where clause in " + sql);
				s = e;
				while ((sql.charAt(s) == ' ' || sql.charAt(s) == '\t') && (s < sql.length()))
					s++;
				if (s >= sql.length())
					throw new java.sql.SQLException("Unexpected end of where clause in " + sql);
				String value;
				e = s;
				if (sql.charAt(s) == '\'')
				{
					e++;
					final StringBuilder str = new StringBuilder("");
					while ((e < sql.length()) && (sql.charAt(e) != '\''))
					{
						if (sql.charAt(e) == '\\')
							e++;
						if (e < sql.length())
							str.append(sql.charAt(e));
						e++;
					}
					if (e >= sql.length())
						throw new java.sql.SQLException("Unexpected end of where clause in " + sql);
					e++;
					value = str.toString();
				}
				else
				{
					while ((e < sql.length()) && (sql.charAt(e) != ' ') && (sql.charAt(e) != '\t'))
						e++;
					value = sql.substring(s, e);
					if (value.equalsIgnoreCase("?"))
						unPrepared = true;
				}
				s = e;
				condition = connection.getBackend().buildFakeCondition(tableName, columnName, comparitor, value, unPrepared);
				conditions.add(condition);
			}
			while ((s < sql.length()) && (sql.charAt(s) == ' ' || sql.charAt(s) == '\t'))
				s++;
			if (s >= sql.length())
				break;
			int e = s;
			while ((e < sql.length()) && (sql.charAt(e) != ' ') && (sql.charAt(e) != '\t'))
				e++;
			if (condition == null)
				continue;
			final String peeker = sql.substring(s, e);
			if (peeker.equalsIgnoreCase(")"))
			{

			}
			else
			if (peeker.equalsIgnoreCase("AND"))
			{
				s = e;
				condition.connector = Backend.ConnectorType.AND;
			}
			else
			if (peeker.equalsIgnoreCase("OR"))
			{
				s = e;
				condition.connector = Backend.ConnectorType.OR;
			}
			else
				break;
		}
		if (parenStack.size() > 0)
			throw new java.sql.SQLException("Unended parenthesis " + sql);
		if (s >= sql.length())
			return "";
		return sql.substring(s);

	}

	public Backend.ImplSelectStatement parseSelect(String sql, final String[] token) throws java.sql.SQLException
	{
		final List<String> cols = new ArrayList<String>();
		sql = splitColumns(sql, cols);
		if (cols.size() == 0)
			throw new java.sql.SQLException("no columns given");
		sql = split(sql, token);
		if (!token[0].equalsIgnoreCase("from"))
			throw new java.sql.SQLException("no from clause");
		sql = split(sql, token);
		final String tableName = token[0];
		final List<Backend.FakeCondition> conditions = new ArrayList<Backend.FakeCondition>();
		String[] orderVars = null;
		String[] orderConditions = null;
		if (sql.length() > 0)
		{
			sql = split(sql, token);
			if (token[0].equalsIgnoreCase("where"))
			{
				sql = parseWhereClause(tableName, sql, conditions);
				if (conditions.size() == 0)
					throw new java.sql.SQLException("no more where clause!");
				sql = split(sql, token);
			}
			if ((token[0] != null) && (token[0].equalsIgnoreCase("order")))
			{
				sql = split(sql, token);
				if (!token[0].equalsIgnoreCase("by"))
					throw new java.sql.SQLException("no by token");
				sql = split(sql, token);
				orderVars = new String[] { token[0] };
				orderConditions = new String[1];
				if (sql.length() > 0)
				{
					split(sql, token);
					if (token[0].equalsIgnoreCase("ASC") || token[0].equalsIgnoreCase("DESC"))
					{
						orderConditions = new String[] { token[0].toUpperCase().trim() };
						sql = split(sql, token);
					}
				}
			}
			if (sql.length() > 0)
				throw new java.sql.SQLException("extra garbage: " + sql);
		}
		return new Backend.ImplSelectStatement(this, tableName, cols, conditions, orderVars, orderConditions);
	}

	@Override
	public java.sql.ResultSet executeQuery(String sql) throws java.sql.SQLException
	{
		lastSQL = sql;
		try
		{
			final String[] token = new String[1];
			sql = split(sql, token);
			if (!token[0].equalsIgnoreCase("select"))
				throw new java.sql.SQLException("first query token not select");
			final Backend.ImplSelectStatement stmt = parseSelect(sql, token);
			return connection.getBackend().constructScan(stmt);
		}
		catch (final java.sql.SQLException e)
		{
			log("unsupported SQL in executeQuery: " + sql);
			throw e;
		}
	}

	protected static String skipWS(final String sql)
	{
		int index;
		for (index = 0; index < sql.length(); index++)
		{
			final char c = sql.charAt(index);
			if ((c != ' ') && (c != '\t') && (c != '\r') && (c != '\n'))
				break;
		}
		if (index == 0)
			return sql;
		return sql.substring(index);
	}

	protected static String[] parseVal(String sql)
	{
		final String[] result = new String[3];
		sql = skipWS(sql);
		if (sql.length() == 0)
		{
			result[0] = result[1] = "";
			result[2] = null;
		}
		else
		if (sql.charAt(0) == '\'')
		{
			final StringBuffer buffer = new StringBuffer();
			int index;
			for (index = 1; index < sql.length(); ++index)
			{
				char c = sql.charAt(index);
				if (c == '\'')
					break;
				if (c == '\\')
					c = sql.charAt(++index);
				buffer.append(c);
			}
			if (index >= sql.length())
				index = sql.length() - 1;
			result[0] = sql.substring(index + 1);
			result[1] = buffer.toString();
			result[2] = null;
		}
		else
		{
			final StringBuffer buffer = new StringBuffer();
			int index;
			for (index = 0; index < sql.length(); ++index)
			{
				final char c = sql.charAt(index);
				if ((c == ' ') || (c == ',') || (c == ')'))
					break;
				buffer.append(c);
			}
			result[0] = sql.substring(index);
			result[1] = buffer.toString();
			result[2] = buffer.toString().equals("?") ? "" : null;
		}
		return result;
	}

	protected Backend.ImplInsertStatement parseInsert(String sql, final String[] token) throws java.sql.SQLException
	{
		sql = split(sql, token);
		if (!token[0].equalsIgnoreCase("into"))
			throw new java.sql.SQLException("no into token");
		sql = split(sql, token);
		final String tableName = token[0];
		sql = skipWS(sql);
		if ((sql.length() < 0) || (sql.charAt(0) != '('))
			throw new java.sql.SQLException("no open paren");
		sql = sql.substring(1);

		final java.util.List<String> columnList = new java.util.LinkedList<String>();
		while (true)
		{
			sql = skipWS(sql);
			int index = sql.indexOf(',');
			final int index2 = sql.indexOf(')');
			if ((index < 0) || (index2 < index))
				index = index2;
			if (index < 0)
				throw new java.sql.SQLException("no comma");
			columnList.add(sql.substring(0, index).trim());
			final char c = sql.charAt(index);
			sql = skipWS(sql.substring(index + 1));
			if (c == ')')
				break;
		}

		sql = split(sql, token);
		if (!token[0].equalsIgnoreCase("values"))
			throw new java.sql.SQLException("no values");
		sql = skipWS(sql);
		if ((sql.length() < 0) || (sql.charAt(0) != '('))
			throw new java.sql.SQLException("no value open paren");
		sql = sql.substring(1);

		final java.util.List<String> valuesList = new java.util.LinkedList<String>();
		final java.util.List<Boolean> unPreparedValueList = new java.util.LinkedList<Boolean>();
		while (true)
		{
			sql = skipWS(sql);
			final String[] r = parseVal(sql);
			final String val = r[1];
			sql = skipWS(r[0]);
			valuesList.add(val);
			unPreparedValueList.add(Boolean.valueOf(r[2] != null));
			if (sql.length() == 0)
				throw new java.sql.SQLException("no sql again");
			final char c = sql.charAt(0);
			sql = skipWS(sql.substring(1));
			if (c == ')')
				break;
			if (c != ',')
				throw new java.sql.SQLException("no comma before last paren");
		}
		if ((sql.length() > 0) && (sql.charAt(0) == ';'))
			sql = skipWS(sql.substring(1));
		if ((sql.length() > 0) || (columnList.size() != valuesList.size()))
		{
			throw new java.sql.SQLException("something very bad");
		}
		return new Backend.ImplInsertStatement(tableName, columnList.toArray(new String[0]), valuesList.toArray(new String[0]), unPreparedValueList.toArray(new Boolean[0]));
	}

	protected Backend.ImplUpdateStatement parseUpdate(String sql, final String[] token) throws java.sql.SQLException
	{
		sql = split(sql, token);
		final String tableName = token[0];
		sql = split(sql, token);
		if (!token[0].equalsIgnoreCase("set"))
			throw new java.sql.SQLException("no set");

		final java.util.List<String> columnList = new java.util.LinkedList<String>();
		final java.util.List<String> valueList = new java.util.LinkedList<String>();
		final java.util.List<Boolean> unPreparedValueList = new java.util.LinkedList<Boolean>();
		final StringBuffer buffer = new StringBuffer();
		while (true)
		{
			sql = skipWS(sql);
			buffer.setLength(0);
			while (sql.length() > 0)
			{
				final char c = sql.charAt(0);
				if ((c == '=') || (c == ' '))
					break;
				buffer.append(c);
				sql = sql.substring(1);
			}
			sql = skipWS(sql);
			final String attr = buffer.toString();
			if (sql.length() == 0)
				throw new java.sql.SQLException("no more sql");
			if (sql.charAt(0) != '=')
			{
				if (!attr.equalsIgnoreCase("where"))
					throw new java.sql.SQLException("no where");
				break;
			}
			sql = skipWS(sql.substring(1));
			if (sql.length() == 0)
				throw new java.sql.SQLException("no no sql no mo");
			buffer.setLength(0);
			if (sql.charAt(0) == '\'')
			{
				int sub = 1;
				for (; sub < sql.length(); sub++)
				{
					char c = sql.charAt(sub);
					if (c == '\'')
						break;
					if (c == '\\')
						c = sql.charAt(++sub);
					buffer.append(c);
				}
				columnList.add(attr);
				valueList.add(buffer.toString());
				sql = sql.substring(sub + 1);
				unPreparedValueList.add(Boolean.valueOf(false));
			}
			else
			{
				final String[] r = parseVal(sql);
				sql = r[0];
				columnList.add(attr);
				valueList.add(r[1]);
				unPreparedValueList.add(Boolean.valueOf(r[2] != null));
			}
			sql = skipWS(sql);
			if ((sql.length() > 0) && (sql.charAt(0) == ','))
				sql = skipWS(sql.substring(1));
		}
		final List<Backend.FakeCondition> conditions = new ArrayList<Backend.FakeCondition>();
		sql = parseWhereClause(tableName, sql, conditions);
		if (conditions.size() == 0)
			throw new java.sql.SQLException("no more where clause!");
		return new Backend.ImplUpdateStatement(tableName, conditions, columnList.toArray(new String[0]), valueList.toArray(new String[0]), unPreparedValueList.toArray(new Boolean[0]));

	}

	protected Backend.ImplDeleteStatement parseDelete(String sql, final String[] token) throws java.sql.SQLException
	{
		sql = split(sql, token);
		if (!token[0].equalsIgnoreCase("from"))
			throw new java.sql.SQLException("no from clause");
		sql = split(sql, token);
		final String tableName = token[0];
		sql = split(sql, token);
		List<Backend.FakeCondition> conditions;
		if (token[0].equalsIgnoreCase("where"))
		{
			sql = skipWS(sql);
			conditions = new ArrayList<Backend.FakeCondition>();
			sql = parseWhereClause(tableName, sql, conditions);
			if (conditions.size() == 0)
				throw new java.sql.SQLException("no more where clause!");
		}
		else
		if (token.length > 0)
		{
			conditions = new ArrayList<Backend.FakeCondition>();
		}
		else
			throw new java.sql.SQLException("no other where clause");
		return new Backend.ImplDeleteStatement(tableName, conditions);
	}

	@Override
	public int executeUpdate(String sql) throws java.sql.SQLException
	{
		lastSQL = sql;
		// log("executeUpdate"+sql);

		// insert into x (a,b,c) values (a,b,c)
		// update x set a=A,b=B where x=y
		// delete from x where x=y

		final String originalSql = sql;
		try
		{
			final String[] token = new String[1];
			sql = split(sql, token);
			if (token[0].equalsIgnoreCase("insert"))
			{
				final Backend.ImplInsertStatement stmt = parseInsert(sql, token);
				connection.getBackend().dupKeyCheck(stmt.tableName, stmt.columns, stmt.sqlValues);
				connection.getBackend().insertValues(stmt);
			}
			else
			if (token[0].equalsIgnoreCase("update"))
			{
				final Backend.ImplUpdateStatement stmt = parseUpdate(sql, token);
				connection.getBackend().updateRecord(stmt);
			}
			else
			if (token[0].equalsIgnoreCase("delete"))
			{
				final Backend.ImplDeleteStatement stmt = parseDelete(sql, token);
				connection.getBackend().deleteRecord(stmt);
			}
			else
				throw new java.sql.SQLException("unimplemented command: " + token[0]);
			return 1;
		}
		catch (final java.sql.SQLException e)
		{
			e.printStackTrace();
			log("unsupported SQL in executeUpdate: " + originalSql);
			throw e;
		}
	}

	@Override
	public int executeUpdate(final String sql, final int a) throws java.sql.SQLException
	{
		return executeUpdate(sql);
	}

	@Override
	public int executeUpdate(final String sql, final int[] a) throws java.sql.SQLException
	{
		return executeUpdate(sql);
	}

	@Override
	public int executeUpdate(final String sql, final String[] a) throws java.sql.SQLException
	{
		return executeUpdate(sql);
	}

	@Override
	public void close() throws java.sql.SQLException
	{

	}

	@Override
	public int getMaxFieldSize() throws java.sql.SQLException
	{
		return 0;
	}

	@Override
	public void setMaxFieldSize(final int max) throws java.sql.SQLException
	{
	}

	@Override
	public int getMaxRows() throws java.sql.SQLException
	{
		return 0;
	}

	@Override
	public void setMaxRows(final int max) throws java.sql.SQLException
	{
	}

	@Override
	public void setEscapeProcessing(final boolean enable) throws java.sql.SQLException
	{
	}

	@Override
	public int getQueryTimeout() throws java.sql.SQLException
	{
		return 60;
	}

	@Override
	public void setQueryTimeout(final int seconds) throws java.sql.SQLException
	{
	}

	@Override
	public void cancel() throws java.sql.SQLException
	{
	}

	@Override
	public java.sql.SQLWarning getWarnings() throws java.sql.SQLException
	{
		return null;
	}

	@Override
	public void clearWarnings() throws java.sql.SQLException
	{
	}

	@Override
	public void setCursorName(final String Name) throws java.sql.SQLException
	{
	}

	@Override
	public boolean execute(String sql) throws java.sql.SQLException
	{
		lastSQL = sql;
		try
		{
			final String[] token = new String[1];
			sql = split(sql, token);
			if (!token[0].equalsIgnoreCase("select"))
			{
				return executeUpdate(lastSQL) == 0;
			}
			final Backend.ImplSelectStatement stmt = parseSelect(sql, token);
			myResultSet = (ResultSet) connection.getBackend().constructScan(stmt);
			return true;
		}
		catch (final java.sql.SQLException e)
		{
			log("unsupported SQL in executeQuery: " + sql);
			throw e;
		}
	}

	@Override
	public boolean execute(final String sql, final int a) throws java.sql.SQLException
	{
		return execute(sql, 0);
	}

	@Override
	public boolean execute(final String sql, final int[] a) throws java.sql.SQLException
	{
		return execute(sql, 0);
	}

	@Override
	public boolean execute(final String sql, final String[] a) throws java.sql.SQLException
	{
		return execute(sql, 0);
	}

	@Override
	public java.sql.ResultSet getResultSet() throws java.sql.SQLException
	{
		return myResultSet;
	}

	@Override
	public int getUpdateCount() throws java.sql.SQLException
	{
		log("getUpdateCount");
		return -1;
	}

	public long getLongUpdateCount()
	{
		log("getLongUpdateCount");
		return -1;
	}

	@Override
	public boolean getMoreResults() throws java.sql.SQLException
	{
		return false;
	}

	@Override
	public boolean getMoreResults(final int a) throws java.sql.SQLException
	{
		return false;
	}

	@Override
	public int getResultSetHoldability() throws java.sql.SQLException
	{
		return 0;
	}

	@Override
	public void setFetchDirection(final int i) throws java.sql.SQLException
	{
	}

	@Override
	public int getFetchDirection() throws java.sql.SQLException
	{
		return 0;
	}

	@Override
	public void addBatch(final String a) throws java.sql.SQLException
	{
	}

	@Override
	public void clearBatch() throws java.sql.SQLException
	{
	}

	@Override
	public int[] executeBatch() throws java.sql.SQLException
	{
		return null;
	}

	@Override
	public void setFetchSize(final int i) throws java.sql.SQLException
	{
	}

	@Override
	public int getFetchSize() throws java.sql.SQLException
	{
		return 0;
	}

	@Override
	public int getResultSetConcurrency() throws java.sql.SQLException
	{
		return 0;
	}

	@Override
	public int getResultSetType() throws java.sql.SQLException
	{
		return 0;
	}

	@Override
	public java.sql.ResultSet getGeneratedKeys() throws java.sql.SQLException
	{
		return null;
	}

	public void closeOnCompletion() throws SQLException
	{
		closeStatementOnResultSetClose = true;
	}

	public boolean isCloseOnCompletion() throws SQLException
	{
		return closeStatementOnResultSetClose;
	}
}
