package tap.metadata;

/*
 * This file is part of TAPLibrary.
 * 
 * TAPLibrary 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.
 * 
 * TAPLibrary 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 TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Copyright 2012-2016 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
 *                       Astronomisches Rechen Institut (ARI)
 */

import java.awt.List;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

import tap.metadata.TAPTable.TableType;

/**
 * <p>Represent a schema as described by the IVOA standard in the TAP protocol definition.</p>
 * 
 * <p>
 * 	This object representation has exactly the same fields as the column of the table TAP_SCHEMA.schemas.
 * 	But it also provides a way to add other data. For instance, if information not listed in the standard
 * 	may be stored here, they can be using the function {@link #setOtherData(Object)}. This object can be
 * 	a single value (integer, string, ...), but also a {@link Map}, {@link List}, etc...
 * </p>
 * 
 * <p><i>Note:
 * 	On the contrary to {@link TAPColumn} and {@link TAPTable}, a {@link TAPSchema} object MAY have no DB name.
 * 	But by default, at the creation the DB name is the simplified ADQL name (i.e. as it is returned by {@link #getADQLName()}).
 * 	Once created, it is possible to set the DB name with {@link #setDBName(String)}. This DB name MAY be qualified,
 * 	BUT MUST BE without double quotes.
 * </i></p>
 * 
 * @author Gr&eacute;gory Mantelet (CDS;ARI)
 * @version 2.1 (07/2016)
 */
public class TAPSchema implements Iterable<TAPTable> {

	/** Name that this schema MUST have in ADQL queries. */
	private final String adqlName;

	/** Indicates whether the given ADQL name must be simplified by {@link #getADQLName()}.
	 * <p>Here, "simplification" means removing the surrounding double quotes.
	 * Since there is no information on the catalog name, none can be detected and so removed.</p>
	 * @since 2.1 */
	private final boolean simplificationNeeded;

	/** Name that this schema have in the database.
	 * <i>Note: NULL by default. When NULL, {@link #getDBName()} returns exactly what {@link #getADQLName()} returns.</i> */
	private String dbName = null;

	/** Descriptive, human-interpretable name of the schema.
	 * <i>Note: Standard TAP schema field ; MAY be NULL.</i>
	 * @since 2.0 */
	private String title = null;

	/** Description of this schema.
	 * <i>Note: Standard TAP schema field ; MAY be NULL.</i> */
	private String description = null;

	/** UType describing the scientific content of this schema.
	 * <i>Note: Standard TAP schema field ; MAY be NULL.</i> */
	private String utype = null;

	/** Ordering index of this schema inside its whole schema set.
	 * <i>Note: SHOULD be a standard TAP schema field in TAP 1.1, as table_index and column_index are resp. in TAP_SCHEMA.tables and TAP_SCHEMA.columns.</i>
	 * @since 2.1 */
	private int index = -1;

	/** Let add some information in addition of the ones of the TAP protocol.
	 * <i>Note: This object can be anything: an {@link Integer}, a {@link String}, a {@link Map}, a {@link List}, ...
	 * Its content is totally free and never used or checked.</i> */
	protected Object otherData = null;

	/** List all tables contained inside this schema. */
	protected final Map<String,TAPTable> tables;

	/**
	 * <p>Build a {@link TAPSchema} instance with the given ADQL name.</p>
	 * 
	 * <p><i>Note:
	 * 	The DB name is set by default to the ADQL name (as returned by {@link #getADQLName()}).
	 * 	To set the DB name, you MUST call then {@link #setDBName(String)}.
	 * </i></p>
	 * 
	 * <p><b>Important notes on the given ADQL name:</b></p>
	 * <ul>
	 * 	<li>Any leading or trailing space is immediately deleted.</li>
	 * 	<li>
	 * 		No catalog prefix is supported. <i>For instance: <code>myCatalog.mySchema</code> will be considered
	 * 		as the schema name instead of <code>mySchema</code>.</i>
	 *	</li>
	 * 	<li>
	 * 		Double quotes may surround the ADQL name. They will be removed by {@link #getADQLName()} but will
	 * 		still appear in the result of {@link #getRawName()}.
	 *	</li>
	 * </ul>
	 * 
	 * @param schemaName	Name that this schema MUST have in ADQL queries.
	 *                  	<i>CAN'T be NULL ; this name can never be changed after initialization.</i>
	 * 
	 * @throws NullPointerException	If the given name is <code>null</code>,
	 *                             	or if the given string is empty after simplification
	 *                             	(i.e. without the surrounding double quotes).
	 */
	public TAPSchema(String schemaName) throws NullPointerException{
		if (schemaName == null)
			throw new NullPointerException("Missing schema name!");

		adqlName = schemaName.trim();
		simplificationNeeded = adqlName.matches("\"[^\"]*\"");

		if (getADQLName().length() == 0)
			throw new NullPointerException("Missing schema name!");

		dbName = getADQLName();

		tables = new LinkedHashMap<String,TAPTable>();
	}

