package tap.metadata;

import adql.db.DBType;
import adql.db.DBType.DBDatatype;

/*
 * 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-2017 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
 *                       Astronomisches Rechen Institut (ARI)
 */

import tap.TAPException;
import uk.ac.starlink.votable.VOSerializer;

/**
 * <p>Describes a full VOTable type. Thus it includes the following field attributes:</p>
 * <ul>
 * 	<li><code>datatype</code>,</li>
 * 	<li><code>arraysize</code>,</li>
 * 	<li><code>xtype</code>.</li>
 * </ul>
 * 
 * @author Gr&eacute;gory Mantelet (CDS;ARI)
 * @version 2.1 (03/2017)
 */
public final class VotType {
	/**
	 * All possible values for a VOTable datatype (i.e. boolean, short, char, ...).
	 * 
	 * @author Gr&eacute;gory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de
	 * @version 2.0 (01/2015)
	 * @since 2.0
	 */
	public static enum VotDatatype{
		BOOLEAN("boolean"), BIT("bit"), UNSIGNEDBYTE("unsignedByte"), SHORT("short"), INT("int"), LONG("long"), CHAR("char"), UNICODECHAR("unicodeChar"), FLOAT("float"), DOUBLE("double"), FLOATCOMPLEX("floatComplex"), DOUBLECOMPLEX("doubleComplex");

		private final String strExpr;

		private VotDatatype(final String str){
			strExpr = (str == null || str.trim().length() == 0) ? name() : str;
		}

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

	/** Special VOTable type (XType) for TAP/DB type BLOB.
	 * @since 2.0*/
	public final static String XTYPE_BLOB = "adql:BLOB";
	/** Special VOTable type (XType) for TAP/DB type CLOB.
	 * @since 2.0 */
	public final static String XTYPE_CLOB = "adql:CLOB";
	/** Special VOTable type (XType) for TAP/DB type TIMESTAMP.
	 * @since 2.0 */
	public final static String XTYPE_TIMESTAMP = "adql:TIMESTAMP";
	/** Special VOTable type (XType) for TAP/DB type POINT.
	 * @since 2.0 */
	public final static String XTYPE_POINT = "adql:POINT";
	/** Special VOTable type (XType) for TAP/DB type REGION.
	 * @since 2.0 */
	public final static String XTYPE_REGION = "adql:REGION";

	/** VOTable datatype
	 * @since 2.0 */
	public final VotDatatype datatype;
	/** Arraysize string of a VOTable field element. */
	public final String arraysize;
	/** Special type specification (i.e. POINT, TIMESTAMP, ...). */
	public final String xtype;

	/**
	 * Build a VOTable field type.
	 * 
	 * @param datatype		A datatype. <b>Null value forbidden</b>
	 * @param arraysize		VOTable arraysize string (<i>may be NULL</i>).
	 */
	public VotType(final VotDatatype datatype, final String arraysize){
		this(datatype, arraysize, null);
	}

	/**
	 * Build a VOTable field type.
	 * 
	 * @param datatype		A datatype. <b>Null value forbidden</b>
	 * @param arraysize		VOTable arraysize string (<i>may be NULL</i>).
	 * @param xtype			A special type (ex: adql:POINT, adql:TIMESTAMP, ...). (<i>may be NULL</i>).
	 */
	public VotType(final VotDatatype datatype, final String arraysize, final String xtype){
		// set the datatype:
		if (datatype == null)
			throw new NullPointerException("missing VOTable datatype !");
		else
			this.datatype = datatype;

		// set the array-size:
		if (arraysize != null && arraysize.trim().length() > 0)
			this.arraysize = arraysize.trim();
		else
			this.arraysize = null;

		// set the xtype:
		if (xtype != null && xtype.trim().length() > 0)
			this.xtype = xtype.trim();
		else
			this.xtype = null;
	}

