package adql.query.from;

/*
 * This file is part of ADQLLibrary.
 *
 * ADQLLibrary is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ADQLLibrary is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with ADQLLibrary.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Copyright 2012-2019 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
 *                       Astronomisches Rechen Institut (ARI)
 */

import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;

import adql.db.DBChecker;
import adql.db.DBColumn;
import adql.db.DBTable;
import adql.db.DefaultDBTable;
import adql.db.SearchColumnList;
import adql.query.ADQLIterator;
import adql.query.ADQLObject;
import adql.query.ADQLQuery;
import adql.query.IdentifierField;
import adql.query.TextPosition;

/**
 * It represents any item of the clause FROM: a table name or a sub-query.<br />
 * A table reference may have an alias (MUST if it is a sub-query).
 *
 * @author Gr&eacute;gory Mantelet (CDS;ARI)
 * @version 1.5 (09/2019)
 */
public class ADQLTable implements ADQLObject, FromContent {

	/** The name of the catalog which contains the table. */
	private String catalog = null;

	/** The name of the schema which contains the table. */
	private String schema = null;

	/** The name of the table. */
	private String table;

	/** A sub-query whose the result will be used as a table. */
	private ADQLQuery subQuery;

	/** Label of the table reference.
	 * If not a sub-query, this attribute is set by default to the table name. */
	private String alias = null;

	/** Lets specify the case sensitivity of the catalog, schema, table and alias parts. */
	private byte caseSensitivity = 0;

	/** The corresponding table in the "database". */
	private DBTable dbLink = null;

	/** Position in the original ADQL query string. */
	private TextPosition position = null;

	/**
	 * Builds a reference to a table with its name (simple or full ({catalog}.{schema}.{table})).
	 *
	 * @param table	Name of the table (simple or full ({catalog}.{schema}.{table})).
	 */
	public ADQLTable(String table){
		setTable(table);
		subQuery = null;
	}

	/**
	 * Builds a reference to a table with its name and the name of its schema.
	 *
	 * @param schema	Name of its schema.
	 * @param table		Name of the table.
	 */
	public ADQLTable(String schema, String table){
		setTableName(table);
		setSchemaName(schema);
	}

	/**
	 * Builds a reference to a table with its name, the name of its schema and the name of its catalog.
	 *
	 * @param catalog	Name of its catalog.
	 * @param schema	Name of its schema.
	 * @param table		Name of the table.
	 */
	public ADQLTable(String catalog, String schema, String table){
		this(schema, table);
		setCatalogName(catalog);
	}

	/**
	 * Builds a reference to a sub-query.
	 *
	 * @param query	Sub-query.
	 *
	 * @see #setSubQuery(ADQLQuery)
	 */
	public ADQLTable(ADQLQuery query){
		setSubQuery(query);
	}

	/**
	 * Builds an ADQL table by copying the given one.
	 *
	 * @param toCopy		The ADQLTable to copy.
	 *
	 * @throws Exception	If there is an error during the copy.
	 */
	public ADQLTable(ADQLTable toCopy) throws Exception{
		catalog = toCopy.catalog;
		schema = toCopy.schema;
		table = toCopy.table;
		subQuery = (toCopy.subQuery == null) ? null : (ADQLQuery)toCopy.subQuery.getCopy();
		alias = toCopy.alias;
		caseSensitivity = toCopy.caseSensitivity;
		position = toCopy.position;
	}

	/**
	 * <p>Lets normalizing any catalog/schema/table name or alias.</p>
	 * <p>If the name is surrounded by double-quotes, they are removed
	 * and the corresponding field will be declared as case sensitive.</p>
	 *
	 * @param name		Name to normalize.
	 * @param field		The name part to normalize and to get (if normalized, the case sensitivity of the given field will be set).
	 *
	 * @return		The normalized name.
	 */
	protected String normalizeName(final String name, IdentifierField field){
		if (name == null)
			return null;

		StringBuffer n = new StringBuffer(name);
		n.trimToSize();
		if (n.length() == 0)
			return null;
		else{
			if (n.length() > 1 && n.charAt(0) == '\"' && n.charAt(n.length() - 1) == '\"'){
				n.deleteCharAt(0);
				n.deleteCharAt(n.length() - 1);
				n.trimToSize();
				if (n.length() == 0)
					return null;
				else
					setCaseSensitive(field, true);
			}
		}
		return n.toString();
	}

