// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.db.oom.sqlgen;

import jodd.db.oom.ColumnData;
import jodd.db.oom.ColumnAliasType;
import jodd.db.oom.DbSqlGenerator;
import jodd.db.oom.DbOomQuery;
import jodd.db.oom.sqlgen.chunks.ColumnValueChunk;
import jodd.db.oom.sqlgen.chunks.SqlChunk;
import jodd.db.oom.sqlgen.chunks.RawSqlChunk;
import jodd.db.oom.sqlgen.chunks.TableChunk;
import jodd.db.oom.sqlgen.chunks.ColumnsSelectChunk;
import jodd.db.oom.sqlgen.chunks.InsertChunk;
import jodd.db.oom.sqlgen.chunks.ReferenceChunk;
import jodd.db.oom.sqlgen.chunks.ValueChunk;
import jodd.db.oom.sqlgen.chunks.UpdateSetChunk;
import jodd.db.oom.sqlgen.chunks.MatchChunk;
import jodd.db.DbSession;
import jodd.util.StringPool;

import java.util.Map;

/**
 * Nice SQL query generator that provides some automatic query generation.
 * <p>
 * Query is built by appending 'chunks' - parts of the query. These chunks may be
 * very simple, as a simple string. However, the main reason of existence of this class are
 * chunks that auto-generate part of the query based on provided domain object. They can be
 * used to easily create some most common queries in an efficient way.
 * <p>
 * Although it is not necessary, use of {@link jodd.db.oom.meta.DbId} annotation boost the functionality
 * of this query builder.
 * <p>
 * Some chunks deals with relations between tables, so they must be aware of foreign key names. Here the
 * naming convention is used, and even users might have their own foreign key naming convention. 
 * <p>
 * Furthermore, if all queries are generated using just sql builder, it is possible to use dialects for various
 * database types.
 */
public class DbSqlBuilder extends TemplateData implements DbSqlGenerator {

	/**
	 * Creates new SQL builder.
	 */
	public DbSqlBuilder() {
		super();
	}

	public DbSqlBuilder(String template) {
		super();
		append(template);
	}

	public static DbSqlBuilder sql() {
		return new DbSqlBuilder();
	}

	/**
	 * Template constructor.
	 */
	public static DbSqlBuilder sql(String template) {
		return new DbSqlBuilder().append(template);
	}

	/**
	 * Resets the builder (soft reset), so it can be used again.
	 * Configuration is preserved.
	 * @see TemplateData#resetSoft()
	 */
	public DbSqlBuilder reset() {
		resetSoft();
		return this;
	}

	/**
	 * Hard reset of the builder, all configuration is reset.
	 * @see TemplateData#resetHard()
	 */
	public DbSqlBuilder resetAll() {
		resetHard();
		return this;
	}

	/**
	 * Builds the query and returns parsed data.
	 * Returned value can be cached or stored as a constant value
	 * to prevent further parsing of the same code.
	 */
	public ParsedSql parse() {
		return new ParsedSql(this);
	}


	// ---------------------------------------------------------------- settings

	/**
	 * Specifies column alias type. May be <code>null</code> when column aliases are not used.
	 */
	public DbSqlBuilder aliasColumnsAs(ColumnAliasType aliasesType) {
		this.columnAliasType = aliasesType;
		return this;
	}

	/**
	 * Defines object reference and an object.
	 */
	public DbSqlBuilder use(String name, Object value) {
		setObjectReference(name, value);
		return this;
	}


	// ---------------------------------------------------------------- chunks

	protected SqlChunk firstChunk;
	protected SqlChunk lastChunk;
	protected int totalChunks;
	protected static final TemplateParser templateParser = new TemplateParser();

	/**
	 * Appends chunk to the list. Chunks <b>must</b> be added using this method.
	 */
	public DbSqlBuilder addChunk(SqlChunk chunk) {
		if (lastChunk == null) {
			lastChunk = firstChunk = chunk;
		} else {
			chunk.insertChunkAfter(lastChunk);
			lastChunk = chunk;
		}
		totalChunks++;
		return this;
	}

	// ---------------------------------------------------------------- template

	/**
	 * Parses provided text into the list of chunks and appends them to the list.
	 */
	public DbSqlBuilder append(String text) {
		templateParser.parse(this, text);
		return this;
	}

	/**
	 * Simply adds text without parsing to the query.
	 */
	public DbSqlBuilder appendRaw(String text) {
		addChunk(new RawSqlChunk(text));
		return this;
	}

	/**
	 * User-friendly {@link #append(String)}.
	 */
	public DbSqlBuilder $(String text) {
		return append(text);
	}

	/**
	 * Single space shortcut.
	 */
	public DbSqlBuilder $() {
		return appendRaw(StringPool.SPACE);
	}

	public DbSqlBuilder $(SqlChunk chunk) {
		return addChunk(chunk);
	}

	// ---------------------------------------------------------------- interface

	protected String generatedQuery;

	/**
	 * {@inheritDoc}
	 */
	public String generateQuery() {
		reset();

		// initialization
		SqlChunk chunk = firstChunk;
		while (chunk != null) {
			chunk.init(this);
			chunk = chunk.getNextChunk();
		} 

		// process
		StringBuilder query = new StringBuilder();
		chunk = firstChunk;
		try {
			while (chunk != null) {
				chunk.process(query);
				chunk = chunk.getNextChunk();
			}
		} catch (DbSqlBuilderException dsbex) {
			dsbex.setQueryString(query.toString());
			throw dsbex;
		}

		generatedQuery = query.toString();

		return generatedQuery;
	}

