package eu.dnetlib.contract.aspects;

import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;

import eu.dnetlib.contract.cp.CheckPointEvaluationException;
import eu.dnetlib.contract.cp.ComplexCheckPoint;
import eu.dnetlib.contract.cp.DummyCheckPoint;
import eu.dnetlib.contract.cp.EntryCheckPoint;
import eu.dnetlib.contract.cp.ExceptionCheckPoint;
import eu.dnetlib.contract.cp.ICheckPoint;
import eu.dnetlib.contract.cp.ResultCheckPoint;
import eu.dnetlib.contract.cp.eval.EvaluationContextTreeNode;
import eu.dnetlib.contract.ctx.GlobalContractContext;
import eu.dnetlib.contract.event.EntryContractEvent;
import eu.dnetlib.contract.event.ExceptionContractEvent;
import eu.dnetlib.contract.event.IContractEvent;
import eu.dnetlib.contract.event.ResultContractEvent;
import eu.dnetlib.contract.event.collector.IEventDataCollector;
import eu.dnetlib.contract.event.collector.IEventDataCollectorProvider;
import eu.dnetlib.contract.node.EvaluationResult;
import eu.dnetlib.contract.node.IContractDefinitionNode;
import eu.dnetlib.contract.node.EvaluationResult.Status;
import eu.dnetlib.contract.node.cursor.CursorEntry;
import eu.dnetlib.contract.node.cursor.IContractDefinitionNodeCursor;


/**
 * Contract aspect handler class.
 * @author mhorst
 *
 */
public class ContractAspectHandler {

	private static final Logger log = Logger.getLogger(ContractAspectHandler.class);
	
	/**
	 * {@link IContractDefinitionNode} cursor.
	 */
	private IContractDefinitionNodeCursor cursor;
	
	/**
	 * Event data collector provider.
	 */
	private IEventDataCollectorProvider eventDataCollectorProvider;
	
	/**
	 * Global contract context.
	 */
	private GlobalContractContext globalContractContext;

	
	/**
	 * Validates inspected method's entry arguments.
	 * @param jp
	 */
	public void validateEntryArguments(JoinPoint jp) throws Exception {
		log.debug("inspecting entry arguments of method call: "+
				jp.getTarget().getClass().getName() + '#' + jp.getSignature().getName());
		EntryContractEvent event = new EntryContractEvent(jp);
		performCommonValidation(jp, event, EntryCheckPoint.class);
	}
	
	/**
	 * Validates inspected method's returned value.
	 * @param jp
	 * @param retVal
	 */
	public void validateResult(JoinPoint jp, Object retVal) throws Exception {
		log.debug("inspecting result object of method call: "+
				jp.getTarget().getClass().getName() + '#' + jp.getSignature().getName());
		ResultContractEvent event = new ResultContractEvent(jp,retVal);
		performCommonValidation(jp, event, ResultCheckPoint.class);
	}
	
	/**
	 *  Validates inspected method's thrown exception.
	 * @param jp
	 * @param ex
	 */
	public void validateException(JoinPoint jp, Exception ex) throws Exception {
		log.debug("inspecting exception of method call: "+
				jp.getTarget().getClass().getName() + '#' + jp.getSignature().getName());
		ExceptionContractEvent event = new ExceptionContractEvent(jp,ex);
		performCommonValidation(jp, event, ExceptionCheckPoint.class);
	}
	
