/*
 * 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.manager;

import java.sql.Connection;
import java.util.Arrays;
import java.util.List;

import javax.sql.DataSource;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.TransactionSynchronizationRegistry;

import org.seasar.extension.datasource.DataSourceFactory;
import org.seasar.extension.datasource.impl.SelectableDataSourceProxy;
import org.seasar.extension.jdbc.AutoBatchDelete;
import org.seasar.extension.jdbc.AutoBatchInsert;
import org.seasar.extension.jdbc.AutoBatchUpdate;
import org.seasar.extension.jdbc.AutoDelete;
import org.seasar.extension.jdbc.AutoFunctionCall;
import org.seasar.extension.jdbc.AutoInsert;
import org.seasar.extension.jdbc.AutoProcedureCall;
import org.seasar.extension.jdbc.AutoSelect;
import org.seasar.extension.jdbc.AutoUpdate;
import org.seasar.extension.jdbc.DbmsDialect;
import org.seasar.extension.jdbc.EntityMeta;
import org.seasar.extension.jdbc.EntityMetaFactory;
import org.seasar.extension.jdbc.JdbcContext;
import org.seasar.extension.jdbc.JdbcManager;
import org.seasar.extension.jdbc.SqlBatchUpdate;
import org.seasar.extension.jdbc.SqlFileBatchUpdate;
import org.seasar.extension.jdbc.SqlFileFunctionCall;
import org.seasar.extension.jdbc.SqlFileProcedureCall;
import org.seasar.extension.jdbc.SqlFileSelect;
import org.seasar.extension.jdbc.SqlFileUpdate;
import org.seasar.extension.jdbc.SqlFunctionCall;
import org.seasar.extension.jdbc.SqlProcedureCall;
import org.seasar.extension.jdbc.SqlSelect;
import org.seasar.extension.jdbc.SqlUpdate;
import org.seasar.extension.jdbc.exception.NoIdPropertyRuntimeException;
import org.seasar.extension.jdbc.query.AutoBatchDeleteImpl;
import org.seasar.extension.jdbc.query.AutoBatchInsertImpl;
import org.seasar.extension.jdbc.query.AutoBatchUpdateImpl;
import org.seasar.extension.jdbc.query.AutoDeleteImpl;
import org.seasar.extension.jdbc.query.AutoFunctionCallImpl;
import org.seasar.extension.jdbc.query.AutoInsertImpl;
import org.seasar.extension.jdbc.query.AutoProcedureCallImpl;
import org.seasar.extension.jdbc.query.AutoSelectImpl;
import org.seasar.extension.jdbc.query.AutoUpdateImpl;
import org.seasar.extension.jdbc.query.SqlBatchUpdateImpl;
import org.seasar.extension.jdbc.query.SqlFileBatchUpdateImpl;
import org.seasar.extension.jdbc.query.SqlFileFunctionCallImpl;
import org.seasar.extension.jdbc.query.SqlFileProcedureCallImpl;
import org.seasar.extension.jdbc.query.SqlFileSelectImpl;
import org.seasar.extension.jdbc.query.SqlFileUpdateImpl;
import org.seasar.extension.jdbc.query.SqlFunctionCallImpl;
import org.seasar.extension.jdbc.query.SqlProcedureCallImpl;
import org.seasar.extension.jdbc.query.SqlSelectImpl;
import org.seasar.extension.jdbc.query.SqlUpdateImpl;
import org.seasar.extension.jdbc.util.DataSourceUtil;
import org.seasar.framework.container.annotation.tiger.Binding;
import org.seasar.framework.container.annotation.tiger.BindingType;
import org.seasar.framework.convention.PersistenceConvention;
import org.seasar.framework.exception.EmptyRuntimeException;

/**
 * {@link JdbcManager}の実装クラスです。
 * 
 * @author higa
 * 
 * 
 */
public class JdbcManagerImpl implements JdbcManager, JdbcManagerImplementor {

