package com.wealoha.thrift;

import java.lang.reflect.Proxy;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.thrift.TServiceClient;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.wealoha.thrift.exception.ConnectionFailException;
import com.wealoha.thrift.exception.NoBackendServiceException;
import com.wealoha.thrift.exception.ThriftException;

/**
 * Pool for ThriftClient <br/>
 * <p/>
 * <code>
 * ThriftClientPool pool = new ThriftClientPool(services, clientFactory)
 * </code>
 *
 * @author javamonk
 * @createTime 2014年7月4日 下午3:55:16
 */
public class ThriftClientPool<T extends TServiceClient> {

    private Logger logger = LoggerFactory.getLogger(getClass());

    private final Function<TTransport, T> clientFactory;

    private final GenericObjectPool<ThriftClient<T>> pool;

    private List<ServiceInfo> services;

    private boolean serviceReset = false;

    private final PoolConfig poolConfig;

    /**
     * Construct a new pool using default config
     *
     * @param services
     * @param factory
     */
    public ThriftClientPool(List<ServiceInfo> services, Function<TTransport, T> factory) {
        this(services, factory, new PoolConfig(), null);
    }

    /**
     * Construct a new pool using
     *
     * @param services
     * @param factory All IFace(subclass of TServiceClient) were generated
     *        by thrift. We don't know their types. Since they all extends
     *        super class TServiceClient,
     *        construct a new Client need only just one line:
     *        transport->new Client(new TBinaryProtocol(transport))
     * @param config
     */
    public ThriftClientPool(List<ServiceInfo> services, Function<TTransport, T> factory,
            PoolConfig config) {
        this(services, factory, config, null);
    }

    public ThriftClientPool(List<ServiceInfo> services, Function<TTransport, T> factory,
            PoolConfig config, ThriftProtocolFactory pFactory) {
        if (services == null || services.size() == 0) {
            throw new IllegalArgumentException("services is empty!");
        }
        if (factory == null) {
            throw new IllegalArgumentException("factory is empty!");
        }
        if (config == null) {
            throw new IllegalArgumentException("config is empty!");
        }

        this.services = services;
        this.clientFactory = factory;
        this.poolConfig = config;
        // test if config change
        this.poolConfig.setTestOnReturn(true);
        this.poolConfig.setTestOnBorrow(true);
        this.pool = new GenericObjectPool<>(new BasePooledObjectFactory<ThriftClient<T>>() {

            @Override
            public ThriftClient<T> create() throws Exception {

                // get from global list first
                List<ServiceInfo> serviceList = ThriftClientPool.this.services;
                ServiceInfo serviceInfo = getRandomService(serviceList);
                TTransport transport = getTransport(serviceInfo);

                try {
                    transport.open();
                } catch (TTransportException e) {
                    logger.info("transport open fail service: host={}, port={}",
                            serviceInfo.getHost(), serviceInfo.getPort());
                    if (poolConfig.isFailover()) {
                        while (true) {
                            try {
                                // mark current fail and try next, until none service available
                                serviceList = removeFailService(serviceList, serviceInfo);
                                serviceInfo = getRandomService(serviceList);
                                transport = getTransport(serviceInfo); // while break here
                                logger.info("failover to next service host={}, port={}",
                                        serviceInfo.getHost(), serviceInfo.getPort());
                                transport.open();
                                break;
                            } catch (TTransportException e2) {
                                logger.warn("failover fail, services left: {}", serviceList.size());
                            }
                        }
                    } else {
                        throw new ConnectionFailException("host=" + serviceInfo.getHost() + ", ip="
                                + serviceInfo.getPort(), e);
                    }
                }

                ThriftClient<T> client = new ThriftClient<>(clientFactory.apply(transport), pool,
                        serviceInfo);

                logger.debug("create new object for pool {}", client);
                return client;
            }

            @Override
            public PooledObject<ThriftClient<T>> wrap(ThriftClient<T> obj) {
                return new DefaultPooledObject<>(obj);
            }

            @Override
            public boolean validateObject(PooledObject<ThriftClient<T>> p) {
                ThriftClient<T> client = p.getObject();

                // check if return client in current service list if 
                if (serviceReset) {
                    if (!ThriftClientPool.this.services.contains(client.getServiceInfo())) {
                        logger.warn("not return object because it's from previous config {}",
                                client);
                        client.closeClient();
                        return false;
                    }
                }

                return super.validateObject(p);
            }

            @Override
            public void destroyObject(PooledObject<ThriftClient<T>> p) throws Exception {
                p.getObject().closeClient();
                super.destroyObject(p);
            }
        }, poolConfig);
    }