	/**
	 * <p>Build a {@link TAPSchema} instance with the given ADQL name and description.</p>
	 * 
	 * <p><i>Note:
	 * 	The DB name is set by default to the ADQL name (as returned by {@link #getADQLName()}).
	 * 	To set the DB name, you MUST call then {@link #setDBName(String)}.
	 * </i></p>
	 * 
	 * <p><b>Important notes on the given ADQL name:</b></p>
	 * <ul>
	 * 	<li>Any leading or trailing space is immediately deleted.</li>
	 * 	<li>
	 * 		No catalog prefix is supported. <i>For instance: <code>myCatalog.mySchema</code> will be considered
	 * 		as the schema name instead of <code>mySchema</code>.</i>
	 *	</li>
	 * 	<li>
	 * 		Double quotes may surround the ADQL name. They will be removed by {@link #getADQLName()} but will
	 * 		still appear in the result of {@link #getRawName()}.
	 *	</li>
	 * </ul>
	 * 
	 * @param schemaName	Name that this schema MUST have in ADQL queries.
	 *                  	<i>CAN'T be NULL ; this name can never be changed after initialization.</i>
	 * @param description	Description of this schema. <i>MAY be NULL</i>
	 * 
	 * @throws NullPointerException	If the given name is <code>null</code>,
	 *                             	or if the given string is empty after simplification
	 *                             	(i.e. without the surrounding double quotes).
	 */
	public TAPSchema(String schemaName, String description) throws NullPointerException{
		this(schemaName, description, null);
	}

	/**
	 * <p>Build a {@link TAPSchema} instance with the given ADQL name, description and UType.</p>
	 * 
	 * <p><i>Note:
	 * 	The DB name is set by default to the ADQL name (as returned by {@link #getADQLName()}).
	 * 	To set the DB name, you MUST call then {@link #setDBName(String)}.
	 * </i></p>
	 * 
	 * <p><b>Important notes on the given ADQL name:</b></p>
	 * <ul>
	 * 	<li>Any leading or trailing space is immediately deleted.</li>
	 * 	<li>
	 * 		No catalog prefix is supported. <i>For instance: <code>myCatalog.mySchema</code> will be considered
	 * 		as the schema name instead of <code>mySchema</code>.</i>
	 *	</li>
	 * 	<li>
	 * 		Double quotes may surround the ADQL name. They will be removed by {@link #getADQLName()} but will
	 * 		still appear in the result of {@link #getRawName()}.
	 *	</li>
	 * </ul>
	 * 
	 * @param schemaName	Name that this schema MUST have in ADQL queries.
	 *                  	<i>CAN'T be NULL ; this name can never be changed after initialization.</i>
	 * @param description	Description of this schema. <i>MAY be NULL</i>
	 * @param utype			UType associating this schema with a data-model. <i>MAY be NULL</i>
	 * 
	 * @throws NullPointerException	If the given name is <code>null</code>,
	 *                             	or if the given string is empty after simplification
	 *                             	(i.e. without the surrounding double quotes).
	 */
	public TAPSchema(String schemaName, String description, String utype) throws NullPointerException{
		this(schemaName);
		this.description = description;
		this.utype = utype;
	}

	/**
	 * Get the ADQL name (the name this schema MUST have in ADQL queries).
	 * 
	 * @return	Its ADQL name.
	 * @see #getADQLName()
	 * @deprecated	Does not do anything special: just call {@link #getADQLName()}.
	 */
	@Deprecated
	public final String getName(){
		return getADQLName();
	}

	/**
	 * Get the name of this schema.
	 * 
	 * <p><i>Note:
	 * 	If a simplification is needed, the double quotes surrounding the ADQL name, will be removed.
	 * </i></p>
	 * 
	 * @return	Its ADQL name. <i>CAN'T be NULL</i>
	 */
	public final String getADQLName(){
		return simplificationNeeded ? adqlName.substring(1, adqlName.length() - 1) : adqlName;
	}

	/**
	 * Get the full ADQL name of this schema, as it has been provided at initialization.
	 * 
	 * @return	Get the original ADQL name.
	 * 
	 * @since 2.1
	 */
	public final String getRawName(){
		return adqlName;
	}

	/**
	 * Get the name this schema MUST have in the database.
	 * 
	 * @return	Its DB name. <i>MAY be NULL</i>
	 */
	public final String getDBName(){
		return dbName;
	}

