/*
 * Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
 * Version 1.0, and under the Eclipse Public License, Version 1.0
 * (http://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.value;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.sql.Array;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.UUID;

import org.h2.constant.ErrorCode;
import org.h2.constant.SysProperties;
import org.h2.engine.SessionInterface;
import org.h2.jdbc.JdbcBlob;
import org.h2.jdbc.JdbcClob;
import org.h2.jdbc.JdbcConnection;
import org.h2.message.DbException;
import org.h2.store.LobStorage;
import org.h2.util.Utils;
import org.h2.util.New;
import org.h2.util.StringUtils;

/**
 * This class contains meta data information about data types,
 * and can convert between Java objects and Values.
 */
public class DataType {

    /**
     * This constant is used for JDK 1.3 compatibility
     * and equal to java.sql.Types.BOOLEAN
     */
    public static final int TYPE_BOOLEAN = 16;

    /**
     * This constant is used for JDK 1.3 compatibility
     * and equal to java.sql.Types.DATALINK
     */
    public static final int TYPE_DATALINK = 70;

    /**
     * This constant is used for JDK 1.5 compatibility
     * and equal to java.sql.Types.LONGNVARCHAR
     */
    public static final int TYPE_LONGNVARCHAR = -16;

    /**
     * This constant is used for JDK 1.5 compatibility
     * and equal to java.sql.Types.NCHAR
     */
    public static final int TYPE_NCHAR = -15;

    /**
     * This constant is used for JDK 1.5 compatibility
     * and equal to java.sql.Types.NVARCHAR
     */
    public static final int TYPE_NVARCHAR = -9;

    /**
     * This constant is used for JDK 1.5 compatibility
     * and equal to java.sql.Types.NCLOB
     */
    public static final int TYPE_NCLOB = 2011;

    /**
     * The list of types. An ArrayList so that Tomcat doesn't set it to null
     * when clearing references.
     */
    private static final ArrayList<DataType> TYPES = New.arrayList();
    private static final HashMap<String, DataType> TYPES_BY_NAME = New.hashMap();
    private static final ArrayList<DataType> TYPES_BY_VALUE_TYPE = New.arrayList();

    /**
     * The value type of this data type.
     */
    public int type;

    /**
     * The data type name.
     */
    public String name;

    /**
     * The SQL type.
     */
    public int sqlType;

    /**
     * The Java class name.
     */
    public String jdbc;

    /**
     * How closely the data type maps to the corresponding JDBC SQL type (low is
     * best).
     */
    public int sqlTypePos;

    /**
     * The maximum supported precision.
     */
    public long maxPrecision;

    /**
     * The lowest possible scale.
     */
    public int minScale;

    /**
     * The highest possible scale.
     */
    public int maxScale;

    /**
     * If this is a numeric type.
     */
    public boolean decimal;

    /**
     * The prefix required for the SQL literal representation.
     */
    public String prefix;

    /**
     * The suffix required for the SQL literal representation.
     */
    public String suffix;

    /**
     * The list of parameters used in the column definition.
     */
    public String params;

    /**
     * If this is an autoincrement type.
     */
    public boolean autoIncrement;

    /**
     * If this data type is an autoincrement type.
     */
    public boolean caseSensitive;

    /**
     * If the precision parameter is supported.
     */
    public boolean supportsPrecision;

    /**
     * If the scale parameter is supported.
     */
    public boolean supportsScale;

    /**
     * The default precision.
     */
    public long defaultPrecision;

    /**
     * The default scale.
     */
    public int defaultScale;

    /**
     * The default display size.
     */
    public int defaultDisplaySize;

    /**
     * If this data type should not be listed in the database meta data.
     */
    public boolean hidden;

    /**
     * The number of bytes required for an object.
     */
    public int memory;