    /**
     * トランザクション同期レジストリです。
     */
    protected TransactionSynchronizationRegistry syncRegistry;

    /**
     * データソースのファクトリです。
     */
    protected DataSourceFactory dataSourceFactory;

    /**
     * データソースです。
     */
    protected DataSource dataSource;

    /**
     * データベースの方言です。
     */
    protected DbmsDialect dialect;

    /**
     * エンティティメタデータファクトリです。
     */
    protected EntityMetaFactory entityMetaFactory;

    /**
     * 永続化層の規約です。
     */
    protected PersistenceConvention persistenceConvention;

    /**
     * デフォルトの最大行数です。
     */
    protected int maxRows = 0;

    /**
     * デフォルトのフェッチサイズです。
     */
    protected int fetchSize = 0;

    /**
     * デフォルトのクエリタイムアウトです。
     */
    protected int queryTimeout = 0;

    /**
     * バッチ更新で可変のSQLを許可する場合は<code>true</code>です。
     */
    protected boolean allowVariableSqlForBatchUpdate = true;

    public <T> AutoSelect<T> from(Class<T> baseClass) {
        return new AutoSelectImpl<T>(this, baseClass).maxRows(maxRows)
                .fetchSize(fetchSize).queryTimeout(queryTimeout);
    }

    public <T> SqlSelect<T> selectBySql(Class<T> baseClass, String sql,
            Object... params) {
        return new SqlSelectImpl<T>(this, baseClass, sql, params).maxRows(
                maxRows).fetchSize(fetchSize).queryTimeout(queryTimeout);
    }

    public long getCountBySql(String sql, Object... params) {
        return new SqlSelectImpl<Long>(this, Long.class, sql, params)
                .getCount();
    }

    public <T> SqlFileSelect<T> selectBySqlFile(Class<T> baseClass, String path) {
        return selectBySqlFile(baseClass, path, null);
    }

    public <T> SqlFileSelect<T> selectBySqlFile(Class<T> baseClass,
            String path, Object parameter) {
        return new SqlFileSelectImpl<T>(this, baseClass, path, parameter)
                .maxRows(maxRows).fetchSize(fetchSize).queryTimeout(
                        queryTimeout);
    }

    public long getCountBySqlFile(String path) {
        return new SqlFileSelectImpl<Long>(this, Long.class, path, null)
                .getCount();
    }

    public long getCountBySqlFile(String path, Object parameter) {
        return new SqlFileSelectImpl<Long>(this, Long.class, path, parameter)
                .getCount();
    }

    public <T> AutoInsert<T> insert(final T entity) {
        return new AutoInsertImpl<T>(this, entity).queryTimeout(queryTimeout);
    }

    public <T> AutoBatchInsert<T> insertBatch(final T... entities) {
        return new AutoBatchInsertImpl<T>(this, Arrays.asList(entities))
                .queryTimeout(queryTimeout);
    }

    public <T> AutoBatchInsert<T> insertBatch(final List<T> entities) {
        return new AutoBatchInsertImpl<T>(this, entities)
                .queryTimeout(queryTimeout);
    }

    public <T> AutoUpdate<T> update(final T entity) {
        final EntityMeta entityMeta = entityMetaFactory.getEntityMeta(entity
                .getClass());
        if (entityMeta.getIdPropertyMetaList().isEmpty()) {
            throw new NoIdPropertyRuntimeException("ESSR0761", entityMeta
                    .getName());
        }
        return new AutoUpdateImpl<T>(this, entity).queryTimeout(queryTimeout);
    }

    public <T> AutoBatchUpdate<T> updateBatch(final T... entities) {
        if (entities == null) {
            throw new NullPointerException("entities");
        }
        if (entities.length == 0) {
            throw new EmptyRuntimeException("entities");
        }
        final EntityMeta entityMeta = entityMetaFactory
                .getEntityMeta(entities[0].getClass());
        if (entityMeta.getIdPropertyMetaList().isEmpty()) {
            throw new NoIdPropertyRuntimeException("ESSR0761", entityMeta
                    .getName());
        }
        return new AutoBatchUpdateImpl<T>(this, Arrays.asList(entities))
                .queryTimeout(queryTimeout);
    }

