/*
 * Copyright 2004-2013 the Seasar Foundation and the Others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.seasar.extension.jdbc.types;

import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.seasar.extension.jdbc.ValueType;
import org.seasar.framework.util.ConstructorUtil;
import org.seasar.framework.util.Disposable;
import org.seasar.framework.util.DisposableUtil;
import org.seasar.framework.util.MapUtil;
import org.seasar.framework.util.MethodUtil;
import org.seasar.framework.util.ModifierUtil;

/**
 * 値タイプのファクトリです。
 * 
 * @author higa
 * 
 */
public class ValueTypes {

    /**
     * String用の値タイプです。
     */
    public final static ValueType STRING = new StringType();

    /**
     * <code>CLOB</code>用の値タイプです。
     */
    public final static ValueType CLOB = new StringClobType();

    /**
     * WaveDashを変換するString用の値タイプです。
     */
    public final static ValueType WAVE_DASH_STRING = new WaveDashStringType();

    /**
     * WaveDashを変換する<code>CLOB</code>用の値タイプです。
     */
    public final static ValueType WAVE_DASH_CLOB = new WaveDashStringClobType();

    /**
     * Character用の値タイプです。
     */
    public final static ValueType CHARACTER = new CharacterType();

    /**
     * Byte用の値タイプです。
     */
    public final static ValueType BYTE = new ByteType();

    /**
     * Short用の値タイプです。
     */
    public final static ValueType SHORT = new ShortType();

    /**
     * Integer用の値タイプです。
     */
    public final static ValueType INTEGER = new IntegerType();

    /**
     * Long用の値タイプです。
     */
    public final static ValueType LONG = new LongType();

    /**
     * Float用の値タイプです。
     */
    public final static ValueType FLOAT = new FloatType();

    /**
     * Double用の値タイプです。
     */
    public final static ValueType DOUBLE = new DoubleType();

    /**
     * BigDecimal用の値タイプです。
     */
    public final static ValueType BIGDECIMAL = new BigDecimalType();

    /**
     * BigInteger用の値タイプです。
     */
    public final static ValueType BIGINTEGER = new BigIntegerType();

    /**
     * Time用の値タイプです。
     */
    public final static ValueType TIME = new TimeType();

    /**
     * java.sql.Date用の値タイプです。
     */
    public final static ValueType SQLDATE = new SqlDateType();

    /**
     * Timestamp用の値タイプです。
     */
    public final static ValueType TIMESTAMP = new TimestampType();

    /**
     * java.sql.Dateと互換性をもつjava.util.Date用の値タイプです。
     */
    public final static ValueType DATE_SQLDATE = new DateSqlDateType();

    /**
     * Timeと互換性をもつjava.util.Date用の値タイプです。
     */
    public final static ValueType DATE_TIME = new DateTimeType();

    /**
     * Timestampと互換性をもつjava.util.Date用の値タイプです。
     */
    public final static ValueType DATE_TIMESTAMP = new DateTimestampType();

    /**
     * java.sql.Dateと互換性をもつCalendar用の値タイプです。
     */
    public final static ValueType CALENDAR_SQLDATE = new CalendarSqlDateType();

    /**
     * Timeと互換性をもつCalendar用の値タイプです。
     */
    public final static ValueType CALENDAR_TIME = new CalendarTimeType();

    /**
     * Timestampと互換性をもつCalendar用の値タイプです。
     */
    public final static ValueType CALENDAR_TIMESTAMP = new CalendarTimestampType();

    /**
     * Binary用の値タイプです。
     */
    public final static ValueType BINARY = new BinaryType();

    /**
     * BinaryStream用の値タイプです。
     */
    public final static ValueType BINARY_STREAM = new BinaryStreamType();

    /**
     * バイト配列用の値タイプです。
     */
    public final static ValueType BYTE_ARRAY = new BytesType(
            BytesType.BYTES_TRAIT);

    /**
     * BLOB用の値タイプです。
     */
    public final static ValueType BLOB = new BytesType(BytesType.BLOB_TRAIT);

    /**
     * オブジェクトをシリアライズしたバイト配列用の値タイプです。
     */
    public final static ValueType SERIALIZABLE_BYTE_ARRAY = new SerializableType(
            BytesType.BYTES_TRAIT);

    /**
     * オブジェクトをシリアライズしたBLOB用の値タイプです。
     */
    public final static ValueType SERIALIZABLE_BLOB = new SerializableType(
            BytesType.BLOB_TRAIT);

    /**
     * Boolean用の値タイプです。
     */
    public final static ValueType BOOLEAN = new BooleanType();

    /**
     * JavaのBooleanの値をデータベースのIntegerに変換する値タイプです。
     */
    public final static ValueType BOOLEAN_INTEGER = new BooleanIntegerType();

