/*
 * Copyright 2006-2021 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.toolkit.modules.concurrency.internal;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import de.rcenvironment.toolkit.modules.concurrency.api.AsyncCallbackExceptionPolicy;
import de.rcenvironment.toolkit.modules.concurrency.api.AsyncOrderedExecutionQueue;
import de.rcenvironment.toolkit.modules.concurrency.api.BatchAggregator;
import de.rcenvironment.toolkit.modules.concurrency.api.BatchProcessor;
import de.rcenvironment.toolkit.modules.concurrency.api.ConcurrencyUtilsFactory;
import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription;

/**
 * A generic class to group sequentially-generated elements into ordered batches. A batch is returned (to a given {@link BatchProcessor})
 * when a maximum number of elements is reached, or after a specified time has elapsed since the batch was created. Batches are created
 * implicitly when an element is added while no batch is already active.
 * 
 * @param <T> the element type to aggregate
 * 
 * @author Robert Mischke
 */
public class BatchAggregatorImpl<T> implements BatchAggregator<T> {

    /**
     * A Runnable to check for maximum latency timeouts.
     * 
     * @author Robert Mischke
     */
    private final class MaxLatencyTimerCallback implements Runnable {

        private List<T> relevantBatch;

        MaxLatencyTimerCallback(List<T> relevantBatch) {
            this.relevantBatch = relevantBatch;
        }

        @Override
        @TaskDescription("BatchAggregator Max Latency Timer")
        public void run() {
            // delegate for proper synchronization
            onMaxLatencyTimerCallback(relevantBatch);
        }
    }

    /**
     * A Runnable to dispatch a completed batch to the configured {@link BatchProcessor}.
     * 
     * @param <T> the element type of the batch that should be dispatched;
     * 
     * @author Robert Mischke
     */
    // TODO this class was static before making statisticsTrackerService a field; review
    private final class DispatchRunnable<T> implements Runnable {

        private static final String ASYNC_TASK_DESCRIPTION = "BatchAggregator dispatch";

        private final List<T> detachedBatchReference;

        private final BatchProcessor<T> processor;

        private DispatchRunnable(List<T> detachedBatchReference, BatchProcessor<T> processor) {
            this.detachedBatchReference = detachedBatchReference;
            this.processor = processor;
        }

        @Override
        @TaskDescription(ASYNC_TASK_DESCRIPTION)
        public void run() {
            try {
                processor.processBatch(detachedBatchReference);
            } catch (RuntimeException e) {
                // the best we can do here is log the error and discard the batch
                // synchronized so that the proper logger is always "seen"
                synchronized (BatchAggregatorImpl.class) {
                    log.error("Uncaught exception in batch processor " + processor, e);
                }
                throw e; // allow then async queue to react on the exception as well
            }
            // TODO improve fetching
            internalServicesHolder.getStatisticsTrackerService()
                .getCounterCategory(AsyncOrderedExecutionQueue.STATS_COUNTER_SHARED_CATEGORY_NAME).count(
                    ASYNC_TASK_DESCRIPTION);
        }
    }

    // not made final as it needs to be replaced from a unit test
    private static Log log = LogFactory.getLog(BatchAggregatorImpl.class);

    private List<T> currentBatch;

    // private Deque<List<T>> outputQueue = new LinkedList<List<T>>();

    private final int maxBatchSize;

    private final long maxLatency;

    private final BatchProcessor<T> processor;

    private final AsyncOrderedExecutionQueue dispatchQueue;

    private final ConcurrencyUtilsServiceHolder internalServicesHolder;

    private final ConcurrencyUtilsFactory concurrencyUtilsFactory;

    public BatchAggregatorImpl(int maxBatchSize, long maxLatency, BatchProcessor<T> processor,
        ConcurrencyUtilsServiceHolder internalServicesHolder) {
        this.maxBatchSize = maxBatchSize;
        this.maxLatency = maxLatency;
        this.processor = processor;
        this.internalServicesHolder = internalServicesHolder;
        this.concurrencyUtilsFactory = internalServicesHolder.getConcurrencyUtilsFactory();
        // TODO @5.0: policy chosen for backwards compatibility; review/make configurable?
        this.dispatchQueue =
            concurrencyUtilsFactory.createAsyncOrderedExecutionQueue(
                AsyncCallbackExceptionPolicy.LOG_AND_PROCEED);
    }

    /**
     * Adds an element for aggregation. May trigger the internal creation of a new batch or the sending of a finished batch when the the
     * maximum size limit is reached.
     * 
     * @param element the element to add
     */
    @Override
    public synchronized void enqueue(T element) {
        if (currentBatch == null) {
            startNewBatch();
        }

        currentBatch.add(element);

        int size = currentBatch.size();
        if (size >= maxBatchSize) {
            // sanity check
            if (size > maxBatchSize) {
                throw new IllegalArgumentException("maxBatchSize exceeded?");
            }
            // send current batch; the next incoming element will start a new one
            endCurrentBatchAndEnqueueForProcessing();
        }
    }

    /**
     * Logger access for unit tests.
     */
    protected static synchronized void setLogger(Log newLog) {
        BatchAggregatorImpl.log = newLog;
    }

    /**
     * Logger access for unit tests.
     */
    protected static synchronized Log getLogger() {
        return log;
    }

    private synchronized void onMaxLatencyTimerCallback(List<T> relevantBatch) {
        if (currentBatch != relevantBatch) {
            // in this case, the batch associated with the calling TimerTask
            // was already sent out because max size was reached
            return;
        }
        endCurrentBatchAndEnqueueForProcessing();
    }

    private void startNewBatch() {
        currentBatch = new ArrayList<T>();
        internalServicesHolder.getAsyncTaskService().scheduleAfterDelay(new MaxLatencyTimerCallback(currentBatch), maxLatency);
    }

    private void endCurrentBatchAndEnqueueForProcessing() {
        // note: it is important that this Runnable copies the current reference, and does not accidentally use the field! - misc_ro
        dispatchQueue.enqueue(new DispatchRunnable<T>(currentBatch, processor));
        currentBatch = null;
    }

}
