package org.apache.slide.projector.processor.process;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.contract.Context;
import org.apache.commons.contract.EnvironmentConsumer;
import org.apache.commons.contract.EnvironmentProvider;
import org.apache.commons.contract.Processor;
import org.apache.commons.contract.Result;
import org.apache.commons.contract.Store;
import org.apache.commons.contract.StoreException;
import org.apache.commons.contract.constraints.Constraints;
import org.apache.commons.contract.constraints.StringConstraints;
import org.apache.commons.contract.constraints.ValidationException;
import org.apache.commons.contract.descriptor.ParameterDescriptor;
import org.apache.commons.contract.descriptor.ProvidedEnvironmentDescriptor;
import org.apache.commons.contract.descriptor.RequiredEnvironmentDescriptor;
import org.apache.commons.contract.descriptor.ResultDescriptor;
import org.apache.commons.contract.descriptor.ResultEntryDescriptor;
import org.apache.commons.contract.descriptor.StateDescriptor;
import org.apache.commons.contract.i18n.ParameterMessage;
import org.apache.commons.i18n.LocalizedError;
import org.apache.commons.i18n.LocalizedMessage;
import org.apache.slide.projector.Projector;
import org.apache.slide.projector.constraints.AnyConstraints;
import org.apache.slide.projector.constraints.ConstraintsManager;
import org.apache.slide.projector.constraints.ContextException;
import org.apache.slide.projector.context.ProjectorContext;
import org.apache.slide.projector.engine.ProcessorManager;
import org.apache.slide.projector.processor.ConfigurableProcessor;
import org.apache.slide.projector.processor.ConfigurationException;
import org.apache.slide.projector.processor.ProcessException;
import org.apache.slide.projector.store.AbstractStore;
import org.apache.slide.projector.store.Cache;
import org.apache.slide.projector.util.StoreHelper;
import org.apache.slide.projector.value.DocumentValue;
import org.apache.slide.projector.value.Streamable;
import org.apache.slide.projector.value.URI;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.xpath.XPath;

public class Process implements ConfigurableProcessor, EnvironmentConsumer, EnvironmentProvider {
	private static Logger logger = Logger.getLogger(Process.class.getName());
	
	public final static String STEP = "step";
	private final static String OK = "ok";
	
	protected Map steps;
	protected String firstStep;
	
	protected RequiredEnvironmentDescriptor[] requiredEnvironmentDescriptors;
	protected ProvidedEnvironmentDescriptor[] providedEnvironmentDescriptors;
	protected ParameterDescriptor[] parameterDescriptors;
	protected ResultDescriptor[] resultDescriptors;
	