    /**
     * PostgreSQLの結果セット用の値タイプです。
     */
    public final static ValueType POSTGRE_RESULT_SET = new PostgreResultSetType();

    /**
     * オラクルの結果セット用の値タイプです。
     */
    public final static ValueType ORACLE_RESULT_SET = new OracleResultSetType();

    /**
     * 汎用的な値タイプです。
     */
    public final static ValueType OBJECT = new ObjectType();

    private static final ValueType NULL = new NullType();

    private static final Class BYTE_ARRAY_CLASS = new byte[0].getClass();

    private static Map types = new HashMap();

    private static Method isEnumMethod;

    private static Constructor enumDefaultValueTypeConstructor;

    private static Constructor enumOrdinalValueTypeConstructor;

    private static Constructor enumStringValueTypeConstructor;

    private static volatile boolean initialized;

    private static Map valueTypeCache = MapUtil.createHashMap(50);

    static {
        registerValueType(String.class, STRING);
        registerValueType(char.class, CHARACTER);
        registerValueType(Character.class, CHARACTER);
        registerValueType(byte.class, BYTE);
        registerValueType(Byte.class, BYTE);
        registerValueType(short.class, SHORT);
        registerValueType(Short.class, SHORT);
        registerValueType(int.class, INTEGER);
        registerValueType(Integer.class, INTEGER);
        registerValueType(long.class, LONG);
        registerValueType(Long.class, LONG);
        registerValueType(float.class, FLOAT);
        registerValueType(Float.class, FLOAT);
        registerValueType(double.class, DOUBLE);
        registerValueType(Double.class, DOUBLE);
        registerValueType(BigInteger.class, BIGINTEGER);
        registerValueType(BigDecimal.class, BIGDECIMAL);
        registerValueType(java.sql.Date.class, SQLDATE);
        registerValueType(java.sql.Time.class, TIME);
        registerValueType(java.util.Date.class, TIMESTAMP);
        registerValueType(Timestamp.class, TIMESTAMP);
        registerValueType(Calendar.class, TIMESTAMP);
        registerValueType(BYTE_ARRAY_CLASS, BINARY);
        registerValueType(InputStream.class, BINARY_STREAM);
        registerValueType(boolean.class, BOOLEAN);
        registerValueType(Boolean.class, BOOLEAN);
        // registerValueType(Object.class, OBJECT);
        try {
            isEnumMethod = Class.class.getMethod("isEnum", null);
            setEnumDefaultValueType(Class
                    .forName("org.seasar.extension.jdbc.types.EnumOrdinalType"));
            setEnumOrdinalValueType(Class
                    .forName("org.seasar.extension.jdbc.types.EnumOrdinalType"));
            setEnumStringValueType(Class
                    .forName("org.seasar.extension.jdbc.types.EnumType"));
        } catch (Throwable ignore) {
            isEnumMethod = null;
            enumStringValueTypeConstructor = null;
            enumOrdinalValueTypeConstructor = null;
        }
        initialize();
    }

    /**
     * インスタンスを構築します。
     */
    protected ValueTypes() {
    }

    /**
     * 初期化を行ないます。
     */
    public static void initialize() {
        DisposableUtil.add(new Disposable() {
            public void dispose() {
                clear();
            }
        });
        initialized = true;
    }

    /**
     * キャッシュをクリアします。
     */
    public static void clear() {
        valueTypeCache.clear();
        initialized = false;
    }

    /**
     * クラスに対する{@link ValueType}を登録します。
     * 
     * @param clazz
     * @param valueType
     */
    public static void registerValueType(Class clazz, ValueType valueType) {
        types.put(clazz, valueType);
    }

    /**
     * クラスに対する{@link ValueType}の登録を解除します。
     * 
     * @param clazz
     */
    public static void unregisterValueType(Class clazz) {
        types.remove(clazz);
    }

    /**
     * enum型に対するデフォルトの{@link ValueType}を設定します。
     * 
     * @param enumDefaultValueTypeClass
     *            enum型に対するデフォルトの{@link ValueType}
     * @throws NoSuchMethodException
     *             指定のクラスにClassを唯一の引数とするコンストラクタがない場合
     */
    public static void setEnumDefaultValueType(Class enumDefaultValueTypeClass)
            throws NoSuchMethodException {
        enumDefaultValueTypeConstructor = enumDefaultValueTypeClass
                .getConstructor(new Class[] { Class.class });
    }