    public <T> AutoBatchUpdate<T> updateBatch(final List<T> entities) {
        if (entities == null) {
            throw new NullPointerException("entities");
        }
        if (entities.isEmpty()) {
            throw new EmptyRuntimeException("entities");
        }
        final EntityMeta entityMeta = entityMetaFactory.getEntityMeta(entities
                .get(0).getClass());
        if (entityMeta.getIdPropertyMetaList().isEmpty()) {
            throw new NoIdPropertyRuntimeException("ESSR0761", entityMeta
                    .getName());
        }
        return new AutoBatchUpdateImpl<T>(this, entities)
                .queryTimeout(queryTimeout);
    }

    public SqlUpdate updateBySql(String sql, Class<?>... paramClasses) {
        return new SqlUpdateImpl(this, sql, paramClasses)
                .queryTimeout(queryTimeout);
    }

    public SqlBatchUpdate updateBatchBySql(String sql, Class<?>... paramClasses) {
        return new SqlBatchUpdateImpl(this, sql, paramClasses)
                .queryTimeout(queryTimeout);
    }

    public SqlFileUpdate updateBySqlFile(String path) {
        return new SqlFileUpdateImpl(this, path).queryTimeout(queryTimeout);
    }

    public SqlFileUpdate updateBySqlFile(String path, Object parameter) {
        return new SqlFileUpdateImpl(this, path, parameter)
                .queryTimeout(queryTimeout);
    }

    public <T> SqlFileBatchUpdate<T> updateBatchBySqlFile(String path,
            List<T> params) {
        return new SqlFileBatchUpdateImpl<T>(this, path, params)
                .queryTimeout(queryTimeout);
    }

    public <T> SqlFileBatchUpdate<T> updateBatchBySqlFile(String path,
            T... params) {
        return new SqlFileBatchUpdateImpl<T>(this, path, Arrays.asList(params))
                .queryTimeout(queryTimeout);
    }

    public <T> AutoDelete<T> delete(final T entity) {
        final EntityMeta entityMeta = entityMetaFactory.getEntityMeta(entity
                .getClass());
        if (entityMeta.getIdPropertyMetaList().isEmpty()) {
            throw new NoIdPropertyRuntimeException("ESSR0762", entityMeta
                    .getName());
        }
        return new AutoDeleteImpl<T>(this, entity).queryTimeout(queryTimeout);
    }

    public <T> AutoBatchDelete<T> deleteBatch(final T... entities) {
        if (entities == null) {
            throw new NullPointerException("entities");
        }
        if (entities.length == 0) {
            throw new EmptyRuntimeException("entities");
        }
        final EntityMeta entityMeta = entityMetaFactory
                .getEntityMeta(entities[0].getClass());
        if (entityMeta.getIdPropertyMetaList().isEmpty()) {
            throw new NoIdPropertyRuntimeException("ESSR0762", entityMeta
                    .getName());
        }
        return new AutoBatchDeleteImpl<T>(this, Arrays.asList(entities))
                .queryTimeout(queryTimeout);
    }

    public <T> AutoBatchDelete<T> deleteBatch(final List<T> entities) {
        if (entities == null) {
            throw new NullPointerException("entities");
        }
        if (entities.isEmpty()) {
            throw new EmptyRuntimeException("entities");
        }
        final EntityMeta entityMeta = entityMetaFactory.getEntityMeta(entities
                .get(0).getClass());
        if (entityMeta.getIdPropertyMetaList().isEmpty()) {
            throw new NoIdPropertyRuntimeException("ESSR0762", entityMeta
                    .getName());
        }
        return new AutoBatchDeleteImpl<T>(this, entities)
                .queryTimeout(queryTimeout);
    }