	/**
	 * Build a {@link VotType} object by converting the given {@link DBType}.
	 * 
	 * @param tapType	{@link DBType} to convert.
	 */
	public VotType(final DBType tapType){
		switch(tapType.type){
			case SMALLINT:
				this.datatype = VotDatatype.SHORT;
				this.arraysize = null;
				this.xtype = null;
				break;

			case INTEGER:
				this.datatype = VotDatatype.INT;
				this.arraysize = null;
				this.xtype = null;
				break;

			case BIGINT:
				this.datatype = VotDatatype.LONG;
				this.arraysize = null;
				this.xtype = null;
				break;

			case REAL:
				this.datatype = VotDatatype.FLOAT;
				this.arraysize = null;
				this.xtype = null;
				break;

			case DOUBLE:
				this.datatype = VotDatatype.DOUBLE;
				this.arraysize = null;
				this.xtype = null;
				break;

			case CHAR:
				this.datatype = VotDatatype.CHAR;
				this.arraysize = tapType.length > 0 ? Integer.toString(tapType.length) : null;
				this.xtype = null;
				break;

			case BINARY:
				this.datatype = VotDatatype.UNSIGNEDBYTE;
				this.arraysize = tapType.length > 0 ? Integer.toString(tapType.length) : null;
				this.xtype = null;
				break;

			case VARBINARY:
				/* TODO HOW TO MANAGE VALUES WHICH WHERE ORIGINALLY NUMERIC ARRAYS ?
				 * (cf the IVOA document TAP#Upload: votable numeric arrays should be converted into VARBINARY...no more array information and particularly the datatype)
				 */
				this.datatype = VotDatatype.UNSIGNEDBYTE;
				this.arraysize = (tapType.length > 0 ? tapType.length + "*" : "*");
				this.xtype = null;
				break;

			case BLOB:
				this.datatype = VotDatatype.UNSIGNEDBYTE;
				this.arraysize = "*";
				this.xtype = VotType.XTYPE_BLOB;
				break;

			case CLOB:
				this.datatype = VotDatatype.CHAR;
				this.arraysize = "*";
				this.xtype = VotType.XTYPE_CLOB;
				break;

			case TIMESTAMP:
				this.datatype = VotDatatype.CHAR;
				this.arraysize = "*";
				this.xtype = VotType.XTYPE_TIMESTAMP;
				break;

			case POINT:
				this.datatype = VotDatatype.CHAR;
				this.arraysize = "*";
				this.xtype = VotType.XTYPE_POINT;
				break;

			case REGION:
				this.datatype = VotDatatype.CHAR;
				this.arraysize = "*";
				this.xtype = VotType.XTYPE_REGION;
				break;

			case VARCHAR:
			default:
				this.datatype = VotDatatype.CHAR;
				this.arraysize = (tapType.length > 0 ? tapType.length + "*" : "*");
				this.xtype = null;
				break;
		}
	}

	@Override
	public boolean equals(Object obj){
		if (obj == null)
			return false;
		try{
			return toString().equals(obj);
		}catch(ClassCastException cce){
			;
		}
		return false;
	}

	@Override
	public int hashCode(){
		return datatype.toString().hashCode();
	}

	@Override
	public String toString(){
		StringBuffer str = new StringBuffer(VOSerializer.formatAttribute("datatype", datatype.toString()));
		str.deleteCharAt(0);

		if (arraysize != null)
			str.append(VOSerializer.formatAttribute("arraysize", arraysize));

		if (xtype != null)
			str.append(VOSerializer.formatAttribute("xtype", xtype));

		return str.toString();
	}