    /**
     * enum型の{@link Enum#ordinal() 序数}に対する{@link ValueType}を設定します。
     * 
     * @param enumOrdinalValueTypeClass
     *            enum型の{@link Enum#ordinal() 序数}に対する{@link ValueType}
     * @throws NoSuchMethodException
     *             指定のクラスにClassを唯一の引数とするコンストラクタがない場合
     */
    public static void setEnumOrdinalValueType(Class enumOrdinalValueTypeClass)
            throws NoSuchMethodException {
        enumOrdinalValueTypeConstructor = enumOrdinalValueTypeClass
                .getConstructor(new Class[] { Class.class });
    }

    /**
     * enum型の{@link Enum#name() 名前}に対する{@link ValueType}を設定します。
     * 
     * @param enumStringValueTypeClass
     *            enum型の{@link Enum#name() 名前}に対する{@link ValueType}
     * @throws NoSuchMethodException
     *             指定のクラスにClassを唯一の引数とするコンストラクタがない場合
     */
    public static void setEnumStringValueType(Class enumStringValueTypeClass)
            throws NoSuchMethodException {
        enumStringValueTypeConstructor = enumStringValueTypeClass
                .getConstructor(new Class[] { Class.class });
    }

    /**
     * {@link ValueType}を返します。
     * 
     * @param obj
     * @return {@link ValueType}
     */
    public static ValueType getValueType(Object obj) {
        if (obj == null) {
            return OBJECT;
        }
        return getValueType(obj.getClass());
    }

    /**
     * {@link ValueType}を返します。
     * 
     * @param clazz
     * @return {@link ValueType}
     */
    public static ValueType getValueType(Class clazz) {
        if (clazz == null) {
            return OBJECT;
        }
        for (Class c = clazz; c != null && c != Object.class; c = c
                .getSuperclass()) {
            ValueType valueType = getValueType0(c);
            if (valueType != null) {
                return valueType;
            }
        }
        ValueType valueType = getCachedValueType(clazz);
        if (valueType != null) {
            return valueType;
        }
        return OBJECT;
    }

    private static ValueType getValueType0(Class clazz) {
        return (ValueType) types.get(clazz);
    }

    private static boolean hasCachedValueType(Class clazz) {
        return getCachedValueType(clazz) != null;
    }

    private static ValueType getCachedValueType(Class clazz) {
        if (!initialized) {
            initialize();
        }
        if (Map.class.isAssignableFrom(clazz)) {
            return null;
        }
        ValueType valueType = (ValueType) valueTypeCache.get(clazz.getName());
        if (valueType == NULL) {
            return null;
        }
        if (valueType != null) {
            return valueType;
        }
        Class normalizedEnumClass = normalizeEnum(clazz);
        if (normalizedEnumClass != null) {
            valueType = getEnumDefaultValueType(normalizedEnumClass);
            valueTypeCache.put(normalizedEnumClass.getName(), valueType);
            return valueType;
        }
        valueType = createUserDefineValueType(clazz);
        if (valueType != null) {
            valueTypeCache.put(clazz.getName(), valueType);
            return valueType;
        }
        valueTypeCache.put(clazz.getName(), NULL);
        return null;
    }

    private static Class normalizeEnum(Class clazz) {
        if (isEnumMethod == null || enumStringValueTypeConstructor == null) {
            return null;
        }
        for (Class c = clazz; c != null; c = c.getSuperclass()) {
            if (MethodUtil.invoke(isEnumMethod, c, null).equals(Boolean.TRUE)) {
                return c;
            }
        }
        return null;
    }

    /**
     * enum型に対するデフォルトの{@link ValueType}を作成して返します。
     * 
     * @param clazz
     *            enum型のクラス
     * @return enum型用の{@link ValueType}
     */
    public static ValueType getEnumDefaultValueType(Class clazz) {
        return (ValueType) ConstructorUtil.newInstance(
                enumDefaultValueTypeConstructor, new Class[] { clazz });
    }

    /**
     * enum型の{@link Enum#name() 名前}に対する{@link ValueType}を作成して返します。
     * 
     * @param clazz
     *            enum型のクラス
     * @return enum型の{@link Enum#name() 名前}用の{@link ValueType}
     */
    public static ValueType getEnumStringValueType(Class clazz) {
        return (ValueType) ConstructorUtil.newInstance(
                enumStringValueTypeConstructor, new Class[] { clazz });
    }

    /**
     * enum型の{@link Enum#ordinal() 序数}に対する{@link ValueType}を作成して返します。
     * 
     * @param clazz
     *            enum型のクラス
     * @return enum型の{@link Enum#ordinal() 序数}用の{@link ValueType}
     */
    public static ValueType getEnumOrdinalValueType(Class clazz) {
        return (ValueType) ConstructorUtil.newInstance(
                enumOrdinalValueTypeConstructor, new Class[] { clazz });
    }

