/*******************************************************************************
 * Copyright © 2004 VHA. All rights reserved
 ******************************************************************************/
package gov.va.med.fw.rule;

// Java classes
import java.util.Map;
import java.io.IOException;
import java.util.Collection;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.PrintWriter;
import java.util.Iterator;

// ILOG classes
import org.apache.commons.lang.Validate;
import ilog.rules.debug.IlrBuilderToolFactory;
import ilog.rules.debug.IlrLocalTracerToolFactory;
import ilog.rules.engine.IlrUserRuntimeException;
import ilog.rules.engine.IlrToolConnectionException;
import ilog.rules.engine.IlrContext;
import ilog.rules.engine.IlrRuleset;
import ilog.rules.engine.IlrParameterMap;

// Spring classes
import org.springframework.core.io.ClassPathResource;

import org.apache.commons.io.IOUtils;

// Framework classes
import gov.va.med.fw.cache.CacheStrategy;
import gov.va.med.fw.service.AbstractListenerService;
import gov.va.med.fw.service.event.ServiceEvent;

/**
 * This class represents a generic rule service that allows a caller to pass a
 * generic rule interface or a list of rule interfaces to an ILOG Rule Engine's
 * rule flow as input parameters. Input "in" parameters are inserted to a rule
 * engine memory (agenda) to be available for business rules, defined in a IRL
 * file, to access during a rule invocation
 * 
 * @author Vu Le
 * @version 1.0
 */
public class RuleFlowManager extends AbstractListenerService {

	/**
	 * An instance of rulesets
	 */
	private Map rulesets = null;

	/**
	 * An instance of cache
	 */
	private CacheStrategy ruleSetsCache = null;

	/**
	 * A default constructor
	 */
	public RuleFlowManager() {
		super();
	}

	/**
	 * @param rulesets The rulesets to set.
	 */
	public void setRulesets(Map rulesets) {
		this.rulesets = rulesets;
	}

	/**
	 * Sets a concrete implementation of a caching strategy
	 * 
	 * @param ruleSetsCache The ruleSetsCache to set.
	 */
	public void setRuleSetsCache(CacheStrategy cache) {
		this.ruleSetsCache = cache;
	}

	/**
	 * Invoke a rule flow specified by a rule flow parameter. An input parameter
	 * is inserted to a Rule Engine memory (agenda) as an in parameter to execute
	 * a rule. A rule interface represents a rule wrapper class containing logic
	 * condition methods, and action methods which a rule can invoke during its
	 * execution. A result of a rule execution is extracted from a rule engine
	 * through an output parameter name specified in a rule interface.
	 * 
	 * @param ruleflow A name of a rule flow to execute
	 * @param parameter A rule wrapper object to be inserted to a rule engine
	 *           memory
	 * @return An output data extracted from a rule engine through an "out"
	 *         parameter
	 * @throws RuleException Thrown if missing required parameters
	 */
	public Object fireRule(String ruleflow, RuleParameter parameter) throws RuleException {

		Map result = null;
      
      // Initialize writer for debugging info
		StringWriter errorWriter = new StringWriter();
		PrintWriter printWriter = new PrintWriter(errorWriter);
      
      // Obtain a rule context to execute a rule flow.
      IlrContext engine = this.getRuleContext( ruleflow );
      
      Throwable error = null;
      
		try {
			// set the input parameters
			IlrParameterMap inParams = new IlrParameterMap();
			inParams.setParameter(parameter.getInputName(), parameter);

			// Set a rule state object into a parameter map
			RuleState state = parameter.getState();
			// The execution state should be set to false at the start fo
			// every call. If there is a better way, please do it.
			state.setRuleExecuted(false);
			inParams.setParameter(state.getName(), state);

			// Put a whole input parameter map into a rule engine
			engine.setParameters(inParams);

			// Execute a rule flow
			RuleExceptionHandler handler = parameter.getExceptionHandler();
			handler.setRuleFlowName(ruleflow);
			engine.setExceptionHandler(handler);

			// Try connecting to a Rule Builder in debug mode
			// otherwise, execute the rules and display execution time.
			if (parameter.isDebugEnabled() == true) {
				try {
					engine.connectTool(new IlrBuilderToolFactory(
							IlrBuilderToolFactory.CONTROLLER_DEBUGGER));
				}
				catch (IlrToolConnectionException e) {
					if (logger.isErrorEnabled()) {
						logger.error("Failed to connect to a rule engine ", e);
					}
				}
			}
			engine.connectTool(new IlrLocalTracerToolFactory(printWriter));

			// Set a flag to indicate the rule parameters are being executed
			parameter.getState().setInSession(true);

			// Execute a rule flow
			IlrParameterMap outParams = engine.execute();
			result = outParams.getMap();
		}
		catch (IlrUserRuntimeException e) {
			// dump helpful log messages //CCR 9565 added for debugging
			if(logger.isErrorEnabled()) {
				logger.error("ILog Exception for target member: " + e.getTargetMember());
				logger.error("ILog Exception caused by: " + e.getTargetException());	
			}
			
			error = e;
			if (e.getTargetException() instanceof RuleValidationException) {
				throw new RuleException(e.getMessage(), 
												e.getTargetException(),
												ruleflow, 
												parameter );
			}
			throw new RuleConfigurationException("An un-expected error occured in rule flow: " + ruleflow, 
															 e,
															 ruleflow, 
															 parameter);
		}
		catch (Exception e) {
         error = e;
			throw new RuleConfigurationException("Failed to obtain a rule flow named" + ruleflow, 
															 e, 
															 ruleflow,
															 parameter);
		}
		finally {
         // If an exception was thrown, log errors
         if( error != null ) {
            logger.error("Invoked rule flow: " + ruleflow);
            logger.error("Start Rule Trace Info:");
            logger.error(errorWriter.toString());
            logger.error("End Rule Trace Info:");
         }
         else {
            // Only log info if debug flag is turned on
            if( logger.isDebugEnabled() ) {
               logger.debug("Invoked rule flow: " + ruleflow);
               logger.debug("Start Rule Trace Info:");
               logger.debug(errorWriter.toString());
               logger.debug("End Rule Trace Info:");
            }
         }
			// Reset a flag to indicate all rule parameters are no longer being
			// executed in the same session
			parameter.getState().setInSession(false);
         
         // Clear memory
         engine.reset();
         engine.resetForPool();
         engine.end();
		}
		return result;
	}

