// 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.chunks;

import jodd.db.oom.DbEntityColumnDescriptor;
import jodd.db.oom.DbEntityDescriptor;
import jodd.db.oom.DbEntityManager;
import jodd.db.oom.sqlgen.DbSqlBuilderException;
import jodd.db.oom.sqlgen.TemplateData;
import jodd.util.CharUtil;
import jodd.util.StringUtil;

/**
 * SQL chunk defines part of the SQL query that can be processed.
 */
public abstract class SqlChunk {

	public static final int COLS_NA = 0;                // using explicit reference.
	public static final int COLS_ONLY_EXISTING = 1;     // using only existing columns i.e. that are not-null
	public static final int COLS_ONLY_IDS = 2;          // using only identity columns
	public static final int COLS_ALL = 3;               // using all available columns
	public static final int COLS_ALL_BUT_ID = 4;        // using all available columns except the identity column
	public static final int COLS_NA_MULTI = 0;          // using explicit reference.

	public static final int CHUNK_RAW = -1;
	public static final int CHUNK_SELECT_COLUMNS = 1;
	public static final int CHUNK_TABLE = 2;
	public static final int CHUNK_REFERENCE = 3;
	public static final int CHUNK_MATCH = 4;
	public static final int CHUNK_VALUE = 5;
	public static final int CHUNK_INSERT = 5;
	public static final int CHUNK_UPDATE = 6;

	private final int chunkType;
	private final DbEntityManager dbEntityManager;

	protected SqlChunk(final DbEntityManager dbEntityManager, final int chunkType) {
		this.dbEntityManager = dbEntityManager;
		this.chunkType = chunkType;
	}

	// ---------------------------------------------------------------- linked list

	protected SqlChunk previousChunk;

	/**
	 * Returns previous chunk.
	 */
	public SqlChunk getPreviousChunk() {
		return previousChunk;
	}

	protected SqlChunk nextChunk;

	/**
	 * Returns next chunk.
	 */
	public SqlChunk getNextChunk() {
		return nextChunk;
	}

	/**
	 * Appends chunk to previous one and maintains the double-linked list of the previous chunk.
	 * Current surrounding connections of this chunk will be cut-off.
	 */
	public void insertChunkAfter(final SqlChunk previous) {
		final SqlChunk next = previous.nextChunk;
		previous.nextChunk = this;
		this.previousChunk = previous;
		if (next != null) {
			next.previousChunk = this;
			this.nextChunk = next;
		}
	}

	/**
	 * Returns <code>true</code> if previous chunk is of provided type.
	 */
	public boolean isPreviousChunkOfType(final int type) {
		if (previousChunk == null) {
			return false;
		}
		return previousChunk.chunkType == type;
	}

	/**
	 * Returns <code>true</code> if previous chunk is of the same type.
	 */
	public boolean isPreviousChunkOfSameType() {
		if (previousChunk == null) {
			return false;
		}
		return previousChunk.chunkType == chunkType;
	}

	/**
	 * Returns <code>true</code> if previous chunk is not raw.
	 */
	public boolean isPreviousMacroChunk() {
		if (previousChunk == null) {
			return false;
		}
		return previousChunk.chunkType != CHUNK_RAW;
	}

	public boolean isPreviousRawChunk() {
		if (previousChunk == null) {
			return false;
		}
		return previousChunk.chunkType == CHUNK_RAW;
	}


	// ---------------------------------------------------------------- process

	protected TemplateData templateData;      // working template context

	/**
	 * Initializes chunk. Assigns {@link jodd.db.oom.sqlgen.TemplateData} to chunk.
	 * If chunk needs some pre-processing, they should be done here.
	 */
	public void init(final TemplateData templateData) {
		this.templateData = templateData;
	}

	/**
	 * Process the chunk and appends data to the output.
	 */
	public abstract void process(StringBuilder out);


	// ---------------------------------------------------------------- lookup

	/**
	 * Lookups for entity name and throws exception if entity name not found.
	 */
	protected DbEntityDescriptor lookupName(final String entityName) {
		final DbEntityDescriptor ded = dbEntityManager.lookupName(entityName);
		if (ded == null) {
			throw new DbSqlBuilderException("Entity name not registered: " + entityName);
		}
		return ded;
	}

