/**
 * Copyright 1999-2011 Alibaba Group
 *
 * 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 com.alibaba.cobar.client.support.execution;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import javax.sql.DataSource;

import org.apache.commons.lang.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
import org.springframework.orm.ibatis.SqlMapClientCallback;

import com.alibaba.cobar.client.support.utils.CollectionUtils;
import com.ibatis.sqlmap.client.SqlMapClient;
import com.ibatis.sqlmap.client.SqlMapSession;

public class DefaultConcurrentRequestProcessor implements IConcurrentRequestProcessor {

    private transient final Logger logger = LoggerFactory
                                                  .getLogger(DefaultConcurrentRequestProcessor.class);

    private SqlMapClient           sqlMapClient;

    public DefaultConcurrentRequestProcessor() {
    }

    public DefaultConcurrentRequestProcessor(SqlMapClient sqlMapClient) {
        this.sqlMapClient = sqlMapClient;
    }

    public List<Object> process(List<ConcurrentRequest> requests) {
        List<Object> resultList = new ArrayList<Object>();

        if (CollectionUtils.isEmpty(requests))
            return resultList;

        List<RequestDepository> requestsDepo = fetchConnectionsAndDepositForLaterUse(requests);
        final CountDownLatch latch = new CountDownLatch(requestsDepo.size());
        List<Future<Object>> futures = new ArrayList<Future<Object>>();
        try {

            for (RequestDepository rdepo : requestsDepo) {
                ConcurrentRequest request = rdepo.getOriginalRequest();
                final SqlMapClientCallback action = request.getAction();
                final Connection connection = rdepo.getConnectionToUse();

                futures.add(request.getExecutor().submit(new Callable<Object>() {
                    public Object call() throws Exception {
                        try {
                            return executeWith(connection, action);
                        } finally {
                            latch.countDown();
                        }
                    }
                }));
            }

            try {
                latch.await();
            } catch (InterruptedException e) {
                throw new ConcurrencyFailureException(
                        "interrupted when processing data access request in concurrency", e);
            }

        } finally {
            for (RequestDepository depo : requestsDepo) {
                Connection springCon = depo.getConnectionToUse();
                DataSource dataSource = depo.getOriginalRequest().getDataSource();
                try {
                    if (springCon != null) {
                        if (depo.isTransactionAware()) {
                            springCon.close();
                        } else {
                            DataSourceUtils.doReleaseConnection(springCon, dataSource);
                        }
                    }
                } catch (Throwable ex) {
                    logger.info("Could not close JDBC Connection", ex);
                }
            }
        }

        fillResultListWithFutureResults(futures, resultList);

        return resultList;
    }

    protected Object executeWith(Connection connection, SqlMapClientCallback action) {
        SqlMapSession session = getSqlMapClient().openSession();
        try {
            try {
                session.setUserConnection(connection);
            } catch (SQLException e) {
                throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", e);
            }
            try {
                return action.doInSqlMapClient(session);
            } catch (SQLException ex) {
                throw new SQLErrorCodeSQLExceptionTranslator().translate("SqlMapClient operation",
                        null, ex);
            }
        } finally {
            session.close();
        }
    }

    private void fillResultListWithFutureResults(List<Future<Object>> futures,
                                                 List<Object> resultList) {
        for (Future<Object> future : futures) {
            try {
                resultList.add(future.get());
            } catch (InterruptedException e) {
                throw new ConcurrencyFailureException(
                        "interrupted when processing data access request in concurrency", e);
            } catch (ExecutionException e) {
                throw new ConcurrencyFailureException("something goes wrong in processing", e);
            }
        }
    }

    private List<RequestDepository> fetchConnectionsAndDepositForLaterUse(
                                                                          List<ConcurrentRequest> requests) {
        List<RequestDepository> depos = new ArrayList<RequestDepository>();
        for (ConcurrentRequest request : requests) {
            DataSource dataSource = request.getDataSource();

            Connection springCon = null;
            boolean transactionAware = (dataSource instanceof TransactionAwareDataSourceProxy);
            try {
                springCon = (transactionAware ? dataSource.getConnection() : DataSourceUtils
                        .doGetConnection(dataSource));
            } catch (SQLException ex) {
                throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
            }

            RequestDepository depo = new RequestDepository();
            depo.setOriginalRequest(request);
            depo.setConnectionToUse(springCon);
            depo.setTransactionAware(transactionAware);
            depos.add(depo);
        }

        return depos;
    }

    public void setSqlMapClient(SqlMapClient sqlMapClient) {
        Validate.notNull(sqlMapClient);
        this.sqlMapClient = sqlMapClient;
    }

    public SqlMapClient getSqlMapClient() {
        return sqlMapClient;
    }

}