    public AutoProcedureCall call(String procedureName) {
        return call(procedureName, null);
    }

    public AutoProcedureCall call(String procedureName, Object parameter) {
        return new AutoProcedureCallImpl(this, procedureName, parameter)
                .maxRows(maxRows).fetchSize(fetchSize).queryTimeout(
                        queryTimeout);
    }

    public SqlProcedureCall callBySql(String sql) {
        return callBySql(sql, null);
    }

    public SqlProcedureCall callBySql(String sql, Object parameter) {
        return new SqlProcedureCallImpl(this, sql, parameter).maxRows(maxRows)
                .fetchSize(fetchSize).queryTimeout(queryTimeout);
    }

    public SqlFileProcedureCall callBySqlFile(String path) {
        return callBySqlFile(path, null);
    }

    public SqlFileProcedureCall callBySqlFile(String path, Object parameter) {
        return new SqlFileProcedureCallImpl(this, path, parameter).maxRows(
                maxRows).fetchSize(fetchSize).queryTimeout(queryTimeout);
    }

    public <T> AutoFunctionCall<T> call(Class<T> resultClass,
            String functionName) {
        return call(resultClass, functionName, null);
    }

    public <T> AutoFunctionCall<T> call(Class<T> resultClass,
            String functionName, Object parameter) {
        return new AutoFunctionCallImpl<T>(this, resultClass, functionName,
                parameter).maxRows(maxRows).fetchSize(fetchSize).queryTimeout(
                queryTimeout);
    }

    public <T> SqlFunctionCall<T> callBySql(Class<T> resultClass, String sql) {
        return callBySql(resultClass, sql, null);
    }

    public <T> SqlFunctionCall<T> callBySql(Class<T> resultClass, String sql,
            Object parameter) {
        return new SqlFunctionCallImpl<T>(this, resultClass, sql, parameter)
                .maxRows(maxRows).fetchSize(fetchSize).queryTimeout(
                        queryTimeout);
    }

    public <T> SqlFileFunctionCall<T> callBySqlFile(Class<T> resultClass,
            String path) {
        return callBySqlFile(resultClass, path, null);
    }

    public <T> SqlFileFunctionCall<T> callBySqlFile(Class<T> resultClass,
            String path, Object parameter) {
        return new SqlFileFunctionCallImpl<T>(this, resultClass, path,
                parameter).maxRows(maxRows).fetchSize(fetchSize).queryTimeout(
                queryTimeout);
    }

    /**
     * JDBCコンテキストを返します。
     * 
     * @return JDBCコンテキスト
     */
    public JdbcContext getJdbcContext() {
        JdbcContext ctx = getTxBoundJdbcContext();
        if (ctx != null) {
            return ctx;
        }
        Connection con = DataSourceUtil.getConnection(dataSource);
        if (hasTransaction()) {
            ctx = createJdbcContext(con, true);
            setTxBoundJdbcContext(ctx);
        } else {
            ctx = createJdbcContext(con, false);
        }
        return ctx;
    }

    public String getSelectableDataSourceName() {
        return getSelectableDataSourceNameInternal();
    }

    /**
     * 動的なデータソース名を返します。
     * 
     * @return 存在する場合は動的なデータソース名、存在しない場合は <code>null</code>
     */
    protected String getSelectableDataSourceNameInternal() {
        if (dataSource instanceof SelectableDataSourceProxy) {
            if (dataSourceFactory != null) {
                return dataSourceFactory.getSelectableDataSourceName();
            }
        }
        return null;
    }

    /**
     * 現在のトランザクションに関連づけられたJDBCコンテキストを返します。
     * 
     * @return 現在のトランザクションに関連づけられたJDBCコンテキスト
     */
    protected JdbcContext getTxBoundJdbcContext() {
        if (hasTransaction()) {
            final JdbcContextRegistryKey key = createJdbcContextRegistryKey();
            final JdbcContext ctx = JdbcContext.class.cast(syncRegistry
                    .getResource(key));
            if (ctx != null) {
                return ctx;
            }
        }
        return null;
    }

