/*
 * Copyright 2010-2013 the original author or authors.
 *
 * 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.springframework.data.gemfire.client;

import java.net.InetSocketAddress;
import java.util.List;

import com.gemstone.gemfire.cache.client.ClientCache;
import com.gemstone.gemfire.cache.client.Pool;
import com.gemstone.gemfire.cache.client.PoolFactory;
import com.gemstone.gemfire.cache.client.PoolManager;
import com.gemstone.gemfire.cache.query.QueryService;
import com.gemstone.gemfire.distributed.DistributedSystem;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.gemfire.GemfireUtils;
import org.springframework.data.gemfire.support.ConnectionEndpoint;
import org.springframework.data.gemfire.support.ConnectionEndpointList;
import org.springframework.data.gemfire.util.DistributedSystemUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * FactoryBean for easy declaration and configuration of a GemFire {@link Pool}. If a new {@link Pool} is created,
 * its lifecycle is bound to that of this declaring factory.
 *
 * Note, if a {@link Pool} having the configured name already exists, then the existing {@link Pool} will be returned
 * as is without any modifications and its lifecycle will be unaffected by this factory.
 *
 * @author Costin Leau
 * @author John Blum
 * @see java.net.InetSocketAddress
 * @see org.springframework.beans.factory.BeanNameAware
 * @see org.springframework.beans.factory.DisposableBean
 * @see org.springframework.beans.factory.FactoryBean
 * @see org.springframework.beans.factory.InitializingBean
 * @see org.springframework.data.gemfire.support.ConnectionEndpoint
 * @see org.springframework.data.gemfire.support.ConnectionEndpointList
 * @see com.gemstone.gemfire.cache.client.Pool
 * @see com.gemstone.gemfire.cache.client.PoolFactory
 * @see com.gemstone.gemfire.cache.client.PoolManager
 */
