/*******************************************************************************
 * Copyright 2015, 2017 Francesco Benincasa (info@abubusoft.com).
 *
 * 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 com.abubusoft.kripton.processor.sqlite.grammars.jql;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.TokenStreamRewriter;
import org.antlr.v4.runtime.tree.ParseTreeWalker;

import com.abubusoft.kripton.common.One;
import com.abubusoft.kripton.common.Pair;
import com.abubusoft.kripton.common.StringUtils;
import com.abubusoft.kripton.common.Triple;
import com.abubusoft.kripton.processor.core.AssertKripton;
import com.abubusoft.kripton.processor.core.Finder;
import com.abubusoft.kripton.processor.sqlite.grammars.jql.JQL.JQLDynamicStatementType;
import com.abubusoft.kripton.processor.sqlite.grammars.jql.JQLPlaceHolder.JQLPlaceHolderType;
import com.abubusoft.kripton.processor.sqlite.grammars.jql.JQLProjection.ProjectionBuilder;
import com.abubusoft.kripton.processor.sqlite.grammars.jql.JQLProjection.ProjectionType;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlBaseListener;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlLexer;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Bind_dynamic_sqlContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Bind_parameterContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Column_aliasContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Column_fully_qualified_nameContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Column_nameContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Column_name_setContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Column_name_to_updateContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Column_value_setContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Columns_to_updateContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Group_stmtContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Having_stmtContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Limit_stmtContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Offset_stmtContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Order_stmtContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Projected_columnsContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Result_columnContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Select_coreContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Select_or_valuesContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Table_nameContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Where_stmtContext;
import com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlParser.Where_stmt_in_clauseContext;
import com.abubusoft.kripton.processor.sqlite.model.SQLProperty;

/**
 * The Class JQLChecker.
 */
public class JQLChecker {

	/** The instance. */
	protected static JQLChecker instance;

	/**
	 * Gets the single instance of JQLChecker.
	 *
	 * @return single instance of JQLChecker
	 */
	public static final JQLChecker getInstance() {
		if (instance == null) {
			instance = new JQLChecker();
		}

		return instance;
	}

	/** The walker. */
	ParseTreeWalker walker = new ParseTreeWalker();

	/**
	 * Instantiates a new JQL checker.
	 */
	private JQLChecker() {

	}

	/**
	 * Analyze internal.
	 *
	 * @param <L>
	 *            the generic type
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @param listener
	 *            the listener
	 */
	protected <L extends JqlBaseListener> void analyzeInternal(JQLContext jqlContext, final String jql, L listener) {
		walker.walk(listener, prepareParser(jqlContext, jql).value0);
	}

	/**
	 * Analyze variable statement internal.
	 *
	 * @param <L>
	 *            the generic type
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @param listener
	 *            the listener
	 */
	protected <L extends JqlBaseListener> void analyzeVariableStatementInternal(JQLContext jqlContext, final String jql,
			L listener) {
		walker.walk(listener, prepareVariableStatement(jqlContext, jql).value0);
	}

	/**
	 * Analyze.
	 *
	 * @param <L>
	 *            the generic type
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @param listener
	 *            the listener
	 */
	public <L extends JqlBaseListener> void analyze(final JQLContext jqlContext, final JQL jql, L listener) {
		analyzeInternal(jqlContext, jql.value, listener);
	}

	/**
	 * Prepare parser.
	 *
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @return the pair
	 */
	protected Pair<ParserRuleContext, CommonTokenStream> prepareParser(final JQLContext jqlContext, final String jql) {
		JqlLexer lexer = new JqlLexer(CharStreams.fromString(jql));
		CommonTokenStream tokens = new CommonTokenStream(lexer);
		JqlParser parser = new JqlParser(tokens);

		parser.removeErrorListeners();
		parser.addErrorListener(new JQLBaseErrorListener() {
			@Override
			public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line,
					int charPositionInLine, String msg, RecognitionException e) {
				AssertKripton.assertTrue(false,
						jqlContext.getContextDescription() + ": unespected char at pos %s of SQL '%s'",
						charPositionInLine, jql);
			}
		});