    /**
     * 現在のトランザクションにJDBCコンテキストを関連づけます。
     * 
     * @param ctx
     *            現在のトランザクションに関連づけるJDBCコンテキスト
     */
    protected void setTxBoundJdbcContext(final JdbcContext ctx) {
        final JdbcContextRegistryKey key = createJdbcContextRegistryKey();
        syncRegistry.putResource(key, ctx);
        syncRegistry.registerInterposedSynchronization(new SynchronizationImpl(
                ctx));
    }

    /**
     * JDBCコンテキストの登録キーを作成します。
     * 
     * @return JDBCコンテキストの登録キー
     */
    protected JdbcContextRegistryKey createJdbcContextRegistryKey() {
        return new JdbcContextRegistryKey(getSelectableDataSourceNameInternal());
    }

    /**
     * 現在のスレッドでトランザクションが開始されていれば<code>true</code>を返します。
     * 
     * @return 現在のスレッドでトランザクションが開始されていれば<code>true</code>
     */
    protected boolean hasTransaction() {
        final int status = syncRegistry.getTransactionStatus();
        return status != Status.STATUS_NO_TRANSACTION
                && status != Status.STATUS_UNKNOWN;
    }

    /**
     * JDBCコンテキストがnullかどうかを返します。
     * 
     * @return JDBCコンテキストがnullかどうか
     */
    protected boolean isJdbcContextNull() {
        return getTxBoundJdbcContext() == null;
    }

    /**
     * JDBCコンテキストを作成します。
     * 
     * @param connection
     *            コネクション
     * @param transactional
     *            トランザクション中かどうか
     * @return JDBCコンテキスト
     */
    protected JdbcContext createJdbcContext(Connection connection,
            boolean transactional) {
        return new JdbcContextImpl(connection, transactional);
    }

    /**
     * トランザクション同期レジストリを返します。
     * 
     * @return トランザクション同期レジストリ
     */
    public TransactionSynchronizationRegistry getSyncRegistry() {
        return syncRegistry;
    }

    /**
     * トランザクション同期レジストリを設定します。
     * 
     * @param syncRegistry
     *            トランザクション同期レジストリ
     */
    public void setSyncRegistry(TransactionSynchronizationRegistry syncRegistry) {
        this.syncRegistry = syncRegistry;
    }

    /**
     * データソースを返します。
     * 
     * @return データソース
     */
    public DataSource getDataSource() {
        return dataSource;
    }

    /**
     * データソースを設定します。
     * 
     * @param dataSource
     *            データソース
     */
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

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

    /**
     * デフォルトのフェッチ数を設定します。
     * 
     * @param fetchSize
     *            デフォルトのフェッチ数
     */
    public void setFetchSize(int fetchSize) {
        this.fetchSize = fetchSize;
    }

    /**
     * デフォルトの最大行数を返します。
     * 
     * @return デフォルトの最大行数
     */
    public int getMaxRows() {
        return maxRows;
    }

    /**
     * デフォルトの最大行数を設定します。
     * 
     * @param maxRows
     *            デフォルトの最大行数
     */
    public void setMaxRows(int maxRows) {
        this.maxRows = maxRows;
    }

    /**
     * デフォルトのクエリタイムアウトを返します。
     * 
     * @return デフォルトのクエリタイムアウト
     */
    public int getQueryTimeout() {
        return queryTimeout;
    }

    /**
     * デフォルトのクエリタイムアウトを設定します。
     * 
     * @param queryTimeout
     *            デフォルトのクエリタイムアウト
     */
    public void setQueryTimeout(int queryTimeout) {
        this.queryTimeout = queryTimeout;
    }

    public boolean isAllowVariableSqlForBatchUpdate() {
        return allowVariableSqlForBatchUpdate;
    }