	public void configure(Streamable config) throws ConfigurationException {
		steps = new HashMap();
		try {
			DocumentValue documentResource = new DocumentValue(config);
			Document document = documentResource.getDocument();
			Element rootElement = document.getRootElement();
			firstStep = rootElement.getAttributeValue("first-step");
			List inputParamters = XPath.newInstance("/process/description/input/parameter").selectNodes(rootElement);
			List parameterDescriptors = new ArrayList();
			for ( Iterator i = inputParamters.iterator(); i.hasNext(); ) {
				Element inputParameter = (Element)i.next();
				String name = inputParameter.getAttributeValue("name");
				String description = inputParameter.getAttributeValue("description");
                Element constraintsElement = (Element)inputParameter.getChild("constraints").getChildren().iterator().next();
                Constraints constraints = ConstraintsManager.getInstance().loadConstraints(constraintsElement);
				ParameterDescriptor parameterDescriptor;
				ParameterMessage parameterDescription;
                if ( description != null ) {
                    parameterDescription = new ParameterMessage(description);
				} else {
                    parameterDescription = new ParameterMessage();
				}
                Element defaultElement = (Element)inputParameter.getChild("default");
                if ( defaultElement != null ) {
                    defaultElement = (Element)defaultElement.getChildren().iterator().next();
                    Object defaultValue = ConstraintsManager.getInstance().loadValue(defaultElement);
                    parameterDescriptor = new ParameterDescriptor(name, parameterDescription, constraints, defaultValue);
                } else {
                    parameterDescriptor = new ParameterDescriptor(name, parameterDescription, constraints);
                }
                parameterDescriptors.add(parameterDescriptor);
			}
            this.parameterDescriptors = (ParameterDescriptor [])parameterDescriptors.toArray(new ParameterDescriptor[parameterDescriptors.size()]);
            List resultDescriptors = new ArrayList();
            List stateElements = XPath.newInstance("/process/description/output/state").selectNodes(rootElement);
            for ( Iterator i = stateElements.iterator(); i.hasNext(); ) {
				Element stateElement = (Element)i.next();
                String name = stateElement.getAttributeValue("name");
				String description = stateElement.getAttributeValue("description");
				StateDescriptor stateDescriptor = new StateDescriptor(name, new LocalizedMessage(description));
                List outputResults = stateElement.getChildren("result");
                List resultEntryDescriptors = new ArrayList();
                for ( Iterator j = outputResults.iterator(); j.hasNext(); ) {
                    Element outputResult = (Element)j.next();
                    String resultName = outputResult.getAttributeValue("name");
                    String resultDescription = outputResult.getAttributeValue("description");
                    Element constraintsElement = (Element)outputResult.getChild("constraints").getChildren().iterator().next();
                    Constraints constraints = ConstraintsManager.getInstance().loadConstraints(constraintsElement);
                    resultEntryDescriptors.add(new ResultEntryDescriptor(resultName, new LocalizedMessage(resultDescription), constraints));
                }
                resultDescriptors.add(new ResultDescriptor(stateDescriptor, (ResultEntryDescriptor[])resultEntryDescriptors.toArray(new ResultEntryDescriptor[resultEntryDescriptors.size()])));
			}
            this.resultDescriptors = (ResultDescriptor [])resultDescriptors.toArray(new ResultDescriptor[resultDescriptors.size()]);
            List providedEnvironmentElements = XPath.newInstance("/process/description/output/environment").selectNodes(rootElement);
			List providedEnvironment = new ArrayList();
			for ( Iterator i = providedEnvironmentElements.iterator(); i.hasNext(); ) {
				Element environmentElement = (Element)i.next();
				String key = environmentElement.getAttributeValue("key");
				String storeId = environmentElement.getAttributeValue("store");
				String description = environmentElement.getAttributeValue("description");
				String contentType = environmentElement.getAttributeValue("content-type");
				ProvidedEnvironmentDescriptor environmentDescriptor = new ProvidedEnvironmentDescriptor(key, new LocalizedMessage(description), new AnyConstraints(contentType));
				environmentDescriptor.setStore(storeId);
				providedEnvironment.add(environmentDescriptor);
			}
			providedEnvironmentDescriptors = (ProvidedEnvironmentDescriptor [])providedEnvironment.toArray(new ProvidedEnvironmentDescriptor[providedEnvironment.size()]);
			List requiredEnvironmentElements = XPath.newInstance("/process/description/input/environment").selectNodes(rootElement);
			List requiredEnvironment = new ArrayList();
			for ( Iterator i = requiredEnvironmentElements.iterator(); i.hasNext(); ) {
				Element requiredEnvironmentElement = (Element)i.next();
				String name = requiredEnvironmentElement.getAttributeValue("name");
				String storeId = requiredEnvironmentElement.getAttributeValue("store");
				String description = requiredEnvironmentElement.getAttributeValue("description");
				RequiredEnvironmentDescriptor environmentDescriptor;
				if ( description != null ) {
					environmentDescriptor = new RequiredEnvironmentDescriptor(name, storeId, new ParameterMessage(description), null);
				} else {
					environmentDescriptor = new RequiredEnvironmentDescriptor(name, storeId, new ParameterMessage(), null);
				}
				requiredEnvironment.add(environmentDescriptor);
				Element valueDescriptorElement = (Element)requiredEnvironmentElement.getChildren().iterator().next();
				Constraints valueDescriptor = ConstraintsManager.getInstance().loadConstraints(valueDescriptorElement);
				environmentDescriptor.setConstraints(valueDescriptor);
			}
			requiredEnvironmentDescriptors = (RequiredEnvironmentDescriptor [])requiredEnvironment.toArray(new RequiredEnvironmentDescriptor[requiredEnvironment.size()]);
			List stepElements = XPath.newInstance("/process/step").selectNodes(rootElement);
			for ( Iterator i = stepElements.iterator(); i.hasNext(); ) {
				Element stepElement = (Element)i.next();
				Step step = new Step();
				step.configure(stepElement);
				steps.put(step.getName(), step);
                if ( firstStep == null ) {
                    firstStep = step.getName();
                }
			}
		} catch (Exception exception) {
			logger.log(Level.SEVERE, "Error while parsing process configuration", exception);
			throw new ConfigurationException(new LocalizedError("process/configurationException"), exception);
		}
	}
	