    /**
     * ユーザ定義型用の{@link ValueType}を作成して返します。
     * <p>
     * <code>clazz</code>がユーザ定義型でなければ<code>null</code>を返します。
     * </p>
     * 
     * @param clazz
     *            ユーザ定義型
     * @return ユーザ定義型用の{@link ValueType}
     */
    public static ValueType createUserDefineValueType(Class clazz) {
        List valueOfMethods = new ArrayList();
        Method valueMethod = null;
        Method[] methods = clazz.getMethods();
        for (int i = 0; i < methods.length; ++i) {
            Method method = methods[i];
            if (MethodUtil.isBridgeMethod(method)
                    || MethodUtil.isSyntheticMethod(method)) {
                continue;
            }
            int mod = method.getModifiers();
            if (method.getName().equals("valueOf")
                    && method.getParameterTypes().length == 1
                    && method.getReturnType() == clazz
                    && ModifierUtil.isPublic(mod) && ModifierUtil.isStatic(mod)) {
                valueOfMethods.add(method);
            } else if (method.getName().equals("value")
                    && method.getParameterTypes().length == 0
                    && ModifierUtil.isPublic(mod)
                    && !ModifierUtil.isStatic(mod)) {
                valueMethod = method;
            }
        }
        if (valueMethod == null) {
            return null;
        }
        for (int i = 0; i < valueOfMethods.size(); ++i) {
            Method valueOfMethod = (Method) valueOfMethods.get(i);
            if (valueOfMethod.getParameterTypes()[0] == valueMethod
                    .getReturnType()) {
                Class baseClass = valueMethod.getReturnType();
                ValueType baseValueType = getValueType0(baseClass);
                if (baseValueType == null) {
                    return null;
                }
                return new UserDefineType(baseValueType, valueOfMethod,
                        valueMethod);
            }
        }
        return null;
    }

    /**
     * sqltypeに応じた {@link Class}を返します。
     * 
     * @param sqltype
     * @return {@link Class}
     */
    public static Class getType(int sqltype) {
        switch (sqltype) {
        case Types.TINYINT:
            return Byte.class;
        case Types.SMALLINT:
            return Short.class;
        case Types.INTEGER:
            return Integer.class;
        case Types.BIGINT:
            return Long.class;
        case Types.REAL:
        case Types.FLOAT:
            return Float.class;
        case Types.DOUBLE:
            return Double.class;
        case Types.DECIMAL:
        case Types.NUMERIC:
            return BigDecimal.class;
        case Types.DATE:
            return Timestamp.class;
        case Types.TIME:
            return java.sql.Time.class;
        case Types.TIMESTAMP:
            return Timestamp.class;
        case Types.BINARY:
        case Types.BLOB:
        case Types.VARBINARY:
        case Types.LONGVARBINARY:
            return BYTE_ARRAY_CLASS;
        case Types.CHAR:
        case Types.LONGVARCHAR:
        case Types.VARCHAR:
            return String.class;
        case Types.BOOLEAN:
            return Boolean.class;
        default:
            return Object.class;
        }
    }

    /**
     * {@link ValueType}を返します。
     * 
     * @param sqltype
     * @return {@link ValueType}
     */
    public static ValueType getValueType(int sqltype) {
        return getValueType(getType(sqltype));
    }

    /**
     * 単純な型かどうかを返します。
     * 
     * @param clazz
     *            クラス
     * @return 単純な型かどうか
     */
    public static boolean isSimpleType(Class clazz) {
        if (clazz == null) {
            throw new NullPointerException("clazz");
        }
        return types.containsKey(clazz) || hasCachedValueType(clazz);
    }

    private static class NullType implements ValueType {

        public void bindValue(CallableStatement cs, String parameterName,
                Object value) throws SQLException {
            throw new SQLException("not supported");
        }

        public void bindValue(PreparedStatement ps, int index, Object value)
                throws SQLException {
            throw new SQLException("not supported");
        }

        public Object getValue(CallableStatement cs, int index)
                throws SQLException {
            throw new SQLException("not supported");
        }

        public Object getValue(CallableStatement cs, String parameterName)
                throws SQLException {
            throw new SQLException("not supported");
        }

        public Object getValue(ResultSet resultSet, int index)
                throws SQLException {
            throw new SQLException("not supported");
        }

        public Object getValue(ResultSet resultSet, String columnName)
                throws SQLException {
            throw new SQLException("not supported");
        }

        public void registerOutParameter(CallableStatement cs, int index)
                throws SQLException {
            throw new SQLException("not supported");
        }

        public void registerOutParameter(CallableStatement cs,
                String parameterName) throws SQLException {
            throw new SQLException("not supported");
        }

        public String toText(Object value) {
            throw new UnsupportedOperationException("toText");
        }

        public int getSqlType() {
            return Types.NULL;
        }
    }
}