/*
 * Copyright 2002-2019 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
 *
 * https://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;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.ResultSender;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

/**
 * Sends {@link Collection} {@link Function} results using a {@link ResultSender} in chunks
 * determined by {@code batchSize}.
 *
 * @author David Turanski
 * @author Udo Kohlmeyer
 * @author John Blum
 * @since 1.3.0
 */
class BatchingResultSender {

	private final int batchSize;

	private ResultSender<Object> resultSender;

	/**
	 * Constructs a new instance of {@link BatchingResultSender} initialized with the given {@link Integer batch size}
	 * and {@link ResultSender} object used to delegate all send operations.
	 *
	 * @param batchSize {@link Integer} specifying the configured batch size.
	 * @param resultSender {@link ResultSender} used to delegate all send operations.
	 * @throws IllegalArgumentException if {@link ResultSender} is {@literal null}
	 * or {@code batchSize} is less than {@literal 0}.
	 * @see org.apache.geode.cache.execute.ResultSender
	 */
	public BatchingResultSender(int batchSize, ResultSender<Object> resultSender) {

		Assert.notNull(resultSender, "ResultSender must not be null");
		Assert.isTrue(batchSize >= 0, "batchSize must be greater than equal to 0");

		this.batchSize = batchSize;
		this.resultSender = resultSender;
	}

	/**
	 * Returns the configured {@link Integer batchSize} of this batching {@link ResultSender}.
	 *
	 * @return an {@link Integer} value specifying the configured {@link Integer batchSize}
	 * of this batching {@link ResultSender}.
	 */
	public int getBatchSize() {
		return this.batchSize;
	}

	/**
	 * Returns a reference to the configured {@link ResultSender} used to send {@link Function} results.
	 *
	 * @return a reference to the configured {@link ResultSender} used to send {@link Function} results.
	 * @see org.apache.geode.cache.execute.ResultSender
	 */
	public ResultSender<Object> getResultSender() {
		return this.resultSender;
	}

	protected boolean isBatchingDisabled() {
		return !isBatchingEnabled();
	}

	protected boolean isBatchingEnabled() {
		return getBatchSize() > 0;
	}

	protected boolean doNotSendChunks(boolean resultSetIsEmpty) {
		return resultSetIsEmpty || isBatchingDisabled();
	}

	public void sendResults(Iterable<?> result) {

		ResultSender<Object> resultSender = getResultSender();

		if (doNotSendChunks(!result.iterator().hasNext())) {
			resultSender.lastResult(result);
		}
		else {

			int batchSize = getBatchSize();

			List<Object> chunk = new ArrayList<>(batchSize);

			for (Iterator<?> it = result.iterator(); it.hasNext(); ) {

				if (chunk.size() < batchSize) {
					chunk.add(it.next());
				}

				if (chunk.size() == batchSize || !it.hasNext()) {

					if (it.hasNext()) {
						resultSender.sendResult(chunk);
					}
					else {
						resultSender.lastResult(chunk);
					}

					chunk.clear();
				}
			}
		}
	}

	public void sendArrayResults(Object result) {

		Assert.isTrue(ObjectUtils.isArray(result),
			() -> String.format("Object must be an array; was [%s]", ObjectUtils.nullSafeClassName(result)));

		int arrayLength = Array.getLength(result);

		ResultSender<Object> resultSender = getResultSender();

		if (doNotSendChunks(arrayLength == 0)) {
			resultSender.lastResult(result);
		}
		else {

			int batchSize = getBatchSize();

			for (int from = 0; from < arrayLength; from += batchSize) {

				int to = Math.min(arrayLength, from + batchSize);

				Object chunk = copyOfRange(result, from, to);

				if (to == arrayLength) {
					resultSender.lastResult(chunk);
				}
				else {
					resultSender.sendResult(chunk);
				}
			}
		}
	}

	private Object copyOfRange(Object result, int from, int to) {

		Class<?> resultType = result.getClass();

		if (boolean[].class.isAssignableFrom(resultType)) {
			return Arrays.copyOfRange((boolean[]) result, from, to);
		}
		else if (byte[].class.isAssignableFrom(resultType)) {
			return Arrays.copyOfRange((byte[]) result, from, to);
		}
		else if (short[].class.isAssignableFrom(resultType)) {
			return Arrays.copyOfRange((short[]) result, from, to);
		}
		else if (int[].class.isAssignableFrom(resultType)) {
			return Arrays.copyOfRange((int[]) result, from, to);
		}
		else if (long[].class.isAssignableFrom(resultType)) {
			return Arrays.copyOfRange((long[]) result, from, to);
		}
		else if (float[].class.isAssignableFrom(resultType)) {
			return Arrays.copyOfRange((float[]) result, from, to);
		}
		else if (double[].class.isAssignableFrom(resultType)) {
			return Arrays.copyOfRange((double[]) result, from, to);
		}
		else if (char[].class.isAssignableFrom(resultType)) {
			return Arrays.copyOfRange((char[]) result, from, to);
		}
		else {
			return Arrays.copyOfRange((Object[]) result, from, to);
		}
	}
}