	public Result process(Map parameter, Context context) throws Exception {
		URI processorUri = ProcessorManager.getInstance().getProcessorDescriptor(this).getUri(); 
		((ProjectorContext)context).setProcess(processorUri);				// Remember current process in context 
		String nextStep = getStep(firstStep, context); 	// Lookup the first step of this process
		Store stepStore = new Cache(); 					// This store is used to allow stack-like result/parameter delivery between steps  
		Result result = new Result(OK); 				// The result of this process processor
		Result stepResult = null;       				// The result of the last executed step
		Step step;										// The current step
		Store enclosingStepStore = context.getStore(Projector.STEP_STORE);  
		Map enclosingParameters = ((Cache)context.getStore(Projector.INPUT_STORE)).getMap();  
		((ProjectorContext)context).setStepStore(stepStore);
		((ProjectorContext)context).setInputParameters(parameter);
		do {
			logger.log(Level.INFO, "Processing "+processorUri+", step=" + nextStep);
			((ProjectorContext)context).setStep(nextStep);					// Remember current step in context
			step = (Step)steps.get(nextStep);
			if (step == null) throw new ProcessException(new LocalizedError("stepNotFound", new String[]{nextStep}));
			Processor processor = ProcessorManager.getInstance().getProcessor(step.getProcessorURI());
			try {
                checkRoutings(step, processor);
                Map processorParameters = loadParameters(step, processor, context);
				try {
					stepResult = ProcessorManager.process(processor, processorParameters, context);
			 		try {
                        // save results by using result configuration
                        for (Iterator i = step.getRoutingConfigurationByState(stepResult.getState()).getResultConfigurations().iterator(); i.hasNext();) {
                            ResultConfiguration resultConfiguration = (ResultConfiguration)i.next();
                            Object resultValue = stepResult.getResultEntries().get(resultConfiguration.getName());
                            resultConfiguration.storeValue(resultValue, stepStore, result, context);
                        }
					} catch ( ProcessException e ) {
						throw new ProcessException(new LocalizedError("saveFailed", new Object[] { processorUri, nextStep }), e );
					}
					nextStep = routeState(step, stepResult.getState());
				} catch (Exception e) {
                    String previousStep = nextStep;
					nextStep = routeException(step, e);
                    if ( nextStep == null ) {
                        throw new ProcessException(new LocalizedError("unhandledException", new Object[] { processorUri, previousStep }), e );
                    }
				}
			} catch ( ValidationException exception ) {
				throw new ValidationException(new LocalizedError("validationFailed", new Object[] { step.getProcessorURI(), nextStep }), exception);
			}
		} while (nextStep != null);
		result.setState(getState(step, stepResult.getState()));
		((ProjectorContext)context).setStepStore(enclosingStepStore);
		((ProjectorContext)context).setInputParameters(enclosingParameters);
		return result;
	}
	