	/**
	 * {@inheritDoc}
	 */
	public Map<String, ColumnData> getColumnData() {
		return columnData;
	}

	public Map<String, ParameterValue> getQueryParameters() {
		return parameters;
	}

	/**
	 * {@inheritDoc}
	 */
	public String[] getJoinHints() {
		if (hints == null) {
			return null;
		}
		return hints.toArray(new String[hints.size()]);
	}

	// ---------------------------------------------------------------- table

	public DbSqlBuilder table(String entityName) {
		return addChunk(new TableChunk(entityName));
	}

	public DbSqlBuilder table(String entityName, String alias) {
		return addChunk(new TableChunk(entityName, alias));
	}

	public DbSqlBuilder table(Object entity, String alias) {
		return addChunk(new TableChunk(entity, alias));
	}

	public DbSqlBuilder table(Object entity, String alias, String tableReference) {
		return addChunk(new TableChunk(entity, alias, tableReference));
	}

	public DbSqlBuilder table(Object entity) {
		return addChunk(new TableChunk(entity));
	}

	// ---------------------------------------------------------------- columns

	public DbSqlBuilder column(String reference) {
		return addChunk(new ColumnsSelectChunk(reference));
	}

	public DbSqlBuilder column(String tableRef, String columnRef) {
		return addChunk(new ColumnsSelectChunk(tableRef, columnRef));
	}

	public DbSqlBuilder columnsAll(String tableRef) {
		return addChunk(new ColumnsSelectChunk(tableRef, true));
	}

	public DbSqlBuilder columnsIds(String tableRef) {
		return addChunk(new ColumnsSelectChunk(tableRef, false));
	}


	// ---------------------------------------------------------------- reference

	public DbSqlBuilder ref(String columnRef) {
		return addChunk(new ReferenceChunk(columnRef));
	}

	public DbSqlBuilder ref(String tableRef, String columnRef) {
		return addChunk(new ReferenceChunk(tableRef, columnRef, false));
	}

	public DbSqlBuilder refId(String tableRef) {
		return addChunk(new ReferenceChunk(tableRef, null, true));
	}


	// ---------------------------------------------------------------- match

	/**
	 * Creates condition part of the query only for existing columns.
	 */
	public DbSqlBuilder match(String tableRef, Object value) {
		return addChunk(new MatchChunk(tableRef, value, SqlChunk.COLS_ONLY_EXISTING));
	}

	public DbSqlBuilder match(String tableRef, String objectRef) {
		return addChunk(new MatchChunk(tableRef, objectRef, SqlChunk.COLS_ONLY_EXISTING));
	}

	/**
	 * Creates condition part of the query for id columns
	 */
	public DbSqlBuilder matchIds(String tableRef, Object value) {
		return addChunk(new MatchChunk(tableRef, value, SqlChunk.COLS_ONLY_IDS));
	}

	public DbSqlBuilder matchIds(String tableRef, String objectRef) {
		return addChunk(new MatchChunk(tableRef, objectRef, SqlChunk.COLS_ONLY_IDS));
	}

	/**
	 * Creates condition part of the query for all columns, including the null values.
	 */
	public DbSqlBuilder matchAll(String tableRef, Object value) {
		return addChunk(new MatchChunk(tableRef, value, SqlChunk.COLS_ALL));
	}

	public DbSqlBuilder matchAll(String tableRef, String objectRef) {
		return addChunk(new MatchChunk(tableRef, objectRef, SqlChunk.COLS_ALL));
	}

	public DbSqlBuilder match(String expression) {
		return addChunk(new MatchChunk(expression));
	}


	// ---------------------------------------------------------------- values

	public DbSqlBuilder value(String name, Object value) {
		return addChunk(new ValueChunk(name, value));
	}

	public DbSqlBuilder value(Object value) {
		return addChunk(new ValueChunk(null, value));
	}

	public DbSqlBuilder valueRef(String objectReference) {
		return addChunk(new ValueChunk(objectReference));
	}

	public DbSqlBuilder columnValue(String name, Object value) {
		return addChunk(new ColumnValueChunk(name, value));
	}

	public DbSqlBuilder columnValue(Object value) {
		return addChunk(new ColumnValueChunk(null, value));
	}

	public DbSqlBuilder columnValueRef(String objectReference) {
		return addChunk(new ColumnValueChunk(objectReference));
	}

	// ---------------------------------------------------------------- insert

	public DbSqlBuilder insert(String entityName, Object values) {
		return addChunk(new InsertChunk(entityName, values));
	}

	public DbSqlBuilder insert(Class entity, Object values) {
		return addChunk(new InsertChunk(entity, values));
	}

	public DbSqlBuilder insert(Object values) {
		return addChunk(new InsertChunk(values.getClass(), values));
	}

	// ---------------------------------------------------------------- update set

	public DbSqlBuilder set(String tableRef, Object values) {
		return addChunk(new UpdateSetChunk(tableRef, values, SqlChunk.COLS_ONLY_EXISTING));
	}

	public DbSqlBuilder setAll(String tableRef, Object values) {
		return addChunk(new UpdateSetChunk(tableRef, values, SqlChunk.COLS_ALL));
	}

	// ---------------------------------------------------------------- query factories

	/**
	 * Returns {@link jodd.db.oom.DbOomQuery} instance for more fluent interface.
	 *
	 */
	public DbOomQuery query() {
		return new DbOomQuery(this);
	}

	public DbOomQuery query(DbSession session) {
		return new DbOomQuery(session, this);
	}

}