	/**
	 * Gets the position in the original ADQL query string.
	 *
	 * @return	The position of this {@link ADQLTable}.
	 */
	@Override
	public final TextPosition getPosition(){
		return position;
	}

	/**
	 * Sets the position at which this {@link ADQLTable} has been found in the original ADQL query string.
	 *
	 * @param pos	Position of this {@link ADQLTable}.
	 */
	@Override
	public final void setPosition(final TextPosition pos){
		position = pos;
	}

	/**
	 * Gets the name of the catalog which contains this table.
	 *
	 * @return Catalog name.
	 */
	public final String getCatalogName(){
		return catalog;
	}

	/**
	 * Sets the name of the catalog which contains this table.
	 *
	 * @param catalog The new name of its catalog.
	 */
	public final void setCatalogName(String catalog){
		final String temp = normalizeName(catalog, IdentifierField.CATALOG);
		if ((this.catalog == null && temp != null) || (this.catalog != null && !this.catalog.equalsIgnoreCase(temp)))
			dbLink = null;
		this.catalog = temp;
	}

	/**
	 * Gets the name of the schema which contains this table.
	 *
	 * @return Schema name.
	 */
	public final String getSchemaName(){
		return schema;
	}

	/**
	 * Sets the name of the schema which contains this table.
	 *
	 * @param schema The new name of its schema.
	 */
	public final void setSchemaName(String schema){
		final String temp = normalizeName(schema, IdentifierField.SCHEMA);
		if ((this.schema == null && temp != null) || (this.schema != null && !this.schema.equalsIgnoreCase(temp)))
			dbLink = null;
		this.schema = temp;
	}

	/**
	 * Gets the name of the table.
	 *
	 * @return Table name.
	 */
	public final String getTableName(){
		return table;
	}

	/**
	 * Gets the full name of this table (catalogName . schemaName . tableName)
	 * by respecting the case sensitivity of each field (if case sensitive double-quotes will surround the concerned fields name).
	 *
	 * @return	Its full name.
	 */
	public final String getFullTableName(){
		if (table == null)
			return "";

		StringBuffer name = new StringBuffer();

		// CATALOG:
		if (catalog != null){
			if (isCaseSensitive(IdentifierField.CATALOG))
				name.append('\"').append(catalog).append('\"').append('.');
			else
				name.append(catalog).append('.');
		}
		// SCHEMA:
		if (schema != null){
			if (isCaseSensitive(IdentifierField.SCHEMA))
				name.append('\"').append(schema).append('\"').append('.');
			else
				name.append(schema).append('.');
		}
		// TABLE:
		if (isCaseSensitive(IdentifierField.TABLE))
			name.append('\"').append(table).append('\"');
		else
			name.append(table);

		return name.toString();
	}

	/**
	 * Sets the name of the table.
	 *
	 * @param newTableName	The new name of the table.
	 */
	public void setTableName(String newTableName){
		// Normalise the table name:
		final String temp = normalizeName(newTableName, IdentifierField.TABLE);

		// Remove the DB metadata ONLY if the table name is different:
		if ((this.table == null && temp != null) || (this.table != null && !this.table.equalsIgnoreCase(temp)))
			dbLink = null;

		// Set the table name:
		this.table = temp;

		// Ensure no sub-query is set:
		if (table != null)
			subQuery = null;
	}

	/**
	 * Updates the whole Table according to the given table reference ({catalog}.{schema}.{table}).
	 *
	 * @param tableRef	The complete table reference ({catalog}.{schema}.{table}).
	 */
	public final void setTable(String tableRef){
		String[] parts = (tableRef == null) ? null : tableRef.split("\\.");
		if (parts != null && parts.length > 3)
			return;
		else{
			int i = (parts == null) ? -1 : (parts.length - 1);
			setTableName((i < 0) ? null : parts[i--]);
			setSchemaName((i < 0) ? null : parts[i--]);
			setCatalogName((i < 0) ? null : parts[i]);
			if (table != null)
				subQuery = null;
		}
	}