    /**
     * バッチ更新で可変のSQLを許可する場合は<code>true</code>、しない場合は<code>false</code>を設定します。
     * 
     * @param allowVariableSqlForBatchUpdate
     *            バッチ更新で可変のSQLを許可する場合は<code>true</code>、しない場合は<code>false</code>
     */
    public void setAllowVariableSqlForBatchUpdate(
            boolean allowVariableSqlForBatchUpdate) {
        this.allowVariableSqlForBatchUpdate = allowVariableSqlForBatchUpdate;
    }

    /**
     * データソースファクトリを返します。
     * 
     * @return 存在する場合はデータソースファクトリ、存在しない場合は <code>null</code>
     */
    public DataSourceFactory getDataSourceFactory() {
        return dataSourceFactory;
    }

    /**
     * データソースファクトリを設定します。
     * 
     * @param dataSourceFactory
     */
    @Binding(bindingType = BindingType.MAY)
    public void setDataSourceFactory(DataSourceFactory dataSourceFactory) {
        this.dataSourceFactory = dataSourceFactory;
    }

    /**
     * データベースの方言を返します。
     * 
     * @return データベースの方言
     */
    public DbmsDialect getDialect() {
        return dialect;
    }

    /**
     * データベースの方言を設定します。
     * 
     * @param dialect
     *            データベースの方言
     */
    public void setDialect(DbmsDialect dialect) {
        this.dialect = dialect;
    }

    /**
     * エンティティメタデータファクトリを返します。
     * 
     * @return エンティティメタデータファクトリ
     */
    public EntityMetaFactory getEntityMetaFactory() {
        return entityMetaFactory;
    }

    /**
     * エンティティメタデータファクトリを設定します。
     * 
     * @param entityMetaFactory
     *            エンティティメタデータファクトリ
     */
    public void setEntityMetaFactory(EntityMetaFactory entityMetaFactory) {
        this.entityMetaFactory = entityMetaFactory;
    }

    public PersistenceConvention getPersistenceConvention() {
        return persistenceConvention;
    }

    /**
     * 永続化層の規約を設定します。
     * 
     * @param persistenceConvention
     *            永続化層の規約
     */
    public void setPersistenceConvention(
            PersistenceConvention persistenceConvention) {
        this.persistenceConvention = persistenceConvention;
    }

    /**
     * {@link Synchronization}の実装です。
     * 
     * @author koichik
     */
    public class SynchronizationImpl implements Synchronization {

        /** JDBCコンテキスト */
        protected final JdbcContext context;

        /**
         * インスタンスを構築します。
         * 
         * @param context
         *            JDBCコンテキスト
         */
        public SynchronizationImpl(final JdbcContext context) {
            this.context = context;
        }

        public final void beforeCompletion() {
        }

        public void afterCompletion(final int status) {
            context.destroy();
        }

    }

    /**
     * JDBCコンテキストを{@link TransactionSynchronizationRegistry}に登録する際のキーです。
     * 
     * @author taedium
     */
    public class JdbcContextRegistryKey {

        /** データソース名 */
        protected String dataSourceName;

        /**
         * インスタンスを構築します。
         * 
         * @param dataSourceName
         *            データソース名
         */
        public JdbcContextRegistryKey(String dataSourceName) {
            this.dataSourceName = dataSourceName;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + getOuterType().hashCode();
            result = prime
                    * result
                    + ((dataSourceName == null) ? 0 : dataSourceName.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            JdbcContextRegistryKey other = (JdbcContextRegistryKey) obj;
            if (!getOuterType().equals(other.getOuterType())) {
                return false;
            }
            if (dataSourceName == null) {
                if (other.dataSourceName != null) {
                    return false;
                }
            } else if (!dataSourceName.equals(other.dataSourceName)) {
                return false;
            }
            return true;
        }

        private JdbcManagerImpl getOuterType() {
            return JdbcManagerImpl.this;
        }

    }
}
