/*
 * Copyright 2004-2015 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.query;

import java.lang.reflect.Field;
import java.sql.CallableStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentMap;

import javax.persistence.Lob;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.seasar.extension.jdbc.JdbcContext;
import org.seasar.extension.jdbc.ModuleCall;
import org.seasar.extension.jdbc.ParamType;
import org.seasar.extension.jdbc.ResultSetHandler;
import org.seasar.extension.jdbc.ValueType;
import org.seasar.extension.jdbc.annotation.InOut;
import org.seasar.extension.jdbc.annotation.Out;
import org.seasar.extension.jdbc.exception.FieldNotGenericsRuntimeException;
import org.seasar.extension.jdbc.handler.BeanListResultSetHandler;
import org.seasar.extension.jdbc.handler.BeanResultSetHandler;
import org.seasar.extension.jdbc.handler.ObjectListResultSetHandler;
import org.seasar.extension.jdbc.handler.ObjectResultSetHandler;
import org.seasar.extension.jdbc.manager.JdbcManagerImplementor;
import org.seasar.extension.jdbc.parameter.LobParameter;
import org.seasar.extension.jdbc.parameter.TemporalParameter;
import org.seasar.extension.jdbc.query.AbstractModuleCall.ParamDesc.ParameterType;
import org.seasar.extension.jdbc.types.ValueTypes;
import org.seasar.framework.exception.SQLRuntimeException;
import org.seasar.framework.util.ClassUtil;
import org.seasar.framework.util.Disposable;
import org.seasar.framework.util.DisposableUtil;
import org.seasar.framework.util.FieldUtil;
import org.seasar.framework.util.ModifierUtil;
import org.seasar.framework.util.StatementUtil;
import org.seasar.framework.util.tiger.CollectionsUtil;
import org.seasar.framework.util.tiger.ReflectionUtil;

/**
 * 永続格納モジュール(ストアドプロシージャまたはストアドファンクション)を呼び出す抽象クラスです。
 * 
 * @author koichik
 * @param <S>
 *            <code>ModuleCall</code>のサブタイプです。
 */