	/**
	 * Gets the sub-query used as table.
	 *
	 * @return	Sub-query.
	 */
	public final ADQLQuery getSubQuery(){
		return subQuery;
	}

	/**
	 * Sets the sub-query to use as table.
	 *
	 * @param query	Sub-query (MUST NOT BE NULL).
	 *
	 * @see #refreshDBLink()
	 */
	public final void setSubQuery(final ADQLQuery query){
		if (query != null){
			// set all ADQLTable attributes:
			subQuery = query;
			catalog = null;
			schema = null;
			table = null;
			dbLink = null;
			position = null;

			// set the DB link:
			refreshDBLink();
		}
	}

	/**
	 * (Re-)Builds a default description of this ADQL table <u>ONLY IF it is a sub-query AND there is an alias</u>.
	 * This method has no effect if this table is not a sub-query or has no alias.
	 *
	 * @see DefaultDBTable
	 * @see ADQLQuery#getResultingColumns()
	 * @see DBColumn#copy(String, String, DBTable)
	 */
	public final void refreshDBLink(){
		if (isSubQuery() && hasAlias()){
			DefaultDBTable dbTable = new DefaultDBTable(alias);
			DBColumn[] columns = subQuery.getResultingColumns();
			for(DBColumn dbCol : columns)
				dbTable.addColumn(dbCol.copy(dbCol.getADQLName(), dbCol.getADQLName(), dbTable));
			dbLink = dbTable;
		}
	}

	/**
	 * Tells whether this table reference is a sub-query or a table name/alias.
	 *
	 * @return	<i>true</i> if this table is a sub-query, <i>false</i> else.
	 */
	public final boolean isSubQuery(){
		return subQuery != null;
	}

	/**
	 * Gets the label of this table.
	 *
	 * @return	Table label.
	 */
	public final String getAlias(){
		return alias;
	}

	/**
	 * Tells whether this table has an alias or not.
	 *
	 * @return	<i>true</i> if this table has an alias, <i>false</i> otherwise.
	 */
	public final boolean hasAlias(){
		return alias != null;
	}

	/**
	 * Sets the label of this table.
	 *
	 * @param alias	Label to put on this table.
	 */
	public void setAlias(String alias){
		this.alias = normalizeName(alias, IdentifierField.ALIAS);
	}

	/**
	 * Indicates whether the specified field (catalog, schema or table) is case sensitive or not.
	 *
	 * @param field		A field (catalog, schema or table).
	 *
	 * @return			<i>true</i> if the specified field is case sensitive, <i>false</i> otherwise.
	 *
	 * @see IdentifierField
	 * @see IdentifierField#isCaseSensitive(byte)
	 */
	public final boolean isCaseSensitive(IdentifierField field){
		return field.isCaseSensitive(caseSensitivity);
	}

	/**
	 * Sets the case sensitivity of the specified field (catalog, schema or table).
	 *
	 * @param field			The field for which the case sensitivity must be updated.
	 *
	 * @param sensitive		<i>true</i> if the specified field must be case sensitive, <i>false</i> otherwise.
	 *
	 * @see IdentifierField
	 * @see IdentifierField#setCaseSensitive(byte, boolean)
	 */
	public final void setCaseSensitive(IdentifierField field, boolean sensitive){
		caseSensitivity = field.setCaseSensitive(caseSensitivity, sensitive);
	}

	/**
	 * Indicates whether all fields (catalog, schema and table) are case sensitive.
	 *
	 * @return			<i>true</i> if all fields are case sensitive, <i>false</i> otherwise.
	 *
	 * @see IdentifierField#isFullCaseSensitive(byte)
	 */
	public final boolean isCaseSensitive(){
		return IdentifierField.isFullCaseSensitive(caseSensitivity);
	}

	/**
	 * Sets the case sensitivity of all fields (catalog, schema and table).
	 *
	 * @param sensitive		<i>true</i> if all fields must be case sensitive, <i>false</i> otherwise.
	 *
	 * @see IdentifierField#getFullCaseSensitive(boolean)
	 */
	public final void setCaseSensitive(boolean sensitive){
		caseSensitivity = IdentifierField.getFullCaseSensitive(sensitive);
	}