    static {
        for (int i = 0; i < Value.TYPE_COUNT; i++) {
            TYPES_BY_VALUE_TYPE.add(null);
        }
        add(Value.NULL, Types.NULL, "Null",
                new DataType(),
                new String[]{"NULL"},
                // the value is always in the cache
                0
        );
        add(Value.STRING, Types.VARCHAR, "String",
                createString(true),
                new String[]{"VARCHAR", "VARCHAR2", "NVARCHAR", "NVARCHAR2", "VARCHAR_CASESENSITIVE", "CHARACTER VARYING", "TID"},
                // 24 for ValueString, 24 for String
                48
        );
        add(Value.STRING, Types.LONGVARCHAR, "String",
                createString(true),
                new String[]{"LONGVARCHAR", "LONGNVARCHAR"},
                48
        );
        add(Value.STRING_FIXED, Types.CHAR, "String",
                createString(true),
                new String[]{"CHAR", "CHARACTER", "NCHAR"},
                48
        );
        add(Value.STRING_IGNORECASE, Types.VARCHAR, "String",
                createString(false),
                new String[]{"VARCHAR_IGNORECASE"},
                48
        );
        add(Value.BOOLEAN, DataType.TYPE_BOOLEAN, "Boolean",
                createDecimal(ValueBoolean.PRECISION, ValueBoolean.PRECISION, 0, ValueBoolean.DISPLAY_SIZE, false, false),
                new String[]{"BOOLEAN", "BIT", "BOOL"},
                // the value is always in the cache
                0
        );
        add(Value.BYTE, Types.TINYINT, "Byte",
                createDecimal(ValueByte.PRECISION, ValueByte.PRECISION, 0, ValueByte.DISPLAY_SIZE, false, false),
                new String[]{"TINYINT"},
                // the value is almost always in the cache
                1
        );
        add(Value.SHORT, Types.SMALLINT, "Short",
                createDecimal(ValueShort.PRECISION, ValueShort.PRECISION, 0, ValueShort.DISPLAY_SIZE, false, false),
                new String[]{"SMALLINT", "YEAR", "INT2"},
                // in many cases the value is in the cache
                20
        );
        add(Value.INT, Types.INTEGER, "Int",
                createDecimal(ValueInt.PRECISION, ValueInt.PRECISION, 0, ValueInt.DISPLAY_SIZE, false, false),
                new String[]{"INTEGER", "INT", "MEDIUMINT", "INT4", "SIGNED"},
                // in many cases the value is in the cache
                20
        );
        add(Value.LONG, Types.BIGINT, "Long",
                createDecimal(ValueLong.PRECISION, ValueLong.PRECISION, 0, ValueLong.DISPLAY_SIZE, false, false),
                new String[]{"BIGINT", "INT8", "LONG"},
                24
        );
        add(Value.LONG, Types.BIGINT, "Long",
                createDecimal(ValueLong.PRECISION, ValueLong.PRECISION, 0, ValueLong.DISPLAY_SIZE, false, true),
                new String[]{"IDENTITY", "SERIAL"},
                24
        );
        add(Value.DECIMAL, Types.DECIMAL, "BigDecimal",
                createDecimal(Integer.MAX_VALUE, ValueDecimal.DEFAULT_PRECISION, ValueDecimal.DEFAULT_SCALE, ValueDecimal.DEFAULT_DISPLAY_SIZE, true, false),
                new String[]{"DECIMAL", "DEC"},
                // 40 for ValueDecimal,
                64
        );
        add(Value.DECIMAL, Types.NUMERIC, "BigDecimal",
                createDecimal(Integer.MAX_VALUE, ValueDecimal.DEFAULT_PRECISION, ValueDecimal.DEFAULT_SCALE, ValueDecimal.DEFAULT_DISPLAY_SIZE, true, false),
                new String[]{"NUMERIC", "NUMBER"},
                64
        );
        add(Value.FLOAT, Types.REAL, "Float",
                createDecimal(ValueFloat.PRECISION, ValueFloat.PRECISION, 0, ValueFloat.DISPLAY_SIZE, false, false),
                new String[] {"REAL", "FLOAT4"},
                24
        );
        add(Value.DOUBLE, Types.DOUBLE, "Double",
                createDecimal(ValueDouble.PRECISION, ValueDouble.PRECISION, 0, ValueDouble.DISPLAY_SIZE, false, false),
                new String[] { "DOUBLE", "DOUBLE PRECISION" },
                24
        );
        add(Value.DOUBLE, Types.FLOAT, "Double",
                createDecimal(ValueDouble.PRECISION, ValueDouble.PRECISION, 0, ValueDouble.DISPLAY_SIZE, false, false),
                new String[] {"FLOAT", "FLOAT8" },
                24
        );
        add(Value.TIME, Types.TIME, "Time",
                createDate(ValueTime.PRECISION, "TIME", 0, ValueTime.DISPLAY_SIZE),
                new String[]{"TIME"},
                // 24 for ValueTime, 32 for java.sql.Time
                56
        );
        add(Value.DATE, Types.DATE, "Date",
                createDate(ValueDate.PRECISION, "DATE", 0, ValueDate.DISPLAY_SIZE),
                new String[]{"DATE"},
                // 24 for ValueDate, 32 for java.sql.Data
                56
        );
        add(Value.TIMESTAMP, Types.TIMESTAMP, "Timestamp",
                createDate(ValueTimestamp.PRECISION, "TIMESTAMP", ValueTimestamp.DEFAULT_SCALE, ValueTimestamp.DISPLAY_SIZE),
                new String[]{"TIMESTAMP", "DATETIME", "SMALLDATETIME"},
                // 24 for ValueTimestamp, 32 for java.sql.Timestamp
                56
        );
        add(Value.BYTES, Types.VARBINARY, "Bytes",
                createString(false),
                new String[]{"VARBINARY"},
                32
        );
        add(Value.BYTES, Types.BINARY, "Bytes",
                createString(false),
                new String[]{"BINARY", "RAW", "BYTEA", "LONG RAW"},
                32
        );
        add(Value.BYTES, Types.LONGVARBINARY, "Bytes",
                createString(false),
                new String[]{"LONGVARBINARY"},
                32
        );
        add(Value.UUID, Types.BINARY, "Bytes",
                createString(false),
                new String[]{"UUID"},
                32
        );
        add(Value.JAVA_OBJECT, Types.OTHER, "Object",
                createString(false),
                new String[]{"OTHER", "OBJECT", "JAVA_OBJECT"},
                24
        );
        add(Value.BLOB, Types.BLOB, "Blob",
                createLob(),
                new String[]{"BLOB", "TINYBLOB", "MEDIUMBLOB", "LONGBLOB", "IMAGE", "OID"},
                // 80 for ValueLob, 24 for String
                104
        );
        add(Value.CLOB, Types.CLOB, "Clob",
                createLob(),
                new String[]{"CLOB", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT", "NTEXT", "NCLOB"},
                // 80 for ValueLob, 24 for String
                104
        );
        DataType dataType = new DataType();
        dataType.prefix = "(";
        dataType.suffix = "')";
        add(Value.ARRAY, Types.ARRAY, "Array",
                dataType,
                new String[]{"ARRAY"},
                32
        );
        dataType = new DataType();
        add(Value.RESULT_SET, 0, "ResultSet",
                dataType,
                new String[]{"RESULT_SET"},
                400
        );
        for (int i = 0, size = TYPES_BY_VALUE_TYPE.size(); i < size; i++) {
            DataType dt = TYPES_BY_VALUE_TYPE.get(i);
            if (dt == null) {
                DbException.throwInternalError("unmapped type " + i);
            }
            Value.getOrder(i);
        }
    }

    private static void add(int type, int sqlType, String jdbc, DataType dataType, String[] names, int memory) {
        for (int i = 0; i < names.length; i++) {
            DataType dt = new DataType();
            dt.type = type;
            dt.sqlType = sqlType;
            dt.jdbc = jdbc;
            dt.name = names[i];
            dt.autoIncrement = dataType.autoIncrement;
            dt.decimal = dataType.decimal;
            dt.maxPrecision = dataType.maxPrecision;
            dt.maxScale = dataType.maxScale;
            dt.minScale = dataType.minScale;
            dt.params = dataType.params;
            dt.prefix = dataType.prefix;
            dt.suffix = dataType.suffix;
            dt.supportsPrecision = dataType.supportsPrecision;
            dt.supportsScale = dataType.supportsScale;
            dt.defaultPrecision = dataType.defaultPrecision;
            dt.defaultScale = dataType.defaultScale;
            dt.defaultDisplaySize = dataType.defaultDisplaySize;
            dt.caseSensitive = dataType.caseSensitive;
            dt.hidden = i > 0;
            dt.memory = memory;
            for (DataType t2 : TYPES) {
                if (t2.sqlType == dt.sqlType) {
                    dt.sqlTypePos++;
                }
            }
            TYPES_BY_NAME.put(dt.name, dt);
            if (TYPES_BY_VALUE_TYPE.get(type) == null) {
                TYPES_BY_VALUE_TYPE.set(type, dt);
            }
            TYPES.add(dt);
        }
    }

    private static DataType createDecimal(int maxPrecision, int defaultPrecision, int defaultScale, int defaultDisplaySize, boolean needsPrecisionAndScale, boolean autoInc) {
        DataType dataType = new DataType();
        dataType.maxPrecision = maxPrecision;
        dataType.defaultPrecision = defaultPrecision;
        dataType.defaultScale = defaultScale;
        dataType.defaultDisplaySize = defaultDisplaySize;
        if (needsPrecisionAndScale) {
            dataType.params = "PRECISION,SCALE";
            dataType.supportsPrecision = true;
            dataType.supportsScale = true;
        }
        dataType.decimal = true;
        dataType.autoIncrement = autoInc;
        return dataType;
    }

    private static DataType createDate(int precision, String prefix, int scale, int displaySize) {
        DataType dataType = new DataType();
        dataType.prefix = prefix + " '";
        dataType.suffix = "'";
        dataType.maxPrecision = precision;
        dataType.supportsScale = scale != 0;
        dataType.maxScale = scale;
        dataType.defaultPrecision = precision;
        dataType.defaultScale = scale;
        dataType.defaultDisplaySize = displaySize;
        return dataType;
    }

    private static DataType createString(boolean caseSensitive) {
        DataType dataType = new DataType();
        dataType.prefix = "'";
        dataType.suffix = "'";
        dataType.params = "LENGTH";
        dataType.caseSensitive = caseSensitive;
        dataType.supportsPrecision = true;
        dataType.maxPrecision = Integer.MAX_VALUE;
        dataType.defaultPrecision = Integer.MAX_VALUE;
        dataType.defaultDisplaySize = Integer.MAX_VALUE;
        return dataType;
    }

    private static DataType createLob() {
        DataType t = createString(true);
        t.maxPrecision = Long.MAX_VALUE;
        t.defaultPrecision = Long.MAX_VALUE;
        return t;
    }

    /**
     * Get the list of data types.
     *
     * @return the list
     */
    public static ArrayList<DataType> getTypes() {
        return TYPES;
    }

    /**
     * Read a value from the given result set.
     *
     * @param session the session
     * @param rs the result set
     * @param columnIndex the column index (1 based)
     * @param type the data type
     * @return the value
     */
    public static Value readValue(SessionInterface session, ResultSet rs, int columnIndex, int type) {
        try {
            Value v;
            switch(type) {
            case Value.NULL: {
                return ValueNull.INSTANCE;
            }
            case Value.BYTES: {
                byte[] buff = rs.getBytes(columnIndex);
                v = buff == null ? (Value) ValueNull.INSTANCE : ValueBytes.getNoCopy(buff);
                break;
            }
            case Value.UUID: {
                byte[] buff = rs.getBytes(columnIndex);
                v = buff == null ? (Value) ValueNull.INSTANCE : ValueUuid.get(buff);
                break;
            }
            case Value.BOOLEAN: {
                boolean value = rs.getBoolean(columnIndex);
                v = rs.wasNull() ? (Value) ValueNull.INSTANCE : ValueBoolean.get(value);
                break;
            }
            case Value.BYTE: {
                byte value = rs.getByte(columnIndex);
                v = rs.wasNull() ? (Value) ValueNull.INSTANCE : ValueByte.get(value);
                break;
            }
            case Value.DATE: {
                Date value = rs.getDate(columnIndex);
                v = value == null ? (Value) ValueNull.INSTANCE : ValueDate.get(value);
                break;
            }
            case Value.TIME: {
                Time value = rs.getTime(columnIndex);
                v = value == null ? (Value) ValueNull.INSTANCE : ValueTime.get(value);
                break;
            }
            case Value.TIMESTAMP: {
                Timestamp value = rs.getTimestamp(columnIndex);
                v = value == null ? (Value) ValueNull.INSTANCE : ValueTimestamp.get(value);
                break;
            }
            case Value.DECIMAL: {
                BigDecimal value = rs.getBigDecimal(columnIndex);
                v = value == null ? (Value) ValueNull.INSTANCE : ValueDecimal.get(value);
                break;
            }
            case Value.DOUBLE: {
                double value = rs.getDouble(columnIndex);
                v = rs.wasNull() ? (Value) ValueNull.INSTANCE : ValueDouble.get(value);
                break;
            }
            case Value.FLOAT: {
                float value = rs.getFloat(columnIndex);
                v = rs.wasNull() ? (Value) ValueNull.INSTANCE : ValueFloat.get(value);
                break;
            }
            case Value.INT: {
                int value = rs.getInt(columnIndex);
                v = rs.wasNull() ? (Value) ValueNull.INSTANCE : ValueInt.get(value);
                break;
            }
            case Value.LONG: {
                long value = rs.getLong(columnIndex);
                v = rs.wasNull() ? (Value) ValueNull.INSTANCE : ValueLong.get(value);
                break;
            }
            case Value.SHORT: {
                short value = rs.getShort(columnIndex);
                v = rs.wasNull() ? (Value) ValueNull.INSTANCE : ValueShort.get(value);
                break;
            }
            case Value.STRING_IGNORECASE: {
                String s = rs.getString(columnIndex);
                v = (s == null) ? (Value) ValueNull.INSTANCE : ValueStringIgnoreCase.get(s);
                break;
            }
            case Value.STRING_FIXED: {
                String s = rs.getString(columnIndex);
                v = (s == null) ? (Value) ValueNull.INSTANCE : ValueStringFixed.get(s);
                break;
            }
            case Value.STRING: {
                String s = rs.getString(columnIndex);
                v = (s == null) ? (Value) ValueNull.INSTANCE : ValueString.get(s);
                break;
            }
            case Value.CLOB: {
                if (session == null) {
                    v = LobStorage.createSmallLob(Value.CLOB, StringUtils.utf8Encode(rs.getString(columnIndex)));
                } else {
                    Reader in = rs.getCharacterStream(columnIndex);
                    if (in == null) {
                        v = ValueNull.INSTANCE;
                    } else {
                        v = session.getDataHandler().getLobStorage().createClob(new BufferedReader(in), -1);
                    }
                }
                break;
            }
            case Value.BLOB: {
                if (session == null) {
                    v = LobStorage.createSmallLob(Value.BLOB, rs.getBytes(columnIndex));
                } else {
                    InputStream in = rs.getBinaryStream(columnIndex);
                    v = (in == null) ? (Value) ValueNull.INSTANCE : session.getDataHandler().getLobStorage().createBlob(in, -1);
                }
                break;
            }
            case Value.JAVA_OBJECT: {
                byte[] buff = rs.getBytes(columnIndex);
                v = buff == null ? (Value) ValueNull.INSTANCE : ValueJavaObject.getNoCopy(buff);
                break;
            }
            case Value.ARRAY: {
                Array array = rs.getArray(columnIndex);
                if (array == null) {
                    return ValueNull.INSTANCE;
                }
                Object[] list = (Object[]) array.getArray();
                if (list == null) {
                    return ValueNull.INSTANCE;
                }
                int len = list.length;
                Value[] values = new Value[len];
                for (int i = 0; i < len; i++) {
                    values[i] = DataType.convertToValue(session, list[i], Value.NULL);
                }
                v = ValueArray.get(values);
                break;
            }
            default:
                throw DbException.throwInternalError("type="+type);
            }
            return v;
        } catch (SQLException e) {
            throw DbException.convert(e);
        }
    }

    /**
     * Get the name of the Java class for the given value type.
     *
     * @param type the value type
     * @return the class name
     */
    public static String getTypeClassName(int type) {
        switch(type) {
        case Value.BOOLEAN:
            // "java.lang.Boolean";
            return Boolean.class.getName();
        case Value.BYTE:
            // "java.lang.Byte";
            return Byte.class.getName();
        case Value.SHORT:
            // "java.lang.Short";
            return Short.class.getName();
        case Value.INT:
            // "java.lang.Integer";
            return Integer.class.getName();
        case Value.LONG:
            // "java.lang.Long";
            return Long.class.getName();
        case Value.DECIMAL:
            // "java.math.BigDecimal";
            return BigDecimal.class.getName();
        case Value.TIME:
            // "java.sql.Time";
            return Time.class.getName();
        case Value.DATE:
            // "java.sql.Date";
            return Date.class.getName();
        case Value.TIMESTAMP:
            // "java.sql.Timestamp";
            return Timestamp.class.getName();
        case Value.BYTES:
        case Value.UUID:
            // "[B", not "byte[]";
            return byte[].class.getName();
        case Value.STRING:
        case Value.STRING_IGNORECASE:
        case Value.STRING_FIXED:
            // "java.lang.String";
            return String.class.getName();
        case Value.BLOB:
            if (SysProperties.RETURN_LOB_OBJECTS) {
                // "java.sql.Blob";
                return java.sql.Blob.class.getName();
            }
            // "java.io.InputStream";
            return java.io.InputStream.class.getName();
        case Value.CLOB:
            if (SysProperties.RETURN_LOB_OBJECTS) {
                // "java.sql.Clob";
                return java.sql.Clob.class.getName();
            }
            // "java.io.Reader";
            return java.io.Reader.class.getName();
        case Value.DOUBLE:
            // "java.lang.Double";
            return Double.class.getName();
        case Value.FLOAT:
            // "java.lang.Float";
            return Float.class.getName();
        case Value.NULL:
            return null;
        case Value.JAVA_OBJECT:
            // "java.lang.Object";
            return Object.class.getName();
        case Value.UNKNOWN:
            // anything
            return Object.class.getName();
        case Value.ARRAY:
            return Array.class.getName();
        case Value.RESULT_SET:
            return ResultSet.class.getName();
        default:
            throw DbException.throwInternalError("type="+type);
        }
    }

    /**
     * Get the data type object for the given value type.
     *
     * @param type the value type
     * @return the data type object
     */
    public static DataType getDataType(int type) {
        DataType dt = TYPES_BY_VALUE_TYPE.get(type);
        if (dt == null) {
            dt = TYPES_BY_VALUE_TYPE.get(Value.NULL);
        }
        return dt;
    }

    /**
     * Convert a value type to a SQL type.
     *
     * @param type the value type
     * @return the SQL type
     */
    public static int convertTypeToSQLType(int type) {
        return getDataType(type).sqlType;
    }

    /**
     * Convert a SQL type to a value type.
     *
     * @param sqlType the SQL type
     * @return the value type
     */
    public static int convertSQLTypeToValueType(int sqlType) {
        switch(sqlType) {
        case Types.CHAR:
        case DataType.TYPE_NCHAR:
            return Value.STRING_FIXED;
        case Types.VARCHAR:
        case Types.LONGVARCHAR:
        case DataType.TYPE_NVARCHAR:
        case DataType.TYPE_LONGNVARCHAR:
            return Value.STRING;
        case Types.NUMERIC:
        case Types.DECIMAL:
            return Value.DECIMAL;
        case Types.BIT:
        case DataType.TYPE_BOOLEAN:
            return Value.BOOLEAN;
        case Types.INTEGER:
            return Value.INT;
        case Types.SMALLINT:
            return Value.SHORT;
        case Types.TINYINT:
            return Value.BYTE;
        case Types.BIGINT:
            return Value.LONG;
        case Types.REAL:
            return Value.FLOAT;
        case Types.DOUBLE:
        case Types.FLOAT:
            return Value.DOUBLE;
        case Types.BINARY:
        case Types.VARBINARY:
        case Types.LONGVARBINARY:
            return Value.BYTES;
        case Types.OTHER:
        case Types.JAVA_OBJECT:
            return Value.JAVA_OBJECT;
        case Types.DATE:
            return Value.DATE;
        case Types.TIME:
            return Value.TIME;
        case Types.TIMESTAMP:
            return Value.TIMESTAMP;
        case Types.BLOB:
            return Value.BLOB;
        case Types.CLOB:
        case DataType.TYPE_NCLOB:
            return Value.CLOB;
        case Types.NULL:
            return Value.NULL;
        case Types.ARRAY:
            return Value.ARRAY;
        default:
            throw DbException.get(ErrorCode.UNKNOWN_DATA_TYPE_1, "" + sqlType);
        }
    }

    /**
     * Get the value type for the given Java class.
     *
     * @param x the Java class
     * @return the value type
     */
    public static int getTypeFromClass(Class <?> x) {
        // TODO refactor: too many if/else in functions, can reduce!
        if (x == null || Void.TYPE == x) {
            return Value.NULL;
        }
        if (x.isPrimitive()) {
            x = Utils.getNonPrimitiveClass(x);
        }
        if (String.class == x) {
            return Value.STRING;
        } else if (Integer.class == x) {
            return Value.INT;
        } else if (Long.class == x) {
            return Value.LONG;
        } else if (Boolean.class == x) {
            return Value.BOOLEAN;
        } else if (Double.class == x) {
            return Value.DOUBLE;
        } else if (Byte.class == x) {
            return Value.BYTE;
        } else if (Short.class == x) {
            return Value.SHORT;
        } else if (Character.class == x) {
            throw DbException.get(ErrorCode.DATA_CONVERSION_ERROR_1, "char (not supported)");
        } else if (Float.class == x) {
            return Value.FLOAT;
        } else if (byte[].class == x) {
            return Value.BYTES;
        } else if (UUID.class == x) {
            return Value.UUID;
        } else if (Void.class == x) {
            return Value.NULL;
        } else if (BigDecimal.class.isAssignableFrom(x)) {
            return Value.DECIMAL;
        } else if (ResultSet.class.isAssignableFrom(x)) {
            return Value.RESULT_SET;
        } else if (Value.ValueBlob.class.isAssignableFrom(x)) {
            return Value.BLOB;
        } else if (Value.ValueClob.class.isAssignableFrom(x)) {
            return Value.CLOB;
        } else if (Date.class.isAssignableFrom(x)) {
            return Value.DATE;
        } else if (Time.class.isAssignableFrom(x)) {
            return Value.TIME;
        } else if (Timestamp.class.isAssignableFrom(x)) {
            return Value.TIMESTAMP;
        } else if (java.util.Date.class.isAssignableFrom(x)) {
            return Value.TIMESTAMP;
        } else if (java.io.Reader.class.isAssignableFrom(x)) {
            return Value.CLOB;
        } else if (java.sql.Clob.class.isAssignableFrom(x)) {
            return Value.CLOB;
        } else if (java.io.InputStream.class.isAssignableFrom(x)) {
            return Value.BLOB;
        } else if (java.sql.Blob.class.isAssignableFrom(x)) {
            return Value.BLOB;
        } else if (Object[].class.isAssignableFrom(x)) {
            // this includes String[] and so on
            return Value.ARRAY;
        } else {
            return Value.JAVA_OBJECT;
        }
    }

    /**
     * Convert a Java object to a value.
     *
     * @param session the session
     * @param x the value
     * @param type the value type
     * @return the value
     */
    public static Value convertToValue(SessionInterface session, Object x, int type) {
        if (x == null) {
            return ValueNull.INSTANCE;
        }
        if (type == Value.JAVA_OBJECT) {
            return ValueJavaObject.getNoCopy(Utils.serialize(x));
        }
        if (x instanceof String) {
            return ValueString.get((String) x);
        } else if (x instanceof Value) {
            return (Value) x;
        } else if (x instanceof Long) {
            return ValueLong.get(((Long) x).longValue());
        } else if (x instanceof Integer) {
            return ValueInt.get(((Integer) x).intValue());
        } else if (x instanceof BigDecimal) {
            return ValueDecimal.get((BigDecimal) x);
        } else if (x instanceof Boolean) {
            return ValueBoolean.get(((Boolean) x).booleanValue());
        } else if (x instanceof Byte) {
            return ValueByte.get(((Byte) x).byteValue());
        } else if (x instanceof Short) {
            return ValueShort.get(((Short) x).shortValue());
        } else if (x instanceof Float) {
            return ValueFloat.get(((Float) x).floatValue());
        } else if (x instanceof Double) {
            return ValueDouble.get(((Double) x).doubleValue());
        } else if (x instanceof byte[]) {
            return ValueBytes.get((byte[]) x);
        } else if (x instanceof Date) {
            return ValueDate.get((Date) x);
        } else if (x instanceof Time) {
            return ValueTime.get((Time) x);
        } else if (x instanceof Timestamp) {
            return ValueTimestamp.get((Timestamp) x);
        } else if (x instanceof java.util.Date) {
            return ValueTimestamp.get(new Timestamp(((java.util.Date) x).getTime()));
        } else if (x instanceof java.io.Reader) {
            Reader r = new BufferedReader((java.io.Reader) x);
            return session.getDataHandler().getLobStorage().createClob(r, -1);
        } else if (x instanceof java.sql.Clob) {
            try {
                Reader r = new BufferedReader(((java.sql.Clob) x).getCharacterStream());
                return session.getDataHandler().getLobStorage().createClob(r, -1);
            } catch (SQLException e) {
                throw DbException.convert(e);
            }
        } else if (x instanceof java.io.InputStream) {
            return session.getDataHandler().getLobStorage().createBlob((java.io.InputStream) x, -1);
        } else if (x instanceof java.sql.Blob) {
            try {
                return session.getDataHandler().getLobStorage().createBlob(((java.sql.Blob) x).getBinaryStream(), -1);
            } catch (SQLException e) {
                throw DbException.convert(e);
            }
        } else if (x instanceof ResultSet) {
            return ValueResultSet.get((ResultSet) x);
        } else if (x instanceof UUID) {
            UUID u = (UUID) x;
            return ValueUuid.get(u.getMostSignificantBits(), u.getLeastSignificantBits());
        } else if (x instanceof Object[]) {
            // (a.getClass().isArray());
            // (a.getClass().getComponentType().isPrimitive());
            Object[] o = (Object[]) x;
            int len = o.length;
            Value[] v = new Value[len];
            for (int i = 0; i < len; i++) {
                v[i] = convertToValue(session, o[i], type);
            }
            return ValueArray.get(v);
        } else if (x instanceof Character) {
            return ValueStringFixed.get(((Character) x).toString());
        } else {
            return ValueJavaObject.getNoCopy(Utils.serialize(x));
        }
    }

    /**
     * Get a data type object from a type name.
     *
     * @param s the type name
     * @return the data type object
     */
    public static DataType getTypeByName(String s) {
        return TYPES_BY_NAME.get(s);
    }

    /**
     * Check if the given value type is a large object (BLOB or CLOB).
     *
     * @param type the value type
     * @return true if the value type is a lob type
     */
    public static boolean isLargeObject(int type) {
        if (type == Value.BLOB || type == Value.CLOB) {
            return true;
        }
        return false;
    }

    /**
     * Check if the given value type is a String (VARCHAR,...).
     *
     * @param type the value type
     * @return true if the value type is a String type
     */
    public static boolean isStringType(int type) {
        if (type == Value.STRING || type == Value.STRING_FIXED || type == Value.STRING_IGNORECASE) {
            return true;
        }
        return false;
    }

    /**
     * Check if the given value type supports the add operation.
     *
     * @param type the value type
     * @return true if add is supported
     */
    public static boolean supportsAdd(int type) {
        switch (type) {
        case Value.BYTE:
        case Value.DECIMAL:
        case Value.DOUBLE:
        case Value.FLOAT:
        case Value.INT:
        case Value.LONG:
        case Value.SHORT:
            return true;
        default:
            return false;
        }
    }

    /**
     * Get the data type that will not overflow when calling 'add' 2 billion times.
     *
     * @param type the value type
     * @return the data type that supports adding
     */
    public static int getAddProofType(int type) {
        switch (type) {
        case Value.BYTE:
            return Value.LONG;
        case Value.FLOAT:
            return Value.DOUBLE;
        case Value.INT:
            return Value.LONG;
        case Value.LONG:
            return Value.DECIMAL;
        case Value.SHORT:
            return Value.LONG;
        default:
            return type;
        }
    }

    /**
     * Get the default value in the form of a Java object for the given Java class.
     *
     * @param clazz the Java class
     * @return the default object
     */
    public static Object getDefaultForPrimitiveType(Class<?> clazz) {
        if (clazz == Boolean.TYPE) {
            return Boolean.FALSE;
        } else if (clazz == Byte.TYPE) {
            return Byte.valueOf((byte) 0);
        } else if (clazz == Character.TYPE) {
            return Character.valueOf((char) 0);
        } else if (clazz == Short.TYPE) {
            return Short.valueOf((short) 0);
        } else if (clazz == Integer.TYPE) {
            return Integer.valueOf(0);
        } else if (clazz == Long.TYPE) {
            return Long.valueOf(0);
        } else if (clazz == Float.TYPE) {
            return Float.valueOf(0);
        } else if (clazz == Double.TYPE) {
            return Double.valueOf(0);
        }
        throw DbException.throwInternalError("primitive=" + clazz.toString());
    }

    /**
     * Convert a value to the specified class.
     *
     * @param session the session
     * @param conn the database connection
     * @param v the value
     * @param paramClass the target class
     * @return the converted object
     */
    public static Object convertTo(SessionInterface session, JdbcConnection conn, Value v, Class<?> paramClass) {
        if (paramClass == Blob.class) {
            return new JdbcBlob(conn, v, 0);
        } else if (paramClass == Clob.class) {
            return new JdbcClob(conn, v, 0);
        }
        if (v.getType() == Value.JAVA_OBJECT) {
            Object o = Utils.deserialize(v.getBytes());
            if (paramClass.isAssignableFrom(o.getClass())) {
                return o;
            }
        }
        throw DbException.getUnsupportedException(paramClass.getName());
    }

}