	/**
	 * Lookups for entity name and throws an exception if entity type is invalid.
	 */
	protected DbEntityDescriptor lookupType(final Class entity) {
		final DbEntityDescriptor ded = dbEntityManager.lookupType(entity);
		if (ded == null) {
			throw new DbSqlBuilderException("Invalid or not-persistent entity: " + entity.getName());
		}
		return ded;
	}

	/**
	 * Lookups for table reference and throws an exception if table reference not found.
	 */
	protected DbEntityDescriptor lookupTableRef(final String tableRef) {
		return lookupTableRef(tableRef, true);
	}

	/**
	 * Lookups for table reference and optionally throws an exception if table reference not found.
	 */
	protected DbEntityDescriptor lookupTableRef(final String tableRef, final boolean throwExceptionIfNotFound) {
		final DbEntityDescriptor ded = templateData.getTableDescriptor(tableRef);
		if (ded == null) {
			if (throwExceptionIfNotFound) {
				throw new DbSqlBuilderException("Invalid table reference: " + tableRef);
			}
		}
		return ded;
	}

	/**
	 * Finds a table that contains given column.
	 */
	protected DbEntityDescriptor findColumnRef(final String columnRef) {
		final DbEntityDescriptor ded = templateData.findTableDescriptorByColumnRef(columnRef);
		if (ded == null) {
			throw new DbSqlBuilderException("Invalid column reference: [" + columnRef + "]");
		}
		return ded;
	}

	// ---------------------------------------------------------------- misc

	/**
	 * Resolves table name or alias that will be used in the query.
	 */
	protected String resolveTable(final String tableRef, final DbEntityDescriptor ded) {
		final String tableAlias = templateData.getTableAlias(tableRef);
		if (tableAlias != null) {
			return tableAlias;
		}
		return ded.getTableNameForQuery();
	}

	/**
	 * Defines parameter with name and its value.
	 */
	protected void defineParameter(final StringBuilder query, String name, final Object value, final DbEntityColumnDescriptor dec) {
		if (name == null) {
			name = templateData.getNextParameterName();
		}
		query.append(':').append(name);
		templateData.addParameter(name, value, dec);
	}

	/**
	 * Resolves object to a class.
	 */
	protected static Class resolveClass(final Object object) {
		final Class type = object.getClass();
		return type == Class.class ? (Class) object : type;
	}

	/**
	 * Returns <code>true</code> if a value is considered empty i.e. not existing.
	 */
	protected boolean isEmptyColumnValue(final DbEntityColumnDescriptor dec, final Object value) {
		if (value == null) {
			return true;
		}

		// special case for ID column
		if (dec.isId() && value instanceof Number) {
			final double d = ((Number) value).doubleValue();
			if (d == 0.0d) {
				return true;
			}
		}

		// special case for primitives
		if (dec.getPropertyType().isPrimitive()) {
			if (char.class == dec.getPropertyType()) {
				final Character c = ((Character) value);
				if ('\u0000' == c.charValue()) {
					return true;
				}
			} else {
				final double d = ((Number) value).doubleValue();
				if (d == 0) {
					return true;
				}
			}
		}

		// special case for strings
		if (value instanceof CharSequence) {
			if (StringUtil.isBlank((CharSequence) value)) {
				return true;
			}
		}

		return false;
	}


	// ---------------------------------------------------------------- separation

	/**
	 * Appends missing space if the output doesn't end with whitespace.
	 */
	protected void appendMissingSpace(final StringBuilder out) {
		int len = out.length();
		if (len == 0) {
			return;
		}
		len--;
		if (!CharUtil.isWhitespace(out.charAt(len))) {
			out.append(' ');
		}
	}

	/**
	 * Separates from previous chunk by comma if is of the same type.
	 */
	protected void separateByCommaOrSpace(final StringBuilder out) {
		if (isPreviousChunkOfSameType()) {
			out.append(',').append(' ');
		} else {
			appendMissingSpace(out);
		}
	}

}