	/**
	 * Gets the whole case sensitivity of this ADQL table.
	 *
	 * @return	Its new case sensitivity (one bit per fields).
	 *
	 * @see IdentifierField
	 */
	public final byte getCaseSensitive(){
		return caseSensitivity;
	}

	/**
	 * Sets the whole case sensitivity of this ADQL table.
	 *
	 * @param sensitivity	Its new case sensitivity (one bit per fields).
	 *
	 * @see IdentifierField
	 */
	public final void setCaseSensitive(final byte sensitivity){
		caseSensitivity = sensitivity;
	}

	/**
	 * <p>Gets the corresponding {@link DBTable}.</p>
	 * <p><i><u>Note:</u> This information is added automatically by {@link DBChecker} when {@link DBChecker#check(adql.query.ADQLQuery)} is called.</i></p>
	 *
	 * @return The corresponding {@link DBTable}.
	 */
	public final DBTable getDBLink(){
		return dbLink;
	}

	/**
	 * Sets the {@link DBTable} corresponding to this {@link ADQLTable}.
	 *
	 * @param dbLink Its corresponding {@link DBTable}.
	 */
	public final void setDBLink(DBTable dbLink){
		this.dbLink = dbLink;
	}

	@Override
	public SearchColumnList getDBColumns(){
		SearchColumnList list = new SearchColumnList();
		if (isSubQuery() && dbLink == null)
			refreshDBLink();
		if (dbLink != null){
			for(DBColumn dbCol : dbLink)
				list.add(dbCol);
		}
		return list;
	}

	@Override
	public List<ADQLTable> getTables(){
		ArrayList<ADQLTable> tables = new ArrayList<ADQLTable>();
		tables.add(this);
		return tables;
	}

	@Override
	public List<ADQLTable> getTablesByAlias(final String alias, final boolean caseSensitive){
		ArrayList<ADQLTable> tables = new ArrayList<ADQLTable>();

		if (hasAlias()){
			if (!caseSensitive){
				if (getAlias().equalsIgnoreCase(alias))
					tables.add(this);
			}else{
				if (IdentifierField.ALIAS.isCaseSensitive(caseSensitivity)){
					if (getAlias().equals(alias))
						tables.add(this);
				}else{
					if (getAlias().toLowerCase().equals(alias))
						tables.add(this);
				}
			}
		}

		return tables;
	}

	@Override
	public ADQLObject getCopy() throws Exception{
		return new ADQLTable(this);
	}

	@Override
	public String getName(){
		return hasAlias() ? alias : (isSubQuery() ? "{subquery}" : getTableName());
	}

	@Override
	public ADQLIterator adqlIterator(){
		return new ADQLIterator() {

			private boolean subQueryGot = !isSubQuery();

			@Override
			public ADQLObject next(){
				if (!subQueryGot){
					subQueryGot = true;
					return subQuery;
				}else
					throw new NoSuchElementException();
			}

			@Override
			public boolean hasNext(){
				return !subQueryGot;
			}

			@Override
			public void replace(ADQLObject replacer) throws UnsupportedOperationException, IllegalStateException{
				if (!subQueryGot)
					throw new IllegalStateException("replace(ADQLObject) impossible: next() has not yet been called !");

				if (replacer == null)
					remove();

				if (replacer instanceof ADQLQuery)
					subQuery = (ADQLQuery)replacer;
				else
					throw new UnsupportedOperationException("Impossible to replace a sub-query (" + subQuery.toADQL() + ") by a " + replacer.getClass().getName() + " (" + replacer.toADQL() + ") !");
			}

			@Override
			public void remove(){
				if (!subQueryGot)
					throw new IllegalStateException("remove() impossible: next() has not yet been called !");
				else
					throw new UnsupportedOperationException("Impossible to remove the sub-query of an ADQLTable (" + toADQL() + ") !");
			}
		};
	}

	@Override
	public String toADQL(){
		return (isSubQuery() ? ("(" + subQuery.toADQL() + ")") : getFullTableName()) + ((alias == null) ? "" : (" AS " + (isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + alias + "\"") : alias)));
	}

}