package com.oath.micro.server.transactions;

import java.util.function.Consumer;
import java.util.function.Function;

import cyclops.control.Try;
import lombok.AllArgsConstructor;
import lombok.experimental.Wither;

import org.springframework.transaction.support.TransactionTemplate;



/**
 * Stream-like monadic transaction processing
 * 
 * @author johnmcclean
 *
 * @param <T> Data input type for transactional process
 * @param <R> Data result type for transactional process
 */
@AllArgsConstructor
@Wither
public class TransactionFlow<T,R> {
	private final TransactionTemplate transactionTemplate;
	private final Function<T,R> transaction;
	
	
	/**
	 * Map from input -&gt; output within the context of a transaction.
	 * An exception in the mapping process will cause Rollback
	 * Transaction will be completed once the entire flow has completed
	 * <pre>
	 * {@code
	 * 	Integer result = TransactionFlow.<Integer>of(transactionTemplate)
										.map(this::load)
										.map(this::save)
										.execute(10)
										.get();
	 * }
	 * </pre>
	 * 
	 * @param fn Map from input type to output type within a transaction
	 * @return TransactionFlow 
	 */
	public <R1> TransactionFlow<T,R1> map(Function<R,R1> fn){
		return of(transactionTemplate,transaction.andThen(fn));
	}
	/**
	 * Consume current data within the context of a transaction.
	 * An exception in the consumption process will cause Rollback
	 * Transaction will be completed once the entire flow has completed
	 * <pre>
	 * {code
	 * 	Integer result = TransactionFlow.of(transactionTemplate, this::load)
										.consume(this::save)
										.execute(10)
										.get();
	 * }
	 * </pre>
	 * 
	 * @param c Consumer to accept current data
	 * @return TransactionFlow 
	 */
	public TransactionFlow<T,R> peek(Consumer<R> c){
		 return map (in-> { c.accept(in); return in;});
	}
	/**
	 * Transform data in the context of a Transaction and optionally propgate to / join with a new TransactionalFlow
	 *  * <pre>
	 * {code
	 * 	String result = TransactionFlow.of(transactionTemplate, this::load)
										.flatMap(this::newTransaction)
										.execute(10)
										.get();
	 * }
	 * </pre>
	 * 
	 * 
	 * @param fn flatMap function to be applied
	 * @return Next stage in the transactional flow
	 */
	public <R1> TransactionFlow<T,R1> flatMap(Function<R,TransactionFlow<T,R1>> fn){
		return of(transactionTemplate,a -> fn.apply(transaction.apply(a)).transaction.apply(a));
	}
	
	/**
	 * Execute the transactional flow - catch all exceptions
	 * 
	 * @param input Initial data input
	 * @return Try that represents either success (with result) or failure (with errors)
	 */
	public Try<R,Throwable> execute(T input){
		return Try.withCatch( ()->transactionTemplate.execute(status-> transaction.apply(input)));
		 
	}
	/**
	 * Execute the transactional flow - catch only specified exceptions
	 * 
	 * @param input Initial data input
	 * @param classes Exception types to catch
	 * @return Try that represents either success (with result) or failure (with errors)
	 */
	public <Ex extends Throwable> Try<R,Ex> execute(T input,Class<Ex> classes){
		
		return Try.withCatch( ()->transactionTemplate.execute(status-> transaction.apply(input)),classes);
		 
	}
	
	/**
	 * Create a new Transactional Flow
	 * 
	 * @param transactionManager Spring TransactionTemplate to manage the transactions
	 * @param fn Initial function to be applied 
	 * @return Create a new Transactional Flow
	 */
	public static <T,R> TransactionFlow<T,R> of(TransactionTemplate transactionManager,Function<T,R> fn){
		return new TransactionFlow<>(transactionManager,fn);
	}
	/**
	 *  Create a new Transactional Flow
	 * 
	 * @param transactionManager Spring TransactionTemplate to manage the transactions
	 * @return Create a new Transactional Flow
	 */
	public static <T> TransactionFlow<T,T> of(TransactionTemplate transactionManager){
		return new TransactionFlow<>(transactionManager,Function.identity());
	}
}