@SuppressWarnings("unused")
public class PoolFactoryBean implements FactoryBean<Pool>, InitializingBean, DisposableBean,
		BeanNameAware, BeanFactoryAware {

	protected static final int DEFAULT_LOCATOR_PORT = DistributedSystemUtils.DEFAULT_LOCATOR_PORT;
	protected static final int DEFAULT_SERVER_PORT = DistributedSystemUtils.DEFAULT_CACHE_SERVER_PORT;

	private static final Log log = LogFactory.getLog(PoolFactoryBean.class);

	// indicates whether the Pool has been created internally (by this FactoryBean) or not
	volatile boolean springBasedPool = true;

	private BeanFactory beanFactory;

	private ConnectionEndpointList locators = new ConnectionEndpointList();
	private ConnectionEndpointList servers = new ConnectionEndpointList();

	private volatile Pool pool;

	private String beanName;
	private String name;

	// GemFire Pool Configuration Settings
	private boolean keepAlive = false;
	private boolean multiUserAuthentication = PoolFactory.DEFAULT_MULTIUSER_AUTHENTICATION;
	private boolean prSingleHopEnabled = PoolFactory.DEFAULT_PR_SINGLE_HOP_ENABLED;
	private boolean subscriptionEnabled = PoolFactory.DEFAULT_SUBSCRIPTION_ENABLED;
	private boolean threadLocalConnections = PoolFactory.DEFAULT_THREAD_LOCAL_CONNECTIONS;

	private int freeConnectionTimeout = PoolFactory.DEFAULT_FREE_CONNECTION_TIMEOUT;
	private int loadConditioningInterval = PoolFactory.DEFAULT_LOAD_CONDITIONING_INTERVAL;
	private int maxConnections = PoolFactory.DEFAULT_MAX_CONNECTIONS;
	private int minConnections = PoolFactory.DEFAULT_MIN_CONNECTIONS;
	private int readTimeout = PoolFactory.DEFAULT_READ_TIMEOUT;
	private int retryAttempts = PoolFactory.DEFAULT_RETRY_ATTEMPTS;
	private int socketBufferSize = PoolFactory.DEFAULT_SOCKET_BUFFER_SIZE;
	private int statisticInterval = PoolFactory.DEFAULT_STATISTIC_INTERVAL;
	private int subscriptionAckInterval = PoolFactory.DEFAULT_SUBSCRIPTION_ACK_INTERVAL;
	private int subscriptionMessageTrackingTimeout = PoolFactory.DEFAULT_SUBSCRIPTION_MESSAGE_TRACKING_TIMEOUT;
	private int subscriptionRedundancy = PoolFactory.DEFAULT_SUBSCRIPTION_REDUNDANCY;

	private long idleTimeout = PoolFactory.DEFAULT_IDLE_TIMEOUT;
	private long pingInterval = PoolFactory.DEFAULT_PING_INTERVAL;

	private String serverGroup = PoolFactory.DEFAULT_SERVER_GROUP;

	/**
	 * Constructs and initializes a GemFire {@link Pool}.
	 *
	 * @throws Exception if the {@link Pool} creation and initialization fails.
	 * @see com.gemstone.gemfire.cache.client.Pool
	 * @see com.gemstone.gemfire.cache.client.PoolFactory
	 * @see com.gemstone.gemfire.cache.client.PoolManager
	 * @see #createPoolFactory()
	 */
	@Override
	public void afterPropertiesSet() throws Exception {
		if (!StringUtils.hasText(name)) {
			Assert.hasText(beanName, "Pool 'name' is required");
			name = beanName;
		}

		// check for an existing, configured Pool with name first
		Pool existingPool = PoolManager.find(name);

		if (existingPool != null) {
			if (log.isDebugEnabled()) {
				log.debug(String.format("A Pool with name [%1$s] already exists; using existing Pool.", name));
			}

			springBasedPool = false;
			pool = existingPool;
		}
		else {
			if (log.isDebugEnabled()) {
				log.debug(String.format("No Pool with name [%1$s] was found. Creating new Pool.", name));
			}

			springBasedPool = true;
		}
	}

	/**
	 * Destroys the GemFire {@link Pool} if created by this {@link PoolFactoryBean} and releases all system resources
	 * used by the {@link Pool}.
	 *
	 * @throws Exception if the {@link Pool} destruction caused an error.
	 * @see DisposableBean#destroy()
	 */
	@Override
	public void destroy() throws Exception {
		if (springBasedPool && pool != null && !pool.isDestroyed()) {
			pool.releaseThreadLocalConnection();
			pool.destroy(keepAlive);
			pool = null;

			if (log.isDebugEnabled()) {
				log.debug(String.format("Destroyed Pool [%1$s]", name));
			}
		}
	}

	/* (non-Javadoc) */
	@Override
	public Pool getObject() throws Exception {
		if (pool == null) {
			eagerlyInitializeClientCacheIfNotPresent();

			PoolFactory poolFactory = createPoolFactory();

			poolFactory.setFreeConnectionTimeout(freeConnectionTimeout);
			poolFactory.setIdleTimeout(idleTimeout);
			poolFactory.setLoadConditioningInterval(loadConditioningInterval);
			poolFactory.setMaxConnections(maxConnections);
			poolFactory.setMinConnections(minConnections);
			poolFactory.setMultiuserAuthentication(multiUserAuthentication);
			poolFactory.setPingInterval(pingInterval);
			poolFactory.setPRSingleHopEnabled(prSingleHopEnabled);
			poolFactory.setReadTimeout(readTimeout);
			poolFactory.setRetryAttempts(retryAttempts);
			poolFactory.setServerGroup(serverGroup);
			poolFactory.setSocketBufferSize(socketBufferSize);
			poolFactory.setStatisticInterval(statisticInterval);
			poolFactory.setSubscriptionAckInterval(subscriptionAckInterval);
			poolFactory.setSubscriptionEnabled(subscriptionEnabled);
			poolFactory.setSubscriptionMessageTrackingTimeout(subscriptionMessageTrackingTimeout);
			poolFactory.setSubscriptionRedundancy(subscriptionRedundancy);
			poolFactory.setThreadLocalConnections(threadLocalConnections);

			for (ConnectionEndpoint locator : locators) {
				poolFactory.addLocator(locator.getHost(), locator.getPort());
			}

			for (ConnectionEndpoint server : servers) {
				poolFactory.addServer(server.getHost(), server.getPort());
			}

			pool = poolFactory.create(name);
		}

		return pool;
	}

	/**
	 * Attempts to eagerly initialize the GemFire {@link ClientCache} if not already present so that the single
	 * {@link com.gemstone.gemfire.distributed.DistributedSystem} will exists, which is required to create
	 * a {@link Pool} instance.
	 *
	 * @see org.springframework.beans.factory.BeanFactory#getBean(Class)
	 * @see com.gemstone.gemfire.cache.client.ClientCache
	 */
	void eagerlyInitializeClientCacheIfNotPresent() {
		if (!isDistributedSystemPresent()) {
			getBeanFactory().getBean(ClientCache.class);
		}
	}

	/**
	 * Determines whether the GemFire DistributedSystem exists yet or not.
	 *
	 * @return a boolean value indicating whether the single, GemFire DistributedSystem has been created already.
	 * @see org.springframework.data.gemfire.GemfireUtils#getDistributedSystem()
	 * @see org.springframework.data.gemfire.GemfireUtils#isConnected(DistributedSystem)
	 * @see com.gemstone.gemfire.distributed.DistributedSystem
	 */
	boolean isDistributedSystemPresent() {
		return GemfireUtils.isConnected(GemfireUtils.getDistributedSystem());
	}

	/**
	 * Creates an instance of the GemFire {@link PoolFactory} interface to construct, configure and initialize
	 * a GemFire {@link Pool}.
	 *
	 * @return a {@link PoolFactory} implementation to create a {@link Pool}.
	 * @see com.gemstone.gemfire.cache.client.PoolManager#createFactory()
	 * @see com.gemstone.gemfire.cache.client.PoolFactory
	 */
	protected PoolFactory createPoolFactory() {
		return PoolManager.createFactory();
	}

	/* (non-Javadoc) */
	@Override
	public Class<?> getObjectType() {
		return (pool != null ? pool.getClass() : Pool.class);
	}

	/* (non-Javadoc) */
	@Override
	public boolean isSingleton() {
		return true;
	}

	/* (non-Javadoc) */
	public void addLocators(ConnectionEndpoint... locators) {
		this.locators.add(locators);
	}

	/* (non-Javadoc) */
	public void addLocators(Iterable<ConnectionEndpoint> locators) {
		this.locators.add(locators);
	}

	/* (non-Javadoc) */
	public void addServers(ConnectionEndpoint... servers) {
		this.servers.add(servers);
	}

	/* (non-Javadoc) */
	public void addServers(Iterable<ConnectionEndpoint> servers) {
		this.servers.add(servers);
	}

	/* (non-Javadoc) */
	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		this.beanFactory = beanFactory;
	}

	/* (non-Javadoc) */
	protected BeanFactory getBeanFactory() {
		return beanFactory;
	}

	/* (non-Javadoc) */
	public void setBeanName(String name) {
		this.beanName = name;
	}

	/* (non-Javadoc) */
	public void setName(String name) {
		this.name = name;
	}

	/* (non-Javadoc) */
	String getName() {
		return name;
	}

	/* (non-Javadoc) */
	public void setPool(Pool pool) {
		this.pool = pool;
	}

	/* (non-Javadoc) */
	public Pool getPool() {
		return (pool != null ? pool : new PoolAdapter() {
			@Override
			public boolean isDestroyed() {
				Pool pool = PoolFactoryBean.this.pool;
				return (pool != null && pool.isDestroyed());
			}

			@Override
			public int getFreeConnectionTimeout() {
				return PoolFactoryBean.this.freeConnectionTimeout;
			}

			@Override
			public long getIdleTimeout() {
				return PoolFactoryBean.this.idleTimeout;
			}

			@Override
			public int getLoadConditioningInterval() {
				return PoolFactoryBean.this.loadConditioningInterval;
			}

			@Override
			public List<InetSocketAddress> getLocators() {
				return PoolFactoryBean.this.locators.toInetSocketAddresses();
			}

			@Override
			public int getMaxConnections() {
				return PoolFactoryBean.this.maxConnections;
			}

			@Override
			public int getMinConnections() {
				return PoolFactoryBean.this.minConnections;
			}

			@Override
			public boolean getMultiuserAuthentication() {
				return PoolFactoryBean.this.multiUserAuthentication;
			}

			@Override
			public String getName() {
				String name = PoolFactoryBean.this.name;
				name = (StringUtils.hasText(name) ? name : PoolFactoryBean.this.beanName);
				return name;
			}

			@Override
			public int getPendingEventCount() {
				Pool pool = PoolFactoryBean.this.pool;
				if (pool != null) {
					return pool.getPendingEventCount();
				}
				throw new IllegalStateException("The Pool is not initialized");
			}

			@Override
			public long getPingInterval() {
				return PoolFactoryBean.this.pingInterval;
			}

			@Override
			public boolean getPRSingleHopEnabled() {
				return PoolFactoryBean.this.prSingleHopEnabled;
			}

			@Override
			public QueryService getQueryService() {
				Pool pool = PoolFactoryBean.this.pool;
				if (pool != null) {
					return pool.getQueryService();
				}
				throw new IllegalStateException("The Pool is not initialized");
			}

			@Override
			public int getReadTimeout() {
				return PoolFactoryBean.this.readTimeout;
			}

			@Override
			public int getRetryAttempts() {
				return PoolFactoryBean.this.retryAttempts;
			}

			@Override
			public String getServerGroup() {
				return PoolFactoryBean.this.serverGroup;
			}

			@Override
			public List<InetSocketAddress> getServers() {
				return PoolFactoryBean.this.servers.toInetSocketAddresses();
			}

			@Override
			public int getSocketBufferSize() {
				return PoolFactoryBean.this.socketBufferSize;
			}

			@Override
			public int getStatisticInterval() {
				return PoolFactoryBean.this.statisticInterval;
			}

			@Override
			public int getSubscriptionAckInterval() {
				return PoolFactoryBean.this.subscriptionAckInterval;
			}

			@Override
			public boolean getSubscriptionEnabled() {
				return PoolFactoryBean.this.subscriptionEnabled;
			}

			@Override
			public int getSubscriptionMessageTrackingTimeout() {
				return PoolFactoryBean.this.subscriptionMessageTrackingTimeout;
			}

			@Override
			public int getSubscriptionRedundancy() {
				return PoolFactoryBean.this.subscriptionRedundancy;
			}

			@Override
			public boolean getThreadLocalConnections() {
				return PoolFactoryBean.this.threadLocalConnections;
			}

			@Override
			public void destroy() {
				destroy(false);
			}

			@Override
			public void destroy(final boolean keepAlive) {
				try {
					PoolFactoryBean.this.destroy();
				}
				catch (Exception ignore) {
					Pool pool = PoolFactoryBean.this.pool;
					if (pool != null) {
						pool.destroy(keepAlive);
					}
				}
			}

			@Override
			public void releaseThreadLocalConnection() {
				Pool pool = PoolFactoryBean.this.pool;
				if (pool != null) {
					pool.releaseThreadLocalConnection();
				}
				else {
					throw new IllegalStateException("The Pool is not initialized");
				}
			}
		});
	}

	/* (non-Javadoc) */
	public void setFreeConnectionTimeout(int freeConnectionTimeout) {
		this.freeConnectionTimeout = freeConnectionTimeout;
	}

	/* (non-Javadoc) */
	public void setIdleTimeout(long idleTimeout) {
		this.idleTimeout = idleTimeout;
	}

	/* (non-Javadoc) */
	public void setKeepAlive(boolean keepAlive) {
		this.keepAlive = keepAlive;
	}

	/* (non-Javadoc) */
	public void setLoadConditioningInterval(int loadConditioningInterval) {
		this.loadConditioningInterval = loadConditioningInterval;
	}

	/* (non-Javadoc) */
	public void setLocators(ConnectionEndpoint[] connectionEndpoints) {
		setLocators(ConnectionEndpointList.from(connectionEndpoints));
	}

	/* (non-Javadoc) */
	public void setLocators(Iterable<ConnectionEndpoint> connectionEndpoints) {
		getLocators().clear();
		getLocators().add(connectionEndpoints);
	}

	/* (non-Javadoc) */
	ConnectionEndpointList getLocators() {
		return locators;
	}

	/* (non-Javadoc) */
	public void setMaxConnections(int maxConnections) {
		this.maxConnections = maxConnections;
	}

	/* (non-Javadoc) */
	public void setMinConnections(int minConnections) {
		this.minConnections = minConnections;
	}

	/* (non-Javadoc) */
	public void setMultiUserAuthentication(boolean multiUserAuthentication) {
		this.multiUserAuthentication = multiUserAuthentication;
	}

	/* (non-Javadoc) */
	public void setPingInterval(long pingInterval) {
		this.pingInterval = pingInterval;
	}

	/* (non-Javadoc) */
	public void setPrSingleHopEnabled(boolean prSingleHopEnabled) {
		this.prSingleHopEnabled = prSingleHopEnabled;
	}

	/* (non-Javadoc) */
	public void setReadTimeout(int readTimeout) {
		this.readTimeout = readTimeout;
	}

	/* (non-Javadoc) */
	public void setRetryAttempts(int retryAttempts) {
		this.retryAttempts = retryAttempts;
	}

	/* (non-Javadoc) */
	public void setServerGroup(String serverGroup) {
		this.serverGroup = serverGroup;
	}

	/* (non-Javadoc) */
	public void setServers(ConnectionEndpoint[] connectionEndpoints) {
		setServers(ConnectionEndpointList.from(connectionEndpoints));
	}

	/* (non-Javadoc) */
	public void setServers(Iterable<ConnectionEndpoint> connectionEndpoints) {
		getServers().clear();
		getServers().add(connectionEndpoints);
	}

	/* (non-Javadoc) */
	ConnectionEndpointList getServers() {
		return servers;
	}

	/* (non-Javadoc) */
	public void setSocketBufferSize(int socketBufferSize) {
		this.socketBufferSize = socketBufferSize;
	}

	/* (non-Javadoc) */
	public void setStatisticInterval(int statisticInterval) {
		this.statisticInterval = statisticInterval;
	}

	/* (non-Javadoc) */
	public void setSubscriptionAckInterval(int subscriptionAckInterval) {
		this.subscriptionAckInterval = subscriptionAckInterval;
	}

	/* (non-Javadoc) */
	public void setSubscriptionEnabled(boolean subscriptionEnabled) {
		this.subscriptionEnabled = subscriptionEnabled;
	}

	/* (non-Javadoc) */
	public void setSubscriptionMessageTrackingTimeout(int subscriptionMessageTrackingTimeout) {
		this.subscriptionMessageTrackingTimeout = subscriptionMessageTrackingTimeout;
	}

	/* (non-Javadoc) */
	public void setSubscriptionRedundancy(int subscriptionRedundancy) {
		this.subscriptionRedundancy = subscriptionRedundancy;
	}

	/* (non-Javadoc) */
	public void setThreadLocalConnections(boolean threadLocalConnections) {
		this.threadLocalConnections = threadLocalConnections;
	}
}