public abstract class AbstractModuleCall<S extends ModuleCall<S>> extends
        AbstractQuery<S> implements ModuleCall<S> {

    /** {@link DisposableUtil} に登録済みなら<code>true</code> */
    protected static boolean initialized;

    /** プロシージャまたはファンクションの引数として使用されるDTOのフィールドを表す{@link ParamDesc}のキャッシュ */
    protected static final ConcurrentMap<Class<?>, ParamDesc[]> paramDescCache = CollectionsUtil
            .newConcurrentHashMap();

    /** 最大行数 */
    protected int maxRows;

    /** フェッチ数 */
    protected int fetchSize;

    /** パラメータ */
    protected Object parameter;

    /** MS SQLServerのような結果セットを<code>OUT</code>パラメータにマッピングしない場合のパラメータのリスト */
    protected List<Param> nonParamList = new ArrayList<Param>();

    /** ファンクション呼び出しの場合は<code>true</code> */
    protected boolean functionCall;

    /**
     * インスタンスを構築します。
     * 
     * @param jdbcManager
     *            内部的なJDBCマネージャ
     * @param functionCall
     *            ファンクション呼び出しの場合は<code>true</code>
     */
    public AbstractModuleCall(final JdbcManagerImplementor jdbcManager,
            final boolean functionCall) {
        super(jdbcManager);
        this.functionCall = functionCall;
    }

    @SuppressWarnings("unchecked")
    public S maxRows(final int maxRows) {
        this.maxRows = maxRows;
        return (S) this;
    }

    @SuppressWarnings("unchecked")
    public S fetchSize(final int fetchSize) {
        this.fetchSize = fetchSize;
        return (S) this;
    }

    /**
     * パラメータの準備をします。
     */
    protected void prepareParameter() {
        if (parameter == null) {
            return;
        }
        final Class<?> paramClass = parameter.getClass();
        if (ValueTypes.isSimpleType(paramClass)
                || TemporalParameter.class == paramClass
                || LobParameter.class == paramClass) {
            addParam(parameter, paramClass);
            return;
        }

        final boolean needsParameterForResultSet = jdbcManager.getDialect()
                .needsParameterForResultSet();
        for (final ParamDesc paramDesc : getParamDescs(paramClass)) {
            final Class<?> clazz = paramDesc.paramClass;
            final ValueType valueType = paramDesc.valueType;
            switch (paramDesc.paramType) {
            case RESULT_SET:
                if (needsParameterForResultSet) {
                    addParam(paramDesc.field, clazz, valueType, ParamType.OUT);
                } else {
                    addNonParam(paramDesc.field);
                }
                break;
            case IN:
                addParam(FieldUtil.get(paramDesc.field, parameter), clazz,
                        valueType);
                break;
            case OUT:
                addParam(paramDesc.field, clazz, valueType, ParamType.OUT);
                break;
            case IN_OUT:
                addParam(paramDesc.field, clazz, valueType, ParamType.IN_OUT);
                break;
            }
        }
    }

    /**
     * パラメータを追加します。
     * 
     * @param field
     *            パラメータとなるフィールド
     * @param paramClass
     *            パラメータのクラス
     * @param valueType
     *            値タイプ
     * @param paramType
     *            パラメータのタイプ
     */
    protected void addParam(final Field field, final Class<?> paramClass,
            final ValueType valueType, final ParamType paramType) {
        final Object value;
        if (field != null && paramType != ParamType.OUT) {
            value = FieldUtil.get(field, parameter);
        } else {
            value = null;
        }
        final Param p = addParam(value, paramClass, valueType);
        p.paramType = paramType;
        p.field = field;
    }

    /**
     * 呼び出し可能なステートメントを返します。
     * 
     * @param jdbcContext
     *            JDBCコンテキスト
     * @return 呼び出し可能なステートメント
     */
    protected CallableStatement getCallableStatement(
            final JdbcContext jdbcContext) {
        final CallableStatement ps = jdbcContext
                .getCallableStatement(executedSql);
        setupCallableStatement(ps);
        return ps;
    }

    /**
     * 呼び出し可能なステートメントをセットアップします。
     * 
     * @param cs
     *            呼び出し可能なステートメント
     */
    protected void setupCallableStatement(final CallableStatement cs) {
        if (maxRows > 0) {
            StatementUtil.setMaxRows(cs, maxRows);
        }
        if (fetchSize > 0) {
            StatementUtil.setFetchSize(cs, fetchSize);
        }
        if (queryTimeout > 0) {
            StatementUtil.setQueryTimeout(cs, queryTimeout);
        }
        prepareInParams(cs);
        prepareOutParams(cs);
    }

    /**
     * <code>OUT</code>パラメータを準備します。
     * 
     * @param cs
     *            呼び出し可能なステートメント
     */
    protected void prepareOutParams(final CallableStatement cs) {
        final int size = getParamSize();
        try {
            for (int i = 0; i < size; i++) {
                final Param param = getParam(i);
                if (param.paramType == ParamType.IN) {
                    continue;
                }
                param.valueType.registerOutParameter(cs, i + 1);
            }
        } catch (final SQLException e) {
            throw new SQLRuntimeException(e);
        }
    }

    /**
     * <code>OUT</code>パラメータにマッピングされない結果セットを処理します。
     * <p>
     * このメソッドは{@link #handleOutParams(CallableStatement)}よりも前に呼び出さなくてはなりません。
     * </p>
     * 
     * @param cs
     *            呼び出し可能なステートメント
     * @param resultSetGettable
     *            呼び出し可能なステートメントから結果セットを取得可能な場合{@code true}
     */
    protected void handleNonParamResultSets(final CallableStatement cs,
            final boolean resultSetGettable) {
        try {
            if (!resultSetGettable) {
                cs.getMoreResults();
            }
            for (int i = 0; i < nonParamList.size(); i++) {
                final ResultSet rs = getResultSet(cs);
                if (rs == null) {
                    break;
                }
                final Param param = nonParamList.get(i);
                final Object value = handleResultSet(param.field, cs
                        .getResultSet());
                FieldUtil.set(param.field, parameter, value);
                cs.getMoreResults();
            }
        } catch (final SQLException e) {
            throw new SQLRuntimeException(e);
        }
    }

    /**
     * <code>OUT/INOUT</code>パラメータとは別に戻される結果セットを返します。
     * 
     * @param cs
     *            呼び出し可能なステートメント
     * @return 結果セット
     */
    protected ResultSet getResultSet(final CallableStatement cs) {
        try {
            for (;;) {
                final ResultSet rs = cs.getResultSet();
                if (rs != null) {
                    return rs;
                }
                if (cs.getUpdateCount() == -1) {
                    return null;
                }
                cs.getMoreResults();
            }
        } catch (final SQLException e) {
            throw new SQLRuntimeException(e);
        }
    }

    /**
     * <code>OUT</code>パラメータを処理します。
     * 
     * @param cs
     *            呼び出し可能なステートメント
     */
    protected void handleOutParams(final CallableStatement cs) {
        try {
            final int start = functionCall ? 1 : 0;
            for (int i = start; i < getParamSize(); i++) {
                final Param param = getParam(i);
                if (param.paramType == ParamType.IN) {
                    continue;
                }
                Object value = param.valueType.getValue(cs, i + 1);
                if (value instanceof ResultSet) {
                    value = handleResultSet(param.field, (ResultSet) value);
                }
                FieldUtil.set(param.field, parameter, value);
            }
        } catch (final SQLException e) {
            throw new SQLRuntimeException(e);
        }
    }

    /**
     * 結果セットを処理します。
     * 
     * @param field
     *            フィールド
     * @param rs
     *            結果セット
     * @return 処理した結果
     */
    protected Object handleResultSet(final Field field, final ResultSet rs) {
        if (!List.class.isAssignableFrom(field.getType())) {
            return handleSingleResult(field.getType(), rs);
        }
        final Class<?> elementClass = ReflectionUtil
                .getElementTypeOfListFromFieldType(field);
        if (elementClass == null) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            throw new FieldNotGenericsRuntimeException(field);
        }
        return handleResultList(elementClass, rs);
    }

    /**
     * 1行だけの結果セットを処理します。
     * 
     * @param resultClass
     *            結果のクラス
     * @param rs
     *            結果セット
     * @return 処理した結果
     */
    protected Object handleSingleResult(final Class<?> resultClass,
            final ResultSet rs) {
        final ResultSetHandler handler;
        if (ValueTypes.isSimpleType(resultClass)) {
            handler = new ObjectResultSetHandler(ValueTypes
                    .getValueType(resultClass), null);
        } else {
            handler = new BeanResultSetHandler(resultClass, jdbcManager
                    .getDialect(), jdbcManager.getPersistenceConvention(), null);
        }
        return handleResultSet(handler, rs);
    }

    /**
     * 複数の行を持つ結果セットを処理します。
     * 
     * @param elementClass
     *            結果となるリストの要素型
     * @param rs
     *            結果セット
     * @return 処理した結果のリスト
     */
    protected Object handleResultList(final Class<?> elementClass,
            final ResultSet rs) {
        final ResultSetHandler handler;
        if (ValueTypes.isSimpleType(elementClass)) {
            handler = new ObjectListResultSetHandler(ValueTypes
                    .getValueType(elementClass));
        } else {
            handler = new BeanListResultSetHandler(elementClass, jdbcManager
                    .getDialect(), jdbcManager.getPersistenceConvention(), null);
        }
        return handleResultSet(handler, rs);
    }

    /**
     * 直接パラメータでは指定しないパラメータの数を返します。
     * 
     * @return 直接パラメータでは指定しないパラメータの数
     */
    protected int getNonParamSize() {
        return nonParamList.size();
    }

    /**
     * 直接パラメータでは指定しないパラメータを返します。
     * 
     * @param index
     *            インデックス
     * @return 直接パラメータでは指定しないパラメータ
     */
    protected Param getNonParam(final int index) {
        return nonParamList.get(index);
    }

    /**
     * 直接パラメータでは指定しないパラメータを追加します。
     * 
     * @param field
     *            フィールド
     * @return 追加されたパラメータ
     */
    protected Param addNonParam(final Field field) {
        final Param param = new Param();
        param.field = field;
        param.paramType = ParamType.OUT;
        nonParamList.add(param);
        return param;
    }

    /**
     * フェッチ数を返します。
     * 
     * @return フェッチ数
     */
    public int getFetchSize() {
        return fetchSize;
    }

    /**
     * 最大行数を返します。
     * 
     * @return 最大行数
     */
    public int getMaxRows() {
        return maxRows;
    }

    /**
     * パラメータを返します。
     * 
     * @return パラメータ
     */
    public Object getParameter() {
        return parameter;
    }

    /**
     * 初期化します。
     */
    protected synchronized void initialize() {
        if (initialized) {
            return;
        }
        DisposableUtil.add(new Disposable() {

            public void dispose() {
                paramDescCache.clear();
                initialized = false;
            }

        });
        initialized = true;
    }

    /**
     * パラメータとして渡されたDTOのフィールドを表す{@link ParamDesc}の配列を返します。
     * 
     * @param dtoClass
     *            パラメータとして渡されたDTOのクラス
     * @return パラメータとして渡されたDTOのフィールドを表す{@link ParamDesc}の配列
     */
    protected ParamDesc[] getParamDescs(final Class<?> dtoClass) {
        initialize();
        final ParamDesc[] paramDesc = paramDescCache.get(dtoClass);
        if (paramDesc != null) {
            return paramDesc;
        }
        return createParamDesc(dtoClass);
    }

    /**
     * パラメータとして渡されたDTOのフィールドを表す{@link ParamDesc}の配列を作成して返します。
     * 
     * @param dtoClass
     *            パラメータとして渡されたDTOのクラス
     * @return パラメータとして渡されたDTOのフィールドを表す{@link ParamDesc}の配列
     */
    protected ParamDesc[] createParamDesc(final Class<?> dtoClass) {
        final Field[] fields = ClassUtil.getDeclaredFields(dtoClass);
        final List<ParamDesc> list = CollectionsUtil
                .newArrayList(fields.length);
        for (int i = 0; i < fields.length; ++i) {
            final Field field = fields[i];
            if (!ModifierUtil.isInstanceField(field)) {
                continue;
            }
            field.setAccessible(true);
            final ParamDesc paramDesc = new ParamDesc();
            paramDesc.field = field;
            paramDesc.name = field.getName();
            paramDesc.paramClass = field.getType();
            final boolean lob = field.getAnnotation(Lob.class) != null;
            final Temporal temporal = field.getAnnotation(Temporal.class);
            final TemporalType temporalType = temporal != null ? temporal
                    .value() : null;
            paramDesc.valueType = getValueType(paramDesc.paramClass, lob,
                    temporalType);
            if (field
                    .getAnnotation(org.seasar.extension.jdbc.annotation.ResultSet.class) != null) {
                paramDesc.paramType = ParameterType.RESULT_SET;
            } else if (field.getAnnotation(Out.class) != null) {
                paramDesc.paramType = ParameterType.OUT;
            } else if (field.getAnnotation(InOut.class) != null) {
                paramDesc.paramType = ParameterType.IN_OUT;
            } else {
                paramDesc.paramType = ParameterType.IN;
            }
            list.add(paramDesc);
        }
        final ParamDesc[] result = list.toArray(new ParamDesc[list.size()]);
        return CollectionsUtil.putIfAbsent(paramDescCache, dtoClass, result);
    }

    /**
     * パラメータとして渡されたDTOのフィールドを表すクラスです。
     * 
     * @author koichik
     */
    public static class ParamDesc {

        /**
         * パラメータのタイプを表す列挙です。
         */
        public enum ParameterType {
            /** <code>IN</code>パラメータを表します。 */
            IN,

            /** <code>INOUT</code>パラメータを表します。 */
            IN_OUT,

            /** <code>OUT</code>パラメータを表します。 */
            OUT,

            /** ストアドのパラメータとは別に戻される結果セットを表します。 */
            RESULT_SET
        }

        /** パラメータを表すフィールド */
        public Field field;

        /** パラメータ名 */
        public String name;

        /** パラメータのクラス */
        public Class<?> paramClass;

        /** パラメータのタイプ */
        public ParameterType paramType;

        /** パラメータのS2JDBCでの型 */
        public ValueType valueType;

    }

}