    public List<ServiceInfo> getServices() {
        return services;
    }

    /**
     * set new services for this pool
     *
     * @param services
     */
    public void setServices(List<ServiceInfo> services) {
        if (services == null || services.size() == 0) {
            throw new IllegalArgumentException("services is empty!");
        }
        this.services = services;
        serviceReset = true;
    }

    private TTransport getTransport(ServiceInfo serviceInfo) {

        if (serviceInfo == null) {
            throw new NoBackendServiceException();
        }

        TTransport transport;
        if (poolConfig.getTimeout() > 0) {
            transport = new TSocket(serviceInfo.getHost(), serviceInfo.getPort(),
                    poolConfig.getTimeout());
        } else {
            transport = new TSocket(serviceInfo.getHost(), serviceInfo.getPort());
        }
        return transport;
    }

    /**
     * get a random service
     *
     * @param serviceList
     * @return
     */
    private ServiceInfo getRandomService(List<ServiceInfo> serviceList) {
        if (serviceList == null || serviceList.size() == 0) {
            return null;
        }
        return serviceList.get(RandomUtils.nextInt(0, serviceList.size()));
    }

    private List<ServiceInfo> removeFailService(List<ServiceInfo> list, ServiceInfo serviceInfo) {
        logger.info("remove service from current service list: host={}, port={}",
                serviceInfo.getHost(), serviceInfo.getPort());
        return list.stream() //
                .filter(si -> !serviceInfo.equals(si)) //
                .collect(Collectors.toList());
    }

    /**
     * get a client from pool
     *
     * @return
     * @throws ThriftException
     * @throws NoBackendServiceException if
     *         {@link PoolConfig#setFailover(boolean)} is set and no
     *         service can connect to
     * @throws ConnectionFailException if
     *         {@link PoolConfig#setFailover(boolean)} not set and
     *         connection fail
     */
    public ThriftClient<T> getClient() throws ThriftException {
        try {
            return pool.borrowObject();
        } catch (Exception e) {
            if (e instanceof ThriftException) {
                throw (ThriftException) e;
            }
            throw new ThriftException("Get client from pool failed.", e);
        }
    }

    /**
     * get a client's IFace from pool
     * <p/>
     * <ul>
     * <li>
     * <span style="color:red">Important: Iface is totally generated by
     * thrift, a ClassCastException will be thrown if assign not
     * match!</span></li>
     * <li>
     * <span style="color:red">Limitation: The return object can only used
     * once.</span></li>
     * </ul>
     *
     * @return
     * @throws ThriftException
     * @throws NoBackendServiceException if
     *         {@link PoolConfig#setFailover(boolean)} is set and no
     *         service can connect to
     * @throws ConnectionFailException if
     *         {@link PoolConfig#setFailover(boolean)} not set and
     *         connection fail
     * @throws IllegalStateException if call method on return object twice
     */
    @SuppressWarnings("unchecked")
    public <X> X iface() throws ThriftException {
        ThriftClient<T> client;
        try {
            client = pool.borrowObject();
        } catch (Exception e) {
            if (e instanceof ThriftException) {
                throw (ThriftException) e;
            }
            throw new ThriftException("Get client from pool failed.", e);
        }
        AtomicBoolean returnToPool = new AtomicBoolean(false);
        return (X) Proxy.newProxyInstance(this.getClass().getClassLoader(), client.iFace()
                .getClass().getInterfaces(), (proxy, method, args) -> {
            if (returnToPool.get()) {
                throw new IllegalStateException("Object returned via iface can only used once!");
            }
            boolean success = false;
            try {
                Object result = method.invoke(client.iFace(), args);
                success = true;
                return result;
            } catch (Throwable e) {
                logger.warn("invoke fail", e);
                throw e;
            } finally {
                if (success) {
                    pool.returnObject(client);
                } else {
                    client.closeClient();
                    pool.invalidateObject(client);
                }
                returnToPool.set(true);
            }
        });
    }

    @Override
    protected void finalize() throws Throwable {
        if (pool != null) {
            pool.close();
        }
        super.finalize();
    }
}