package org.infinispan.stream.impl.termop;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.BaseStream;
import java.util.stream.Stream;

import org.infinispan.stream.impl.TerminalOperation;
import org.infinispan.stream.impl.intops.IntermediateOperation;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

/**
 * A terminal based operation that runs the provided function to evaluate the operation.  If a segment is lost during
 * the evaluation of the function the function results will be ignored and subsequently retried with the new stable
 * segments.  This is repeated until either a full stable run is completed of the function or if the lost segment
 * states that there are no more segments left.
 * @param <Original> original stream type
 * @param <E> output type of the function
 * @param <T> type of the stream entries
 * @param <S> type of the stream itself
 */
public class SegmentRetryingOperation<Original, E, T, S extends BaseStream<T, S>, S2 extends S> extends BaseTerminalOperation<Original>
        implements TerminalOperation<Original, E> {
   private static final Log log = LogFactory.getLog(SegmentRetryingOperation.class);

   private static final BaseStream<?, ?> EMPTY = Stream.empty();

   private final Function<? super S2, ? extends E> function;
   private transient AtomicReference<BaseStream<?, ?>> streamRef = new AtomicReference<>(EMPTY);
   private transient AtomicBoolean continueTrying = new AtomicBoolean(true);

   public SegmentRetryingOperation(Iterable<IntermediateOperation> intermediateOperations,
           Supplier<Stream<Original>> supplier, Function<? super S2, ? extends E> function) {
      super(intermediateOperations, supplier);
      this.function = function;
   }

   @Override
   public boolean lostSegment(boolean stopIfLost) {
      BaseStream<?, ?> oldStream = streamRef.get();
      continueTrying.set(!stopIfLost);
      boolean affected;
      if (oldStream != null) {
         // If the stream was non null and wasn't empty that means we were processing it at the time of the segment
         // being lost - so we tell that one to close
         if (oldStream != EMPTY) {
            // This can only fail if the operation completes concurrently
            if ((affected = streamRef.compareAndSet(oldStream, EMPTY))) {
               // This can short circuit some things like sending a response or waiting for retrieval from a
               // cache loader
               oldStream.close();
            }
         } else {
            affected = true;
         }
      } else {
         affected = false;
      }
      return affected;
   }

   private E innerPerformOperation(BaseStream<?, ?> stream) {
      for (IntermediateOperation intOp : intermediateOperations) {
         stream = intOp.perform(stream);
      }
      return function.apply((S2) stream);
   }

   @Override
   public E performOperation() {
      boolean keepTrying = true;
      BaseStream<?, ?> stream;
      E value;
      do {
         stream = supplier.get();
         streamRef.set(stream);
         value = innerPerformOperation(stream);
         log.trace("Completed an operation, trying to see if we are done.");
      } while (!streamRef.compareAndSet(stream, null) && (keepTrying = continueTrying.get()));
      log.tracef("Operation now done, due to try denial: " + !keepTrying);
      return keepTrying ? value : null;
   }

   public Function<? super S2, ? extends E> getFunction() {
      return function;
   }
}
