package com.atlassian.bitbucket.mesh.transaction;

import com.atlassian.bitbucket.mesh.rpc.v1.RpcTransaction;

import javax.annotation.Nonnull;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import static java.util.Objects.requireNonNull;

public interface TransactionLog {

    /**
     * Appends a {@link RpcTransaction transactions} to the log
     *
     * @param transaction the transaction to append
     */
    void append(@Nonnull RpcTransaction transaction);

    /**
     * Iterates over the transaction log, and invokes the provided {@code processor} in order, for batches of
     * {@link RpcTransaction}s. The {@code processor}'s return value determines whether the transactions are removed
     * from the log, and if so, whether more transactions should be offered to the processor.
     * <p>
     * Note that the processor will be invoked with batches of at most {@code preferredBatchSize} transactions.
     * For performance considerations, smaller batches may be offered to the processor. In that case, the caller should
     * <em>NOT</em> conclude that the transaction log is empty.
     *
     * @param processor the transaction processor
     * @param preferredBatchSize the preferred batch size
     */
    void process(@Nonnull Processor processor, int preferredBatchSize);

    /**
     * {@link #process(Processor, int) Processor} of {@link RpcTransaction transactions} in the log.
     */
    @FunctionalInterface
    interface Processor {

        @Nonnull
        Result process(@Nonnull List<RpcTransaction> transactions);
    }

    /**
     * The result of {@link #process processing} a {@link RpcTransaction} from the log.
     */
    class Result {

        private static final Result ABORT = new Result(true, 0, null);
        private static final Result PROCEED = new Result(false, 0, null);

        private final boolean abort;
        private final Exception exception;
        private final int unprocessed;

        Result(boolean abort, int unprocessed, Exception exception) {
            this.abort = abort;
            this.exception = exception;
            this.unprocessed = unprocessed;
        }

        /**
         * @return a {@link Result} instance that indicates that the processor has processed the complete batch that
         *         was offered and does not want to receive further transactions
         */
        @Nonnull
        public static Result abort() {
            return ABORT;
        }

        /**
         * @param unprocessed the number of transactions from the batch that have not been processed and should be
         *                    retained. When this is larger than 0, the <em>last</em> {@code unprocessed} transactions
         *                    from the batch are retained in the log.
         * @return a {@link Result} instance that indicates that the processor has partially processed the batch
         *         that was offered, that the remaining unprocessed transactions should be retained in the log, and
         *         that the processor does not want to receive further transactions
         */
        @Nonnull
        public static Result abortAndReturn(int unprocessed) {
            return new Result(true, unprocessed, null);
        }

        /**
         * @param unprocessed the number of transactions from the batch that have not been processed and should be
         *                    retained. When this is larger than 0, the <em>last</em> {@code unprocessed} transactions
         *                    from the batch are retained in the log.
         * @param exception the exception encountered by the processor
         * @return a {@link Result} instance that indicates that the processor encountered an error while processing the
         *         batch that was offered, that the remaining unprocessed transactions should be retained in the log,
         *         and that the processor does not want to receive further transactions
         */
        @Nonnull
        public static Result error(int unprocessed, @Nonnull Exception exception) {
            return new Result(true, unprocessed, requireNonNull(exception, "exception"));
        }

        @Nonnull
        public static Result proceed() {
            return PROCEED;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            Result result = (Result) o;
            return abort == result.abort &&
                    unprocessed == result.unprocessed &&
                    Objects.equals(exception, result.exception);
        }

        /**
         * @return the exception encountered by the processor, or {@link Optional#empty()} if the processor did not
         *         fail
         */
        @Nonnull
        public Optional<Exception> getException() {
            return Optional.ofNullable(exception);
        }

        /**
         * @return the number of transactions the processor received, but did not process and should be retained in
         *         the log
         */
        public int getUnprocessed() {
            return unprocessed;
        }

        @Override
        public int hashCode() {
            return Objects.hash(abort, unprocessed, exception);
        }

        /**
         * @return {@code true} if the processor has completed processing and it does not want to receive more
         *         transactions (if there are any)
         */
        public boolean isAbort() {
            return abort;
        }

        /**
         * @return {@code true} if the processor threw an exception, or returned an
         *         {@link Result#error(int, Exception) exceptional result}
         */
        public boolean isError() {
            return exception != null;
        }
    }
}