	/**
	 * Invoke a rule flow specified by a rule flow parameter. A list of input
	 * parameters is inserted to a Rule Engine memory (agenda) as an in parameter
	 * to execute a rule. A rule interface represents a rule wrapper class
	 * containing logic condition methods, and action methods which a rule can
	 * invoke during its execution. A result of a rule execution is extracted
	 * from a rule engine through an output parameter name specified in a rule
	 * interface.
	 * 
	 * @param ruleflow A name of a rule flow to execute
	 * @param parameters A wrapper of a collection rule parameters to be inserted
	 *           to a rule engine memory
	 * @return An output data extracted from a rule engine through an "out"
	 *         parameter
	 * @throws RuleException Thrown if missing required parameters
	 */
	public Object fireRule(String ruleflow, RuleParameters parameters) throws RuleException {

		Map result = null;

      // Initialize writer for debugging info
      StringWriter errorWriter = new StringWriter();
      PrintWriter printWriter = new PrintWriter(errorWriter);
      
      Throwable error = null;
      
      // Obtain a rule context to execute a rule flow.
      IlrContext engine = this.getRuleContext( ruleflow );
      
		try {
			// set the input parameters
			IlrParameterMap inParams = new IlrParameterMap();
			Collection params = parameters.getRuleParameters().values();
			for (Iterator i = params.iterator(); i.hasNext();) {
				RuleParameter param = (RuleParameter) i.next();
				inParams.setParameter(param.getInputName(), param);
			}

			// Set a rule state object into a parameter map
			RuleState state = parameters.getState();
			// The execution state should be set to false at the start fo
			// every call. If there is a better way, please do it.
			state.setRuleExecuted(false);
			inParams.setParameter(state.getName(), state);

			// Put a whole input parameter map into a rule engine
			engine.setParameters(inParams);

			// Execute a rule flow
			RuleExceptionHandler handler = parameters.getExceptionHandler();
			handler.setRuleFlowName(ruleflow);
			engine.setExceptionHandler(handler);

			// Try connecting to a Rule Builder in debug mode
			// otherwise, execute the rules and display execution time.
			if (parameters.isDebugEnabled() == true) {
				try {
					engine.connectTool(new IlrBuilderToolFactory(IlrBuilderToolFactory.CONTROLLER_DEBUGGER));
				}
				catch (IlrToolConnectionException e) {
					if (logger.isErrorEnabled()) {
						logger.error("Failed to connect to a rule engine ", e);
					}
				}
			}
			engine.connectTool(new IlrLocalTracerToolFactory(printWriter));

			// Set a flag to indicate the rule parameters are being executed
			setRuleParametersInSession(parameters, true);

			// Execute a rule flow
			IlrParameterMap outParams = engine.execute();
			result = outParams.getMap();
		}
		catch (IlrUserRuntimeException e) {
			// dump helpful log messages
			if(logger.isErrorEnabled()) {
				logger.error("ILog Exception for target member: " + e.getTargetMember());
				logger.error("ILog Exception caused by: " + e.getTargetException());	
			}
			
         error = e;
			if (e.getTargetException() instanceof RuleValidationException) {
				throw new RuleException( e.getMessage(), 
                                     e.getTargetException(),
												 ruleflow, 
												 (RuleParameter[]) parameters.getRuleParameters().values().toArray(new RuleParameter[0]));
			}
			throw new RuleConfigurationException("An un-expected error occured in rule flow: " + ruleflow, 
															 e,
															 ruleflow, 
															 (RuleParameter[]) parameters.getRuleParameters().values().toArray(new RuleParameter[0]));
		}
		catch (Exception e) {
         error = e;
			throw new RuleConfigurationException("Failed to obtain a rule flow named: " + ruleflow, 
															 e, 
															 ruleflow,
															 (RuleParameter[]) parameters.getRuleParameters().values().toArray(new RuleParameter[0]));
		}
		finally {
         // If an exception was thrown, log errors
			if( error != null ) {
				logger.error("Invoked rule flow: " + ruleflow);
				logger.error("Start Rule Trace Info:");
				logger.error(errorWriter.toString());
				logger.error("End Rule Trace Info:");
			}
         else {
            // Only log info if debug flag is turned on
            if( logger.isDebugEnabled() ) {
               logger.debug("Invoked rule flow: " + ruleflow);
               logger.debug("Start Rule Trace Info:");
               logger.debug(errorWriter.toString());
               logger.debug("End Rule Trace Info:");
            }
         }
			// Reset a flag to indicate all rule parameters are no longer being
			// executed in the same session
			setRuleParametersInSession(parameters, false);
         
         // Clear memory 
         engine.reset();
         engine.resetForPool();
         engine.end();
		}
		return result;
	}

