/*
 * Copyright 2002-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.function.execution;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.geode.cache.execute.Execution;
import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.FunctionException;
import org.apache.geode.cache.execute.FunctionService;
import org.apache.geode.cache.execute.ResultCollector;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Base class for * Creating a GemFire {@link Execution} using {@link FunctionService}.  Protected setters support
 * method chaining.
 *
 * @author David Turanski
 * @author John Blum
 */
abstract class AbstractFunctionExecution {

	private final static String NO_RESULT_MESSAGE = "Cannot return any result as the Function#hasResult() is false";

	private long timeout;

	private Function function;

	protected final Log logger = LogFactory.getLog(this.getClass());

	private Object[] args;

	private volatile ResultCollector<?, ?> resultCollector;

	private String functionId;

	public AbstractFunctionExecution(Function function, Object... args) {
		Assert.notNull(function, "function cannot be null");
		this.function = function;
		this.functionId = function.getId();
		this.args = args;
	}

	public AbstractFunctionExecution(String functionId, Object... args) {
		Assert.isTrue(StringUtils.hasLength(functionId), "functionId cannot be null or empty");
		this.functionId = functionId;
		this.args = args;
	}

	AbstractFunctionExecution() {
	}

	Object[] getArgs() {
		return args;
	}

	ResultCollector<?, ?> getCollector() {
		return resultCollector;
	}

	Function getFunction() {
		return function;
	}

	String getFunctionId() {
		return functionId;
	}

	long getTimeout() {
		return timeout;
	}

	<T> Iterable<T> execute() {
		return execute(true);
	}

	@SuppressWarnings("unchecked")
	<T> Iterable<T> execute(Boolean returnResult) {
		Execution execution = getExecution();

		execution = execution.withArgs(getArgs());
		execution = (getCollector() == null ? execution : execution.withCollector(getCollector()));
		execution = (getKeys() == null ? execution : execution.withFilter(getKeys()));

		ResultCollector<?, ?> resultCollector;

		if (isRegisteredFunction()) {
			resultCollector = execution.execute(functionId);
		}
		else {
			resultCollector = execution.execute(function);

			if (!function.hasResult()) {
				return null;
			}
		}

		if (!returnResult) {
			return null;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("using ResultsCollector " + resultCollector.getClass().getName());
		}

		Iterable<T> results = null;

		try {
			if (this.timeout > 0) {
				try {
					results = (Iterable<T>) resultCollector.getResult(this.timeout, TimeUnit.MILLISECONDS);
				}
				catch (FunctionException e) {
					throw new RuntimeException(e);
				}
				catch (InterruptedException e) {
					throw new RuntimeException(e);
				}
			}
			else {
				results = (Iterable<T>) resultCollector.getResult();
			}

			return replaceSingletonNullCollectionWithEmptyList(results);
		}
		catch (FunctionException e) {
			//TODO Come up with a better way to determine that the function should not return a result;
			if (!e.getMessage().equals(NO_RESULT_MESSAGE)) {
				throw e;
			}
		}

		return results;
	}

	@SuppressWarnings("unchecked")
	<T> T executeAndExtract() {
		Iterable<T> results = execute();

		if (results == null || !results.iterator().hasNext()) {
			return null;
		}

		Object result = results.iterator().next();

		if (result instanceof Throwable) {
			throw new FunctionException(String.format("Execution of Function %1$s failed",
				(function != null ? function.getClass().getName() : String.format("with ID '%1$s'", functionId))),
					(Throwable) result);
		}

		return (T) result;
	}

	protected abstract Execution getExecution();

	protected AbstractFunctionExecution setArgs(Object... args) {
		this.args = args;
		return this;
	}

	protected AbstractFunctionExecution setFunction(Function function) {
		this.function = function;
		return this;
	}

	protected AbstractFunctionExecution setFunctionId(String functionId) {
		this.functionId = functionId;
		return this;
	}

	protected AbstractFunctionExecution setResultCollector(ResultCollector<?, ?> resultCollector) {
		this.resultCollector = resultCollector;
		return this;
	}

	protected AbstractFunctionExecution setTimeout(long timeout) {
		this.timeout = timeout;
		return this;
	}

	protected Set<?> getKeys() {
		return null;
	}

	private boolean isRegisteredFunction() {
		return function == null;
	}

	private <T> Iterable<T> replaceSingletonNullCollectionWithEmptyList(Iterable<T> results) {
		if (results != null) {
			Iterator<T> it = results.iterator();

			if (!it.hasNext()) {
				return results;
			}

			if (it.next() == null && !it.hasNext()) {
				return new ArrayList<T>();
			}
		}

		return results;
	}

}