	/**
	 * Convert this VOTable type definition into a TAPColumn type.
	 * 
	 * @return	The corresponding {@link DBType}.
	 * 
	 * @throws TAPException	If the conversion is impossible (particularly if the array-size refers to a multi-dimensional array ; only 1D arrays are allowed).
	 */
	public DBType toTAPType() throws TAPException{

		/* Stop immediately if the arraysize refers to a multi-dimensional array:
		 * (Note: 'x' is the dimension separator of the VOTable attribute 'arraysize') */
		if (arraysize != null && arraysize.indexOf('x') >= 0)
			throw new TAPException("failed conversion of a VOTable datatype: multi-dimensional arrays (" + datatype + "[" + arraysize + "]) are not allowed!");

		// Convert the VOTable datatype into TAP datatype:
		switch(datatype){
			/* NUMERIC TYPES */
			case SHORT:
			case BOOLEAN:
				return convertNumericType(DBDatatype.SMALLINT);

			case INT:
				return convertNumericType(DBDatatype.INTEGER);

			case LONG:
				return convertNumericType(DBDatatype.BIGINT);

			case FLOAT:
				return convertNumericType(DBDatatype.REAL);

			case DOUBLE:
				return convertNumericType(DBDatatype.DOUBLE);

			/* BINARY TYPES */
			case UNSIGNEDBYTE:
				// BLOB exception:
				if (xtype != null && xtype.equalsIgnoreCase(XTYPE_BLOB))
					return new DBType(DBDatatype.BLOB);

				// Or else, just (var)binary:
				else
					return convertVariableLengthType(DBDatatype.VARBINARY, DBDatatype.BINARY);

				/* CHARACTER TYPES */
			case CHAR:
			default:
				/* Special type cases: */
				if (xtype != null){
					if (xtype.equalsIgnoreCase(VotType.XTYPE_CLOB))
						return new DBType(DBDatatype.CLOB);
					else if (xtype.equalsIgnoreCase(VotType.XTYPE_TIMESTAMP))
						return new DBType(DBDatatype.TIMESTAMP);
					else if (xtype.equalsIgnoreCase(VotType.XTYPE_POINT))
						return new DBType(DBDatatype.POINT);
					else if (xtype.equalsIgnoreCase(VotType.XTYPE_REGION))
						return new DBType(DBDatatype.REGION);
				}

				// Or if not known or missing, just a (var)char:
				return convertVariableLengthType(DBDatatype.VARCHAR, DBDatatype.CHAR);
		}
	}

	/**
	 * <p>Convert this numeric {@link VotType} object into a corresponding {@link DBType} whose the datatype is provided in parameter.</p>
	 * 
	 * <p>
	 * 	Thus, just the arraysize must be managed here. If there is no arraysize or if equals to '1', the given datatype will be used.
	 * 	Otherwise, it is ignored and a {@link DBType} with VARBINARY is returned.
	 * </p>
	 * 
	 * @param tapDatatype	TAP datatype corresponding to this {@link VotType} (only when arraysize != '*' and 'n').
	 * 
	 * @return	The corresponding {@link DBType}.
	 */
	protected DBType convertNumericType(final DBDatatype tapDatatype){
		// If no arraysize:
		if (arraysize == null || arraysize.equals("1"))
			return new DBType(tapDatatype);

		// If only one dimension:
		else
			return new DBType(DBDatatype.VARBINARY);

		/* Note: The test of multi-dimensional array should have been already done at the beginning of #toTAPType(). */
	}

	/**
	 * <p>
	 * 	Convert this variable length {@link VotType} (unsignedByte and char) object into a corresponding {@link DBType}
	 * 	whose the variable length and fixed length versions are given in parameters.
	 * </p>
	 * 
	 * <p>Thus, just the arraysize must be managed here. The following cases are taken into account:</p>
	 * <ul>
	 * 	<li><i>No arraysize or '*'</i>: variable length type (i.e. VARCHAR, VARBINARY),</li>
	 * 	<li><i>'n*'</i>: variable length type with the maximal length (i.e. VARCHAR(n), VARBINARY(n)),</li>
	 * 	<li><i>'n'</i>: fixed length type with the exact length (i.e. CHAR(n), BINARY(n)).</li>
	 * </ul>
	 * 
	 * @param varType		Variable length type (i.e. VARCHAR, VARBINARY).
	 * @param fixedType		Fixed length type (i.e. CHAR, BINARY).
	 * 
	 * @return	The corresponding {@link DBType}.
	 * 
	 * @throws TAPException	If the arraysize is not valid (that's to say, different from the following syntaxes: NULL, '*', 'n' or 'n*' (where n is a positive and not-null integer)).
	 */
	protected DBType convertVariableLengthType(final DBDatatype varType, final DBDatatype fixedType) throws TAPException{
		try{
			// no arraysize or '*' => VARCHAR or VARBINARY
			if (arraysize == null || arraysize.equals("*"))
				return new DBType(varType);

			// 'n*' => VARCHAR(n) or VARBINARY(n)
			else if (arraysize.charAt(arraysize.length() - 1) == '*')
				return new DBType(varType, Integer.parseInt(arraysize.substring(0, arraysize.length() - 1)));

			// 'n' => CHAR(n) or BINARY(n)
			else
				return new DBType(fixedType, Integer.parseInt(arraysize));

		}catch(NumberFormatException nfe){
			throw new TAPException("failed conversion of a VOTable datatype: non-numeric arraysize (" + arraysize + ")!");
		}
	}

}