	/**
	 * Performs CheckPoint validation.
	 * @param jp JoinPoint object
	 * @param event contract event object
	 * @param reqClass required ICheckPoint class
	 * @throws CheckPointEvaluationException
	 */
	protected void performCommonValidation(JoinPoint jp, IContractEvent event, 
			Class<? extends ICheckPoint<IContractEvent>> reqClass) throws CheckPointEvaluationException {
		if (globalContractContext.isPerformCheckPointValidation()) {
			store(event);
			if (cursor.hasNext()) {
				CursorEntry cursorEntry = cursor.next();
				if (cursorEntry==null || cursorEntry.getEvaluationContextSet()==null ||
						cursorEntry.getEvaluationContextSet().size()==0) {
					String message = "No check points got in cursor entry, " +
							"cannot validate contract context!";
					log.warn(message);
					return;
				} else {
					for (EvaluationContextTreeNode evalCtx : cursorEntry.getEvaluationContextSet()) {
						if (evalCtx.getCheckPoint() instanceof ComplexCheckPoint 
								|| evalCtx.getCheckPoint() instanceof DummyCheckPoint
								|| reqClass.isInstance(evalCtx.getCheckPoint())) {
							EvaluationResult result = evalCtx.getCheckPoint().check(event);
							handleResult(result, evalCtx);
						} else {
							String message = "Unsupported ICheckPoint instance: " +
							evalCtx.getCheckPoint().getClass().getName() + " for "+ event.getClass().getName() +" event!";
							log.warn(message);
							EvaluationResult result = new EvaluationResult(Status.FAIL, 
									evalCtx.getCheckPoint(), event, message);
							handleResult(result, evalCtx);
						}
					}
				}
			} else {
				String message = "No check points left to validate contract context!";
				log.warn(message);
				return;
			}
		} else {
			log.debug("performCheckPointValidation flag is disabled, check points will not be validated!");
			return;
		}
	}
	
	/**
	 * Handles CheckPoint result.
	 * @param result
	 * @param checkPoint
	 * @throws CheckPointEvaluationException
	 */
	protected void handleResult(EvaluationResult result, EvaluationContextTreeNode context) 
		throws CheckPointEvaluationException {
		context.setResult(result);
		if (result.getStatus()==Status.OK) {
			log.debug("JoinPoint succesfully validated");			
		} else if (result.getStatus()==Status.WARN) {
			log.warn("JoinPoint succesfully validated, " +
					"but some problems occured: " + result.getMessage());
		} else if (result.getStatus()==Status.FAIL) {
			String message = "validation of JoinPoint failed! Details: "+result.getMessage(); 
			log.warn(message);
			if (context.getCheckPoint().isCritical()) {
//				currently exception is not thrown when critical failure
//				it is handled in ContractDefinitionNodeCursor
//				throw new CheckPointEvaluationException(message);
			}
		} else if (result.getStatus()==Status.FATAL) {
//			TODO should FATAL result in breaking the test scenario even if some evaluation path may be valid?
			String message = "Fatal error occured when evaluating check point! " +
					"Fault message: "+result.getMessage();
			throw new CheckPointEvaluationException(message, result.getThrowable());
		} 
	}
	
	/**
	 * Stores event data.
	 * @param event
	 */
	public void store(IContractEvent event) {
		IEventDataCollector<IContractEvent> eventDataCollector = eventDataCollectorProvider.provideInstance();
		if (eventDataCollector!=null) {
			eventDataCollector.store(event);
		} else {
			log.debug("got null EventDataCollector, no data will be stored!");
		}
	}

	/**
	 * Returns global contract context.
	 * @return global contract context
	 */
	public GlobalContractContext getGlobalContractContext() {
		return globalContractContext;
	}

	/**
	 * Sets global contract context.
	 * @param globalContractContext
	 */
	public void setGlobalContractContext(GlobalContractContext globalContractContext) {
		this.globalContractContext = globalContractContext;
	}

	/**
	 * Returns event data collector provider.
	 * @return event data collector provider
	 */
	public IEventDataCollectorProvider getEventDataCollectorProvider() {
		return eventDataCollectorProvider;
	}

	/**
	 * Sets event data collector provider.
	 * @param eventDataCollectorProvider
	 */
	public void setEventDataCollectorProvider(
			IEventDataCollectorProvider eventDataCollectorProvider) {
		this.eventDataCollectorProvider = eventDataCollectorProvider;
	}

	/**
	 * Returns {@link IContractDefinitionNode} cursor.
	 * @return IContractDefinitionNode cursor
	 */
	public IContractDefinitionNodeCursor getCursor() {
		return cursor;
	}

	/**
	 * Sets {@link IContractDefinitionNode} cursor.
	 * @param cursor
	 */
	public void setCursor(IContractDefinitionNodeCursor cursor) {
		this.cursor = cursor;
	}
	
}