	/**
	 * Set the name this schema MUST have in the database.
	 * 
	 * <p>Notes:</p>
	 * <ul>
	 * 	<li>The given name may be NULL. In such case {@link #getDBName()} will then return NULL.</li>
	 * 	<li>It may be prefixed by a catalog name.</li>
	 * 	<li>
	 * 		It MUST be NON delimited/double-quoted. Otherwise an SQL error will be raised when
	 * 		querying any item of this schema because the library double-quotes systematically the
	 * 		DB name of schemas, tables and columns.
	 *	</li>
	 * </ul>
	 * 
	 * @param name	Its new DB name. <i>MAY be NULL</i>
	 */
	public final void setDBName(String name){
		name = (name != null) ? name.trim() : name;
		dbName = name;
	}

	/**
	 * Get the title of this schema.
	 * 
	 * @return	Its title. <i>MAY be NULL</i>
	 * 
	 * @since 2.0
	 */
	public final String getTitle(){
		return title;
	}

	/**
	 * Set the title of this schema.
	 * 
	 * @param title	Its new title. <i>MAY be NULL</i>
	 * 
	 * @since 2.0
	 */
	public final void setTitle(final String title){
		this.title = title;
	}

	/**
	 * Get the description of this schema.
	 * 
	 * @return	Its description. <i>MAY be NULL</i>
	 */
	public final String getDescription(){
		return description;
	}

	/**
	 * Set the description of this schema.
	 * 
	 * @param description	Its new description. <i>MAY be NULL</i>
	 */
	public final void setDescription(String description){
		this.description = description;
	}

	/**
	 * Get the UType associating this schema with a data-model.
	 * 
	 * @return	Its UType. <i>MAY be NULL</i>
	 */
	public final String getUtype(){
		return utype;
	}

	/**
	 * Set the UType associating this schema with a data-model.
	 * 
	 * @param utype	Its new UType. <i>MAY be NULL</i>
	 */
	public final void setUtype(String utype){
		this.utype = utype;
	}

	/**
	 * Get the ordering index of this schema inside its whole schema set.
	 * 
	 * @return	Its ordering index.
	 * 
	 * @since 2.1
	 */
	public final int getIndex(){
		return index;
	}

	/**
	 * Set the ordering index of this schema inside its whole schema set.
	 * 
	 * @param schemaIndex	Its new ordering index.
	 * 
	 * @since 2.1
	 */
	public final void setIndex(int schemaIndex){
		this.index = schemaIndex;
	}

	/**
	 * <p>Get the other (piece of) information associated with this schema.</p>
	 * 
	 * <p><i>Note:
	 * 	By default, NULL is returned, but it may be any kind of value ({@link Integer},
	 * 	{@link String}, {@link Map}, {@link List}, ...).
	 * </i></p>
	 * 
	 * @return	The other (piece of) information. <i>MAY be NULL</i>
	 */
	public Object getOtherData(){
		return otherData;
	}

	/**
	 * Set the other (piece of) information associated with this schema.
	 * 
	 * @param data	Another information about this schema. <i>MAY be NULL</i>
	 */
	public void setOtherData(Object data){
		otherData = data;
	}

	/**
	 * <p>Add the given table inside this schema.</p>
	 * 
	 * <p><i>Note:
	 * 	If the given table is NULL, nothing will be done.
	 * </i></p>
	 * 
	 * <p><i><b>Important note:</b>
	 * 	By adding the given table inside this schema, it
	 * 	will be linked with this schema using {@link TAPTable#setSchema(TAPSchema)}.
	 * 	In this function, if the table was already linked with another {@link TAPSchema},
	 * 	the former link is removed using {@link TAPSchema#removeTable(String)}.
	 * </i></p>
	 * 
	 * @param newTable	Table to add inside this schema.
	 */
	public final void addTable(TAPTable newTable){
		if (newTable != null && newTable.getADQLName() != null){
			newTable.setSchema(this);
			tables.put(newTable.getADQLName(), newTable);
		}
	}

	/**
	 * <p>Build a {@link TAPTable} object whose the ADQL and DB name will the given one.
	 * Then, add this table inside this schema.</p>
	 * 
	 * <p><i>Note:
	 * 	The built {@link TAPTable} object is returned, so that being modified afterwards if needed.
	 * </i></p>
	 * 
	 * @param tableName	ADQL name (and indirectly also the DB name) of the table to create and add.
	 * 
	 * @return	The created and added {@link TAPTable} object,
	 *        	or NULL if the given name is NULL or an empty string.
	 * 
	 * @see TAPTable#TAPTable(String)
	 * @see #addTable(TAPTable)
	 */
	public TAPTable addTable(String tableName){
		if (tableName == null)
			return null;

		TAPTable t = new TAPTable(tableName);
		addTable(t);
		return t;
	}