	public ParameterDescriptor[] getParameterDescriptors() {
		return parameterDescriptors;
	}
	
	public ResultDescriptor[] getResultDescriptors() {
		return resultDescriptors;
	}
	
	public RequiredEnvironmentDescriptor[] getRequiredEnvironmentDescriptors() {
		return requiredEnvironmentDescriptors;
	}
	
	public ProvidedEnvironmentDescriptor[] getProvidedEnvironmentDescriptors() {
		return providedEnvironmentDescriptors;
	}
	
	static String getStep(String firstStep, Context context) {
		Store sessionStore = context.getStore(Projector.SESSION_STORE);
		if ( sessionStore != null ) {
			Object stepParameter = StoreHelper.get(sessionStore, ((ProjectorContext)context).getProcess().toString(), STEP, context);
			if (stepParameter != null) {
				return (String)stepParameter;
			}
		}
     	return firstStep;
	}
	
	static void checkRoutings(Step step, Processor processor) throws ValidationException {
		ResultDescriptor[] resultDescriptors = processor.getResultDescriptors();
		for ( int i = 0; i < resultDescriptors.length; i++ ) {
			String state = resultDescriptors[i].getStateDescriptor().getState();
			List routings = step.getRoutingConfigurations();
			boolean routingFound = false;
			for ( Iterator j = routings.iterator(); j.hasNext() ; ) {
				if ( ((RoutingConfiguration)j.next()).getState().equals(state) ) {
					routingFound = true;
					break;
				}
			}
			if ( !routingFound ) {
				throw new ValidationException(new LocalizedError("stateNotRouted", new String[] { step.getName(), state }));
			}
		}
	}
	
	public static void checkRequirements(EnvironmentConsumer processor, Context context) throws ValidationException, StoreException {
		RequiredEnvironmentDescriptor[] requirementDescriptor = processor.getRequiredEnvironmentDescriptors();
		for ( int i = 0; i < requirementDescriptor.length; i++ ) {
			Store store = context.getStore(requirementDescriptor[i].getStore());
			Object value = store.get(requirementDescriptor[i].getName(), context);
			if ( value == null ) {
				if ( requirementDescriptor[i].isRequired() ) {
					throw new ContextException(new LocalizedError("requiredContextMissing", new Object[] { requirementDescriptor[i].getName(), requirementDescriptor[i].getStore() }));
				} else {
					value = requirementDescriptor[i].getDefaultValue();
					store.put(requirementDescriptor[i].getName(), value, context);
				}
			}
			Object castedValue = requirementDescriptor[i].getConstraints().cast(value, context);
			requirementDescriptor[i].getConstraints().validate(castedValue, context);
			if ( castedValue != value ) {
				store.put(requirementDescriptor[i].getName(), castedValue, context);
			}
		}
	}
	
	public static String evaluateKey(String key, Context context) {
		int start, end = 0;
		while ( (start = key.indexOf('{') ) != -1 ) {
			end = key.indexOf('}');
			if ( end == -1 ) break;
			int delimiter = key.indexOf(':', start);
			String storeToken = key.substring(start+1, delimiter);
			String keyToken = key.substring(delimiter+1, end);
			Store keyStore = context.getStore(storeToken);
			String evaluatedKey = null;
			if ( keyStore != null ) {
				try {
					Object dynamicKey = keyStore.get(keyToken, context);
					evaluatedKey = StringConstraints.UNCONSTRAINED.cast(dynamicKey, context).toString();
				} catch ( Exception e ) {
					logger.log(Level.SEVERE, "Dynamic key '"+keyToken+"' could not be loaded from store '"+storeToken+"'", e);
				}
			} 
			if ( evaluatedKey != null ) {
				key = key.substring(0, start)+evaluatedKey+key.substring(end+1);
			} else {
				key = key.substring(0, start)+key.substring(end+1);
			}
		}
		return key;
	}
	