	/**
	 * Checks if a service is properly configured with its required properties.
	 * This method is called right after all properties are set.
	 * 
	 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
	 */
	public void afterPropertiesSet() throws Exception {
		Validate.notEmpty( this.rulesets, "A ruleset to rule file mapping map must be configured");
		Validate.notNull( this.ruleSetsCache, "A ruleset cache must be configured" );
	}

	/**
	 * Deallocate resources
	 * 
	 * @see java.lang.Object#finalize()
	 */
	protected void finalize() throws Throwable {
		super.finalize();
		this.rulesets = null;
		this.ruleSetsCache = null;
	}

	/**
	 * Responds to a service event. This method only handles RuleEvent. A rule
	 * flow name is extracted from a rule event to pass to a fireRule method to
	 * execue a rule.
	 * 
	 * @see gov.va.med.fw.service.AbstractListenerService#processServiceEvent(com.VHA.fw.service.ServiceEvent)
	 */
	protected void processServiceEvent(ServiceEvent event) {
		if (event instanceof RuleEvent) {
			RuleEvent ruleEvent = (RuleEvent) event;
			try {
				RuleParameter param = ruleEvent.getRuleParameter();
				if (param != null) {
					fireRule(ruleEvent.getRuleflow(), param);
				}
				else {
					RuleParameters params = ruleEvent.getRuleParameters();
					if (params != null) {
						fireRule(ruleEvent.getRuleflow(), params);
					}
				}
			}
			catch (RuleException e) {
				processException(e, event.getSource());
			}
		}
	}

	private void setRuleParametersInSession(RuleParameters parameters,boolean flag) {
		Collection params = parameters.getRuleParameters().values();
		for (Iterator i = params.iterator(); i.hasNext();) {
			RuleParameter param = (RuleParameter) i.next();
			param.getState().setInSession(flag);
		}
		parameters.getState().setInSession(flag);
	}
	
	private IlrContext getRuleContext( String ruleFlow ) throws RuleConfigurationException {
		
		// Obtain a rule context to execute a rule flow.
		Object item = ruleSetsCache.getItem( ruleFlow );
      IlrRuleset ruleset = item instanceof IlrRuleset ? (IlrRuleset)item : null;
		if( ruleset == null ) {
			InputStream stream = null;
			synchronized( ruleSetsCache ) {

				try {
					// Get a rule set file name
					String ruleFile = (String)this.rulesets.get( ruleFlow );
					
					// create an instance of a rule engine for the specific ruleset
					ruleset = new IlrRuleset();

					ClassPathResource resource = new ClassPathResource(ruleFile);
					stream = resource.getInputStream();

					// Create a writer to print error
					StringWriter errorWriter = new StringWriter();
					PrintWriter printWriter = new PrintWriter(errorWriter);
					ruleset.setMessageWriter(printWriter);

					boolean parsed = ruleset.parseStream(stream);
					if (parsed) {
						this.ruleSetsCache.cacheItem(ruleFlow, ruleset);
						if (logger.isDebugEnabled()) {
							logger.debug("Rule set " + ruleFlow	+ " was parsed successfully");
						}
					}
					else {
						throw new RuleConfigurationException( errorWriter.toString() );
					}
				}
				catch( IOException e ) {
					throw new RuleConfigurationException( "Failed to load a rule file", e  );
				} finally {
					if (stream != null) {
						IOUtils.closeQuietly(stream);
					}
				}
			}
		}
		return new IlrContext(ruleset);
	}
}