	/**
	 * <p>Build a {@link TAPTable} object whose the ADQL and DB name will the given one.
	 * Then, add this table inside this schema.</p>
	 * 
	 * <p><i>Note:
	 * 	The built {@link TAPTable} object is returned, so that being modified afterwards if needed.
	 * </i></p>
	 * 
	 * @param tableName		ADQL name (and indirectly also the DB name) of the table to create and add.
	 * @param tableType		Type of the new table. <i>If NULL, "table" will be the type of the created table.</i>
	 * @param description	Description of the new table. <i>MAY be NULL</i>
	 * @param utype			UType associating the new column with a data-model. <i>MAY be NULL</i>
	 * 
	 * @return	The created and added {@link TAPTable} object,
	 *        	or NULL if the given name is NULL or an empty string.
	 * 
	 * @see TAPTable#TAPTable(String, TableType, String, String)
	 * @see #addTable(TAPTable)
	 */
	public TAPTable addTable(String tableName, TableType tableType, String description, String utype){
		if (tableName == null)
			return null;

		TAPTable t = new TAPTable(tableName, tableType, description, utype);
		addTable(t);

		return t;
	}

	/**
	 * <p>Tell whether this schema contains a table having the given ADQL name.</p>
	 * 
	 * <p><i><b>Important note:</b>
	 * 	This function is case sensitive!
	 * </i></p>
	 * 
	 * @param tableName	Name of the table whose the existence in this schema must be checked.
	 * 
	 * @return	<i>true</i> if a table with the given ADQL name exists, <i>false</i> otherwise.
	 */
	public final boolean hasTable(String tableName){
		if (tableName == null)
			return false;
		else
			return tables.containsKey(tableName);
	}

	/**
	 * <p>Search for a table having the given ADQL name.</p>
	 * 
	 * <p><i><b>Important note:</b>
	 * 	This function is case sensitive!
	 * </i></p>
	 * 
	 * @param tableName	ADQL name of the table to search.
	 * 
	 * @return	The table having the given ADQL name,
	 *        	or NULL if no such table can be found.
	 */
	public final TAPTable getTable(String tableName){
		if (tableName == null)
			return null;
		else
			return tables.get(tableName);
	}

	/**
	 * Get the number of all tables contained inside this schema.
	 * 
	 * @return	Number of its tables.
	 */
	public final int getNbTables(){
		return tables.size();
	}

	/**
	 * Tell whether this schema contains no table.
	 * 
	 * @return	<i>true</i> if this schema contains no table,
	 *        	<i>false</i> if it has at least one table.
	 */
	public final boolean isEmpty(){
		return tables.isEmpty();
	}

	/**
	 * <p>Remove the table having the given ADQL name.</p>
	 * 
	 * <p><i><b>Important note:</b>
	 * 	This function is case sensitive!
	 * </i></p>
	 * 
	 * <p><i>Note:
	 * 	If the specified table is removed, its schema link is also deleted.
	 * </i></p>
	 * 
	 * <p><i><b>WARNING:</b>
	 * 	If the goal of this function's call is to delete definitely the specified table
	 * 	from the metadata, you SHOULD also call {@link TAPTable#removeAllForeignKeys()}.
	 * 	Indeed, foreign keys of the table would still link the removed table with other tables
	 * 	AND columns of the whole metadata set.
	 * </i></p>
	 * 
	 * @param tableName	ADQL name of the table to remove from this schema.
	 * 
	 * @return	The removed table,
	 *        	or NULL if no table with the given ADQL name can be found.
	 */
	public final TAPTable removeTable(String tableName){
		if (tableName == null)
			return null;

		TAPTable removedTable = tables.remove(tableName);
		if (removedTable != null)
			removedTable.setSchema(null);
		return removedTable;
	}

	/**
	 * <p>Remove all the tables contained inside this schema.</p>
	 * 
	 * <p><i>Note:
	 * 	When a table is removed, its schema link is also deleted.
	 * </i></p>
	 * 
	 * <p><b>CAUTION:
	 * 	If the goal of this function's call is to delete definitely all the tables of this schema
	 * 	from the metadata, you SHOULD also call {@link TAPTable#removeAllForeignKeys()}
	 * 	on all tables before calling this function.
	 * 	Indeed, foreign keys of the tables would still link the removed tables with other tables
	 * 	AND columns of the whole metadata set.
	 * </b></p>
	 */
	public final void removeAllTables(){
		Iterator<Map.Entry<String,TAPTable>> it = tables.entrySet().iterator();
		while(it.hasNext()){
			Map.Entry<String,TAPTable> entry = it.next();
			it.remove();
			entry.getValue().setSchema(null);
		}
	}

	@Override
	public Iterator<TAPTable> iterator(){
		return tables.values().iterator();
	}

	@Override
	public String toString(){
		return getADQLName();
	}

}