	public static Map loadParameters(Step step, Processor processor, Context context) throws Exception {
		// Collect parameters for this processor
		Map parameters = new HashMap();
		ParameterDescriptor[] parameterDescriptors = processor.getParameterDescriptors();
		for (int i = 0; i < parameterDescriptors.length; i++) {
			String key = parameterDescriptors[i].getName(); 
			ParameterConfiguration parameterConfiguration = (ParameterConfiguration)step.getParameterConfigurations().get(key);
			if ( parameterConfiguration == null ) {
				parameters.put(key, null);
			} else if ( parameterConfiguration != null ) {
				parameters.put(key, parameterConfiguration.getValue());
			}
		}
		return parameters;
	}

	public static void saveResults(Step step, Result stepResult, Store stepStore, Result result, ResultEntryDescriptor[] resultEntryDescriptors, Context context) throws ProcessException {
	}
	
	private static String routeState(Step step, String state) {
		// find routing for result state
		for (Iterator i = step.getRoutingConfigurations().iterator(); i.hasNext();) {
			RoutingConfiguration routingConfiguration = (RoutingConfiguration)i.next();
			if (state.equals(routingConfiguration.getState())) {
				if (routingConfiguration.getStep() != null) {
					return routingConfiguration.getStep();
				}
			}
		}
		return null;
	}
	
	private static String routeException(Step step, Exception e) throws Exception {
		logger.log(Level.SEVERE, "Exception occured:", e);
		for (Iterator i = step.getRoutingConfigurations().iterator(); i.hasNext();) {
			RoutingConfiguration routingConfiguration = (RoutingConfiguration)i.next();
			Class exception = routingConfiguration.getException();
			if (exception != null && exception.isAssignableFrom(e.getClass())) {
				return routingConfiguration.getStep();
			}
		}
		throw(e);
	}
	
	private String getState(Step step, String state) throws ProcessException {
		// find processor return state for result state
		for (Iterator i = step.getRoutingConfigurations().iterator(); i.hasNext();) {
			RoutingConfiguration routingConfiguration = (RoutingConfiguration)i.next();
			if (routingConfiguration.getReturnValue() != null && state.equals(routingConfiguration.getState())) {
				String returnState = routingConfiguration.getReturnValue();
				// check if state is legal
                ResultDescriptor[] resultDescriptors = getResultDescriptors(); 
				for (int j = 0; j < resultDescriptors.length; j++) {
					if (resultDescriptors[j].getStateDescriptor().getState().equals(returnState)) {
						return returnState;
					}
				}
				logger.log(Level.SEVERE, "State '" + returnState + "' not defined!");
				throw new ProcessException(new LocalizedError("stateNotDefined", new String[]{returnState}));
			}
		}
		return OK;
	}
	
	private Result generateResult(String state, Result result) throws ProcessException {
		Result processResult = new Result(state);
		// copy defined results from context store to result
		ResultEntryDescriptor[] resultEntryDescriptors = ProcessorManager.getResultDescriptorByState(getResultDescriptors(), state).getResultEntryDescriptors();
		for (int i = 0; i < resultEntryDescriptors.length; i++) {
			ResultEntryDescriptor descriptor = resultEntryDescriptors[i];
			Object resultValue = result.getResultEntries().get(descriptor.getName());
			if (resultValue != null) {
				processResult.addResultEntry(descriptor.getName(), resultValue);
			}
		}
		return processResult;
	}
	
	public class ProcessStore extends AbstractStore {
	    private final static String ID = "process";
        protected Map map = new HashMap();
		
		public void put(String key, Object value, Context context) throws StoreException {
			map.put(key, value);
		}
		
		public Object get(String key, Context context) throws StoreException {
			return map.get(key);
		}
		
		public void dispose(String key, Context context) throws StoreException {
			map.remove(key);
		}

        public String getId() {
            return ID;
        }
	}
}