		ParserRuleContext context = parser.parse();
		return new Pair<>(context, tokens);
	}

	/**
	 * <p>
	 * Parse the variable parts of a SQL:
	 * </p>
	 * 
	 * <ul>
	 * <li>where_stmt</li>
	 * <li>group_stmt</li>
	 * <li>having_stmt</li>
	 * <li>order_stmt</li>
	 * <li>limit_stmt</li>
	 * <li>offset_stmt</li>
	 * </ul>
	 * .
	 *
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @return the pair
	 */
	protected Pair<ParserRuleContext, CommonTokenStream> prepareVariableStatement(final JQLContext jqlContext,
			final String jql) {
		JqlLexer lexer = new JqlLexer(CharStreams.fromString(jql));
		CommonTokenStream tokens = new CommonTokenStream(lexer);
		JqlParser parser = new JqlParser(tokens);

		parser.removeErrorListeners();
		parser.addErrorListener(new JQLBaseErrorListener() {
			@Override
			public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line,
					int charPositionInLine, String msg, RecognitionException e) {
				AssertKripton.assertTrue(false,
						jqlContext.getContextDescription() + ": unespected char at pos %s of JQL '%s'",
						charPositionInLine, jql);
			}
		});

		ParserRuleContext context = parser.parse_variable();
		return new Pair<>(context, tokens);
	}

	/**
	 * Retrieve set of projected field.
	 *
	 * @param jqlContext
	 *            the jql context
	 * @param jqlValue
	 *            the jql value
	 * @param entity
	 *            the entity
	 * @return the sets the
	 */
	public Set<JQLProjection> extractProjections(final JQLContext jqlContext, String jqlValue,
			final Finder<SQLProperty> entity) {
		final Set<JQLProjection> result = new LinkedHashSet<JQLProjection>();

		final One<Boolean> projection = new One<Boolean>(null);

		analyzeInternal(jqlContext, jqlValue, new JqlBaseListener() {

			@Override
			public void enterProjected_columns(Projected_columnsContext ctx) {
				if (projection.value0 == null) {
					projection.value0 = true;
				}
			}

			@Override
			public void exitProjected_columns(Projected_columnsContext ctx) {
				projection.value0 = false;
			}

			@Override
			public void enterResult_column(Result_columnContext ctx) {
				if (projection.value0 != true)
					return;
				ProjectionBuilder builder = ProjectionBuilder.create();

				if (ctx.getText().endsWith("*")) {
					builder.type(ProjectionType.STAR);
				} else if (ctx.table_name() != null) {
					builder.table(ctx.expr().table_name().getText());
				} else if (ctx.expr().column_fully_qualified_name() != null
						&& ctx.expr().column_fully_qualified_name().column_simple_name() != null) {
					Finder<SQLProperty> currentEntity = entity;
					if (ctx.expr().column_fully_qualified_name().table_simple_name() != null) {
						String entityName = ctx.expr().column_fully_qualified_name().table_simple_name().getText();
						builder.table(entityName);
						currentEntity = jqlContext.findEntityByName(entityName);
					}

					String jqlColumnName = ctx.expr().column_fully_qualified_name().column_simple_name().getText();
					builder.column(jqlColumnName);

					SQLProperty property = currentEntity.findPropertyByName(jqlColumnName);
					AssertKripton.assertTrueOrUnknownPropertyInJQLException(property != null, jqlContext,
							jqlColumnName);

					builder.property(property);
					builder.type(ProjectionType.COLUMN);

					if (ctx.column_alias() != null) {
						String columnAlias = ctx.column_alias().getText();
						builder.alias(columnAlias);

						SQLProperty property1 = entity.findPropertyByName(columnAlias);
						AssertKripton.assertTrueOrUnknownPropertyInJQLException(property1 != null, jqlContext,
								columnAlias);

						builder.property(property1);
					}
				} else {
					builder.type(ProjectionType.COMPLEX);
					builder.expression(ctx.expr().getText());

					if (ctx.column_alias() != null) {
						String columnAlias = ctx.column_alias().getText();
						builder.alias(columnAlias);

						SQLProperty property1 = entity.findPropertyByName(columnAlias);
						AssertKripton.assertTrueOrUnknownPropertyInJQLException(property1 != null, jqlContext,
								columnAlias);

						builder.property(property1);
					}
				}

				result.add(builder.build());

			}

			@Override
			public void exitResult_column(Result_columnContext ctx) {
			}
		});

		if (result.size() == 1 && result.toArray(new JQLProjection[1])[0].type == ProjectionType.STAR) {
			// the projected columns are full
			result.clear();

			if (entity != null) {

				for (SQLProperty item : entity.getCollection()) {
					JQLProjection col = new JQLProjection(ProjectionType.COLUMN, entity.getSimpleName(), item.getName(),
							null, null, item);
					result.add(col);
				}
			}
		}

		return result;
	}

	/**
	 * Extract columns to insert or update.
	 *
	 * @param jqlContext
	 *            the jql context
	 * @param jqlValue
	 *            the jql value
	 * @param entity
	 *            the entity
	 * @return the sets the
	 */
	public Set<String> extractColumnsToInsertOrUpdate(final JQLContext jqlContext, String jqlValue,
			final Finder<SQLProperty> entity) {
		final Set<String> result = new LinkedHashSet<String>();

		final One<Boolean> selectionOn = new One<Boolean>(null);
		final One<Boolean> insertOn = new One<Boolean>(null);

		// Column_name_set is needed for insert
		// Columns_to_update is needed for update
		analyzeInternal(jqlContext, jqlValue, new JqlBaseListener() {

			@Override
			public void enterColumn_name_set(Column_name_setContext ctx) {
				if (insertOn.value0 == null) {
					insertOn.value0 = true;
				}
			}

			@Override
			public void exitColumn_name_set(Column_name_setContext ctx) {
				insertOn.value0 = false;
			}

			@Override
			public void enterColumns_to_update(Columns_to_updateContext ctx) {
				if (selectionOn.value0 == null) {
					selectionOn.value0 = true;
				}
			}

			@Override
			public void exitColumns_to_update(Columns_to_updateContext ctx) {
				selectionOn.value0 = false;
			}

			@Override
			public void enterColumn_name(Column_nameContext ctx) {
				// works for INSERTS
				if (insertOn.value0 != null && insertOn.value0 == true) {
					result.add(ctx.getText());
				}
			}

			@Override
			public void enterColumn_name_to_update(Column_name_to_updateContext ctx) {
				result.add(ctx.getText());
			}

		});

		return result;
	}

	/**
	 * Replace place holder with element passed by listener.
	 *
	 * @param context
	 *            the context
	 * @param jql
	 *            the jql
	 * @param listener
	 *            the listener
	 * @return string obtained by replacements
	 */
	public String replaceFromVariableStatement(JQLContext context, String jql, final JQLReplacerListener listener) {
		JQLRewriterListener rewriterListener = new JQLRewriterListener();
		rewriterListener.init(listener);

		return replaceFromVariableStatementInternal(context, jql, replace, rewriterListener);
	}

	/** The replace. */
	List<Triple<Token, Token, String>> replace = new ArrayList<>();

	/**
	 * The listener interface for receiving JQLRewriter events. The class that is interested in processing a JQLRewriter event implements this interface, and the object created
	 * with that class is registered with a component using the component's <code>addJQLRewriterListener</code> method. When the JQLRewriter event occurs, that object's appropriate
	 * method is invoked.
	 *
	 * @author Francesco Benincasa (info@abubusoft.com)
	 */
	public class JQLRewriterListener extends JqlBaseListener {

		private boolean inStatement = false;

		/** The listener. */
		private JQLReplacerListener listener;

		/**
		 * Inits the.
		 *
		 * @param listener
		 *            the listener
		 */
		public void init(JQLReplacerListener listener) {
			this.listener = listener;
			replace.clear();
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlBaseListener# enterTable_name(com.abubusoft.kripton.processor.sqlite.grammars.jsql.
		 * JqlParser.Table_nameContext)
		 */
		@Override
		public void enterTable_name(Table_nameContext ctx) {
			String value = listener.onTableName(ctx.getText());

			// skip without replace
			if (value == null)
				return;

			replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlBaseListener# enterBind_parameter(com.abubusoft.kripton.processor.sqlite.grammars.
		 * jsql.JqlParser.Bind_parameterContext)
		 */
		@Override
		public void enterBind_parameter(Bind_parameterContext ctx) {
			String value;
			if (ctx.bind_parameter_name() != null) {
				value = listener.onBindParameter(ctx.bind_parameter_name().getText(), inStatement);
			} else {
				value = listener.onBindParameter(ctx.getText(), inStatement);
			}

			// skip without replace
			if (value == null)
				return;

			replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlBaseListener# enterColumn_name_to_update(com.abubusoft.kripton.processor.sqlite.
		 * grammars.jsql.JqlParser.Column_name_to_updateContext)
		 */
		@Override
		public void enterColumn_name_to_update(Column_name_to_updateContext ctx) {
			String value = listener.onColumnNameToUpdate(ctx.getText());

			// skip without replace
			if (value == null)
				return;

			replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlBaseListener# enterColumn_fully_qualified_name(com.abubusoft.kripton.processor.
		 * sqlite.grammars.jsql.JqlParser.Column_fully_qualified_nameContext)
		 */
		@Override
		public void enterColumn_fully_qualified_name(Column_fully_qualified_nameContext ctx) {
			String value = listener.onColumnFullyQualifiedName(
					ctx.table_simple_name() != null ? ctx.table_simple_name().getText() : "",
					ctx.column_simple_name().getText());

			// skip without replace
			if (value == null)
				return;

			replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlBaseListener# enterColumn_name(com.abubusoft.kripton.processor.sqlite.grammars.jsql
		 * .JqlParser.Column_nameContext)
		 */
		@Override
		public void enterColumn_name(Column_nameContext ctx) {
			String value = listener.onColumnName(ctx.getText());

			// skip without replace
			if (value == null)
				return;

			replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
		}

		@Override
		public void enterColumn_alias(Column_aliasContext ctx) {
			String value = listener.onColumnAlias(ctx.getText());

			// skip without replace
			if (value == null)
				return;

			replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlBaseListener# enterBind_dynamic_sql(com.abubusoft.kripton.processor.sqlite.grammars
		 * .jsql.JqlParser.Bind_dynamic_sqlContext)
		 */
		@Override
		public void enterBind_dynamic_sql(Bind_dynamic_sqlContext ctx) {
			String value = listener.onDynamicSQL(JQLDynamicStatementType.valueOf(ctx.bind_parameter_name().getText()));

			// skip without replace
			if (value == null)
				return;

			replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlBaseListener# enterWhere_stmt(com.abubusoft.kripton.processor.sqlite.grammars.jsql.
		 * JqlParser.Where_stmtContext)
		 */
		@Override
		public void enterWhere_stmt(Where_stmtContext ctx) {
			listener.onWhereStatementBegin(ctx);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlBaseListener# exitWhere_stmt(com.abubusoft.kripton.processor.sqlite.grammars.jsql.
		 * JqlParser.Where_stmtContext)
		 */
		@Override
		public void exitWhere_stmt(Where_stmtContext ctx) {
			listener.onWhereStatementEnd(ctx);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlBaseListener# enterColumn_name_set(com.abubusoft.kripton.processor.sqlite.grammars.
		 * jsql.JqlParser.Column_name_setContext)
		 */
		@Override
		public void enterColumn_name_set(Column_name_setContext ctx) {
			listener.onColumnNameSetBegin(ctx);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlBaseListener# exitColumn_name_set(com.abubusoft.kripton.processor.sqlite.grammars.
		 * jsql.JqlParser.Column_name_setContext)
		 */
		@Override
		public void exitColumn_name_set(Column_name_setContext ctx) {
			listener.onColumnNameSetEnd(ctx);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlBaseListener# enterColumn_value_set(com.abubusoft.kripton.processor.sqlite.grammars
		 * .jsql.JqlParser.Column_value_setContext)
		 */
		@Override
		public void enterColumn_value_set(Column_value_setContext ctx) {
			listener.onColumnValueSetBegin(ctx);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see com.abubusoft.kripton.processor.sqlite.grammars.jsql.JqlBaseListener# exitColumn_value_set(com.abubusoft.kripton.processor.sqlite.grammars.
		 * jsql.JqlParser.Column_value_setContext)
		 */
		@Override
		public void exitColumn_value_set(Column_value_setContext ctx) {
			listener.onColumnValueSetEnd(ctx);
		}

		@Override
		public void enterWhere_stmt_in_clause(Where_stmt_in_clauseContext ctx) {
			inStatement = true;
		}

		@Override
		public void exitWhere_stmt_in_clause(Where_stmt_in_clauseContext ctx) {
			inStatement = false;
		}
	}

	/**
	 * Replace place holder with element passed by listener.
	 *
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @param listener
	 *            the listener
	 * @return string obtained by replacements
	 */
	public String replace(final JQLContext jqlContext, JQL jql, final JQLReplacerListener listener) {
		return replace(jqlContext, jql.value, listener);
	}

	/**
	 * Replace place holder with element passed by listener.
	 *
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            string
	 * @param listener
	 *            the listener
	 * @return string obtained by replacements
	 */
	public String replace(final JQLContext jqlContext, String jql, final JQLReplacerListener listener) {
		JQLRewriterListener rewriterListener = new JQLRewriterListener();
		rewriterListener.init(listener);

		return replaceInternal(jqlContext, jql, replace, rewriterListener);

	}

	/**
	 * Replace internal.
	 *
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @param replace
	 *            the replace
	 * @param rewriterListener
	 *            the rewriter listener
	 * @return the string
	 */
	private String replaceInternal(final JQLContext jqlContext, String jql,
			final List<Triple<Token, Token, String>> replace, JqlBaseListener rewriterListener) {
		Pair<ParserRuleContext, CommonTokenStream> parser = prepareParser(jqlContext, jql);
		walker.walk(rewriterListener, parser.value0);

		TokenStreamRewriter rewriter = new TokenStreamRewriter(parser.value1);

		for (Triple<Token, Token, String> item : replace) {
			rewriter.replace(item.value0, item.value1, item.value2);
		}

		return rewriter.getText();
	}

	/**
	 * Replace from variable statement internal.
	 *
	 * @param context
	 *            the context
	 * @param jql
	 *            the jql
	 * @param replace
	 *            the replace
	 * @param rewriterListener
	 *            the rewriter listener
	 * @return the string
	 */
	private String replaceFromVariableStatementInternal(JQLContext context, String jql,
			final List<Triple<Token, Token, String>> replace, JqlBaseListener rewriterListener) {
		Pair<ParserRuleContext, CommonTokenStream> parser = prepareVariableStatement(context, jql);
		walker.walk(rewriterListener, parser.value0);

		TokenStreamRewriter rewriter = new TokenStreamRewriter(parser.value1);

		for (Triple<Token, Token, String> item : replace) {
			rewriter.replace(item.value0, item.value1, item.value2);
		}

		return rewriter.getText();
	}

	/**
	 * The Class JQLParameterName.
	 */
	public static class JQLParameterName {

		/**
		 * Instantiates a new JQL parameter name.
		 *
		 * @param value
		 *            the value
		 */
		private JQLParameterName(String value) {
			values = value.split("\\.");
		}

		/** The values. */
		private String[] values;

		/**
		 * Gets the value.
		 *
		 * @return the value
		 */
		public String getValue() {
			return values[values.length - 1];
		}

		/**
		 * Gets the bean name.
		 *
		 * @return the bean name
		 */
		public String getBeanName() {
			if (isNested())
				return values[0];

			return "";
		}

		/**
		 * Checks if is nested.
		 *
		 * @return true, if is nested
		 */
		public boolean isNested() {
			return values.length > 1;
		}

		/**
		 * Parses the.
		 *
		 * @param value
		 *            the value
		 * @return the JQL parameter name
		 */
		public static JQLParameterName parse(String value) {
			return new JQLParameterName(value);
		}
	}

	/**
	 * <p>
	 * Verify sql is syntactally correct, otherwise a KriptonProcessorException will be thrown.
	 * </p>
	 *
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 */
	public void verify(final JQLContext jqlContext, final JQL jql) {
		this.analyzeInternal(jqlContext, jql.value, new JqlBaseListener());
	}

	/**
	 * Extract all bind parameters and dynamic part used in query.
	 *
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @return the list
	 */
	public List<JQLPlaceHolder> extractPlaceHoldersAsList(final JQLContext jqlContext, String jql) {
		return extractPlaceHolders(jqlContext, jql, new ArrayList<JQLPlaceHolder>());
	}

	/**
	 * Extract all bind parameters and dynamic part used in query.
	 *
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @return the sets the
	 */
	public Set<JQLPlaceHolder> extractPlaceHoldersAsSet(final JQLContext jqlContext, String jql) {
		return extractPlaceHolders(jqlContext, jql, new LinkedHashSet<JQLPlaceHolder>());
	}

	/**
	 * Extract all bind parameters and dynamic part used in query.
	 *
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @return the sets the
	 */
	public Set<JQLPlaceHolder> extractPlaceHoldersFromVariableStatementAsSet(JQLContext jqlContext, String jql) {
		return extractPlaceHoldersFromVariableStatement(jqlContext, jql, new LinkedHashSet<JQLPlaceHolder>());
	}

	/**
	 * Extract all bind parameters and dynamic part used in query.
	 *
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @return the list
	 */
	public List<JQLPlaceHolder> extractFromVariableStatement(JQLContext jqlContext, String jql) {
		return extractPlaceHoldersFromVariableStatement(jqlContext, jql, new ArrayList<JQLPlaceHolder>());
	}

	/**
	 * Extract place holders.
	 *
	 * @param <L>
	 *            the generic type
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @param result
	 *            the result
	 * @return the l
	 */
	private <L extends Collection<JQLPlaceHolder>> L extractPlaceHolders(final JQLContext jqlContext, String jql,
			final L result) {
		final One<Boolean> valid = new One<>();
		valid.value0 = false;

		analyzeInternal(jqlContext, jql, new JqlBaseListener() {

			@Override
			public void enterBind_parameter(Bind_parameterContext ctx) {
				String value;
				if (ctx.bind_parameter_name() != null) {
					value = ctx.bind_parameter_name().getText();
				} else {
					value = ctx.getText();
				}
				result.add(new JQLPlaceHolder(JQLPlaceHolderType.PARAMETER, value));
			}

			@Override
			public void enterBind_dynamic_sql(Bind_dynamic_sqlContext ctx) {
				result.add(new JQLPlaceHolder(JQLPlaceHolderType.DYNAMIC_SQL, ctx.bind_parameter_name().getText()));
			}

		});
		return result;
	}

	/**
	 * Extract place holders from variable statement.
	 *
	 * @param <L>
	 *            the generic type
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @param result
	 *            the result
	 * @return the l
	 */
	private <L extends Collection<JQLPlaceHolder>> L extractPlaceHoldersFromVariableStatement(
			final JQLContext jqlContext, String jql, final L result) {
		final One<Boolean> valid = new One<>();

		if (!StringUtils.hasText(jql))
			return result;

		valid.value0 = false;

		analyzeVariableStatementInternal(jqlContext, jql, new JqlBaseListener() {

			@Override
			public void enterBind_parameter(Bind_parameterContext ctx) {
				String parameter;
				if (ctx.bind_parameter_name() != null) {
					parameter = ctx.bind_parameter_name().getText();
				} else {
					parameter = ctx.getText();
				}
				result.add(new JQLPlaceHolder(JQLPlaceHolderType.PARAMETER, parameter));
			}

			@Override
			public void enterBind_dynamic_sql(Bind_dynamic_sqlContext ctx) {
				result.add(new JQLPlaceHolder(JQLPlaceHolderType.DYNAMIC_SQL, ctx.bind_parameter_name().getText()));
			}

		});
		return result;
	}

	/**
	 * <p>
	 * Given a sql, replace som component like where, order by, etc..
	 * 
	 * <p>
	 * Note that only first level of variable statements will be replaced.
	 *
	 * @param jqlContext
	 *            the jql context
	 * @param jql
	 *            the jql
	 * @param listener
	 *            the listener
	 * @return the string
	 */
	public String replaceVariableStatements(final JQLContext jqlContext, final String jql,
			final JQLReplaceVariableStatementListener listener) {
		final List<Triple<Token, Token, String>> replace = new ArrayList<>();
		final One<Integer> currentSelectLevel = new One<Integer>(-1);

		JqlBaseListener rewriterListener = new JqlBaseListener() {

			@Override
			public void enterSelect_core(Select_coreContext ctx) {
				currentSelectLevel.value0++;
			}

			@Override
			public void enterSelect_or_values(Select_or_valuesContext ctx) {
				currentSelectLevel.value0++;
			}

			@Override
			public void exitSelect_core(Select_coreContext ctx) {
				currentSelectLevel.value0--;
			}

			@Override
			public void exitSelect_or_values(Select_or_valuesContext ctx) {
				currentSelectLevel.value0--;
			}

			@Override
			public void enterProjected_columns(Projected_columnsContext ctx) {
				// we work on level 0
				if (currentSelectLevel.value0 > 0)
					return;

				int start = ctx.getStart().getStartIndex() - 1;
				int stop = ctx.getStop().getStopIndex() + 1;

				if (start == stop)
					return;

				String statement = jql.substring(start, stop);

				String value = listener.onProjectedColumns(statement);

				if (value != null) {
					replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
				}
			}

			@Override
			public void enterWhere_stmt(Where_stmtContext ctx) {
				// we work on level 0
				if (currentSelectLevel.value0 > 0)
					return;

				int start = ctx.getStart().getStartIndex() - 1;
				int stop = ctx.getStop().getStopIndex() + 1;

				if (start == stop)
					return;

				String statement = jql.substring(start, stop);

				String value = listener.onWhere(statement);

				if (value != null) {
					replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
				}
			}

			@Override
			public void enterOrder_stmt(Order_stmtContext ctx) {
				// we work on level 0
				if (currentSelectLevel.value0 > 0)
					return;

				int start = ctx.getStart().getStartIndex() - 1;
				int stop = ctx.getStop().getStopIndex() + 1;

				if (start == stop)
					return;

				String statement = jql.substring(start, stop);

				String value = listener.onOrderBy(statement);

				if (value != null) {
					replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
				}
			}

			@Override
			public void enterGroup_stmt(Group_stmtContext ctx) {
				// we work on level 0
				if (currentSelectLevel.value0 > 0)
					return;

				int start = ctx.getStart().getStartIndex() - 1;
				int stop = ctx.getStop().getStopIndex() + 1;

				if (start == stop)
					return;

				String statement = jql.substring(start, stop);

				String value = listener.onGroup(statement);

				if (value != null) {
					replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
				}
			}

			@Override
			public void enterHaving_stmt(Having_stmtContext ctx) {
				// we work on level 0
				if (currentSelectLevel.value0 > 0)
					return;

				int start = ctx.getStart().getStartIndex() - 1;
				int stop = ctx.getStop().getStopIndex() + 1;

				if (start == stop)
					return;

				String statement = jql.substring(start, stop);

				String value = listener.onHaving(statement);

				if (value != null) {
					replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
				}
			}

			@Override
			public void enterOffset_stmt(Offset_stmtContext ctx) {
				// we work on level 0
				if (currentSelectLevel.value0 > 0)
					return;

				int start = ctx.getStart().getStartIndex() - 1;
				int stop = ctx.getStop().getStopIndex() + 1;

				if (start == stop)
					return;

				String statement = jql.substring(start, stop);

				String value = listener.onOffset(statement);

				if (value != null) {
					replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
				}
			}

			@Override
			public void enterLimit_stmt(Limit_stmtContext ctx) {
				// we work on level 0
				if (currentSelectLevel.value0 > 0)
					return;

				int start = ctx.getStart().getStartIndex() - 1;
				int stop = ctx.getStop().getStopIndex() + 1;

				if (start == stop)
					return;

				String statement = jql.substring(start, stop);

				String value = listener.onLimit(statement);

				if (value != null) {
					replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
				}
			}

			@Override
			public void enterColumn_name_set(Column_name_setContext ctx) {
				// we work on level 0
				if (currentSelectLevel.value0 > 0)
					return;

				int start = ctx.getStart().getStartIndex() - 1;
				int stop = ctx.getStop().getStopIndex() + 2;

				if (start == stop)
					return;

				String statement = jql.substring(start, stop);

				String value = listener.onColumnNameSet(statement);

				if (value != null) {
					replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
				}
			}

			@Override
			public void enterColumn_value_set(Column_value_setContext ctx) {
				// we work on level 0
				if (currentSelectLevel.value0 > 0)
					return;

				int start = ctx.getStart().getStartIndex() - 1;
				int stop = ctx.getStop().getStopIndex() + 2;

				if (start == stop)
					return;

				String statement = jql.substring(start, stop);

				String value = listener.onColumnValueSet(statement);

				if (value != null) {
					replace.add(new Triple<Token, Token, String>(ctx.start, ctx.stop, value));
				}
			}

		};

		return replaceInternal(jqlContext, jql, replace, rewriterListener);
	}

}
