package org.apache.slide.projector.engine;

import java.io.InputStream;
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.Processor;
import org.apache.commons.contract.Result;
import org.apache.commons.contract.constraints.Constraints;
import org.apache.commons.contract.constraints.ValidationException;
import org.apache.commons.contract.descriptor.ParameterDescriptor;
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.i18n.LocalizedError;
import org.apache.commons.i18n.LocalizedMessage;
import org.apache.commons.xmlio.in.DefaultSimpleImportHandler;
import org.apache.commons.xmlio.in.SimpleImporter;
import org.apache.commons.xmlio.in.SimplePath;
import org.apache.slide.projector.Projector;
import org.apache.slide.projector.application.Application;
import org.apache.slide.projector.application.ApplicationListener;
import org.apache.slide.projector.descriptor.ProcessorDescriptor;
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.processor.SimpleProcessor;
import org.apache.slide.projector.processor.process.Process;
import org.apache.slide.projector.repository.RepositoryException;
import org.apache.slide.projector.value.Any;
import org.apache.slide.projector.value.Streamable;
import org.apache.slide.projector.value.URI;
import org.apache.slide.projector.value.URIValue;
import org.apache.webdav.lib.Subscriber;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.AttributesImpl;

/**
 * The ProcessorManager is responsible for loading processors and their
 * configuration. Because every processor is located via its URI, it might be
 * possible to handle different versions of the same processor.
 * They are reloaded if an event occurs that indicates, that the class or configuration has changed
 * or was updated.
 */
public class ProcessorManager implements ApplicationListener, Subscriber {
    public final static URI BINARY = new URIValue("image");
    public final static URI LOCALE_RESOLVER = new URIValue("localeResolver");
    public final static URI URL = new URIValue("url");
    public final static URI THREAD = new URIValue("thread");

    private final static String CLASSES_DIR = "classes/";

    private final static Logger logger = Logger.getLogger(ProcessorManager.class.getName());
    private final static String PROCESSOR_CONFIG = "processors.xml";
    private final static URI SMALL_DEFAULT_ICON = new URIValue("/projector/applications/core/images/process.jpg");
    private final static URI LARGE_DEFAULT_ICON = new URIValue("/projector/applications/core/images/process.jpg");

    private static ProcessorManager processorManager = new ProcessorManager();
    
    private Map configurationToApplication = new HashMap(); // URI -> URI
    private Map installedProcessors = new HashMap(); 
    private Map processorMap = new HashMap(256); // URI -> Processor
    private Map configuredProcessors = new HashMap(128); 
    private Map configurationListeners = new HashMap(128); 
    private Map processorDescriptors = new HashMap(256); // URI -> ProcessorDescriptor
    private ProjectorClassLoader processorClassLoader = new ProjectorClassLoader(this.getClass().getClassLoader(), new URIValue(Projector.getProjectorDir()+CLASSES_DIR));
    
    private ProcessorManager() {
    }
    	
    public void install(String type, URI applicationUri, URI configurationUri) {
    	if ( type == Application.PROCESSORS ) {
    		install(applicationUri, configurationUri);
    		configurationToApplication.put(configurationUri, applicationUri);
    	}
    }
    
    public void uninstall(String type, URI applicationUri, URI configurationUri) {
    	if ( type == Application.PROCESSORS ) {
    		uninstall(applicationUri, configurationUri);
    		configurationToApplication.remove(configurationUri);
    	}
    }
    
    public void update(String type, URI applcationUri, URI configurationUri) {
    	if ( type == Application.PROCESSORS ) {
    		update(applcationUri, configurationUri);
    	}
    }
    
    private void install(URI applicationUri, URI configurationUri) {
    	logger.log(Level.FINE, "Installing processors '"+configurationUri+"'");
    	try {
    		InputStream configuration = ((Streamable)Projector.getRepository().getResource(configurationUri, Projector.getCredentials())).getInputStream();
    		SimpleImporter importer = new SimpleImporter();
    		List alreadyInstalledProcessors = (List)installedProcessors.get(configurationUri);
    		if ( alreadyInstalledProcessors == null ) alreadyInstalledProcessors = new ArrayList();
    		ConfigurationHandler handler = new ConfigurationHandler(applicationUri, alreadyInstalledProcessors);
    		importer.addSimpleImportHandler(handler);
    		importer.parse(new InputSource(configuration));
    		List removedProcessors = new ArrayList();
    		alreadyInstalledProcessors.addAll(handler.getAddedProcessors());
    		for ( Iterator i = handler.getRemovedProcessors().iterator(); i.hasNext(); ) {
    			URI removedProcessorUri = (URI)i.next();
    			uninstallProcessor(removedProcessorUri);
    			alreadyInstalledProcessors.remove(removedProcessorUri);
    		}
    		installedProcessors.put(configurationUri, alreadyInstalledProcessors);
    		Projector.getRepository().subscribe("Update", configurationUri, 0, this, Projector.getCredentials());
    	} catch (Exception exception) {
    		logger.log(Level.SEVERE, "Error while parsing configuration", exception);
    	}
    }

    public void uninstall(URI applicationUri, URI configurationUri) {
    	logger.log(Level.FINE, "Uninstalling processors '"+configurationUri+"'");
    	List processors = (List)installedProcessors.get(configurationUri);
    	for ( Iterator j = processors.iterator(); j.hasNext(); ) {
    		URI processorUri = (URI)j.next();
    		uninstallProcessor(processorUri);
    	}
		installedProcessors.remove(configurationUri);
        try {
            Projector.getRepository().unsubscribe(configurationUri, this, Projector.getCredentials());
        } catch ( RepositoryException exception ) {
            logger.log(Level.SEVERE, "Failed to unsubscribe listener on Uri'"+configurationUri+"'", exception);
        }
    }
    
    private void uninstallProcessor(URI processorUri) {
		processorMap.remove(processorUri);
		ProcessorDescriptor processorDescriptor = getProcessorDescriptor(processorUri); 
		URI processorConfiguration = processorDescriptor.getConfiguration(); 
		if ( processorConfiguration != null ) {
			configuredProcessors.remove(processorConfiguration);
			Subscriber subscriber = (Subscriber)configurationListeners.get(processorConfiguration);
try {
            Projector.getRepository().unsubscribe(processorConfiguration, subscriber, Projector.getCredentials());
        } catch ( RepositoryException exception ) {
            logger.log(Level.SEVERE, "Failed to unsubscribe listener on Uri'"+processorConfiguration+"'", exception);
        }

			configurationListeners.remove(processorConfiguration);
		}
		processorDescriptors.remove(processorUri);
		logger.log(Level.FINE, "Removing processor: "+processorUri);
    }
    
    private void update(URI applicationUri, URI configurationUri) {
        try {
		Projector.getRepository().unsubscribe(configurationUri, this, Projector.getCredentials());
    } catch ( RepositoryException exception ) {
        logger.log(Level.SEVERE, "Failed to unsubscribe listener on Uri'"+configurationUri+"'", exception);
    }
    	install(applicationUri, configurationUri);
    }
    
    public static ProcessorManager getInstance() {
        return processorManager;
    }

	public void notify(String uri, Map information) {
		URI configurationUri = new URIValue(uri);
		update((URI)configurationToApplication.get(configurationUri), configurationUri);
	}

	public Processor getProcessor(URI uri) throws ProcessException {
        if ( processorMap.containsKey(uri) ) {
            return (Processor)processorMap.get(uri);
        }
        logger.log(Level.SEVERE, "Requested processor with URI=" + uri+ " not found!");
        throw new ProcessException(new LocalizedError("processorNotFound", new Object[] {uri.toString()}));
    }

    public URI getURI(Processor processor) throws ProcessException  {
        for ( Iterator i = processorMap.entrySet().iterator(); i.hasNext(); ) {
            Map.Entry entry = (Map.Entry)i.next();
            if ( entry.getValue() == processor ) {
                return ((URI)entry.getKey());
            }
        }
        logger.log(Level.SEVERE, "Requested URI for processor=" + processor + " not found!");
        throw new ProcessException(new LocalizedError("processorNotFound", new Object[] {processor.toString()}));
    }

    public Result process(URI processorUri, Map parameters, Context context) throws Exception {
        Processor  processor = getProcessor(processorUri);
        return process(processor, parameters, context);
    }
    
    public static Result process(Processor processor, Map parameters, Context context) throws Exception {
    	prepareValues(processor.getParameterDescriptors(), parameters, context);
        if ( processor instanceof EnvironmentConsumer ) {
            Process.checkRequirements((EnvironmentConsumer)processor, context);
        }
    	Result result = processor.process(parameters, context);
        validateResult(processor.getResultDescriptors(), result, context);
        return result;
    }
    
    public Object process(URI simpleProcessorUri, Object input, Context context) throws Exception {
        return process(simpleProcessorUri, input, null, context);
    }

    public Object process(URI simpleProcessorUri, Object input, String resultKey, Context context) throws Exception {
        Processor  processor = getProcessor(simpleProcessorUri);
        ParameterDescriptor[] parameterDescriptors = processor.getParameterDescriptors();
        if ( parameterDescriptors.length > 1 && countRequiredParameters(parameterDescriptors) > 1 ) {
            throw new ProcessException(new LocalizedError("valueProcessorNeedsTooManyParametersException"));
        }
        if ( resultKey  == null ) {
            ResultDescriptor resultDescriptor = getResultDescriptorByState(processor.getResultDescriptors(), StateDescriptor.OK);
            if ( resultDescriptor.getResultEntryDescriptors().length == 0 ) {
                throw new ProcessException(new LocalizedError("parameterProcessingException", new String[] { simpleProcessorUri.toString() }));
            } else {
                resultKey = resultDescriptor.getResultEntryDescriptors()[0].getName();
            }
        }
        if ( input == null && parameterDescriptors.length == 1 && parameterDescriptors[0].isRequired()) {
            throw new ProcessException(new LocalizedError("requiredParameterMissing", new String[]{parameterDescriptors[0].getName()}));
        } else if ( input == null && parameterDescriptors.length > 1 ) {
            ParameterDescriptor requiredParameter = getRequiredParameter(parameterDescriptors);
            if ( requiredParameter != null ) throw new ProcessException(new LocalizedError("requiredParameterMissing", new String[]{ requiredParameter.getName()}));
        } else {
            try {
                // check for SimpleProcessor to avoid HashMap creation
                if ( processor instanceof EnvironmentConsumer ) {
                    Process.checkRequirements((EnvironmentConsumer)processor, context);
                }
                if ( processor instanceof SimpleProcessor ) {
                    Object preparedValue = prepareValue(parameterDescriptors[0], input, context);
                    input = ((SimpleProcessor)processor).process(preparedValue, context);
                } else {
                    Map parameters = new HashMap();
                    if ( parameterDescriptors.length == 1 ) {
                        parameters.put(parameterDescriptors[0].getName(), prepareValue(parameterDescriptors[0], input, context));
                    } else {
                    	for ( int i = 0; i < parameterDescriptors.length; i++ ) {
                            if ( parameterDescriptors[i].isRequired() ) {
                                parameters.put(parameterDescriptors[i].getName(), prepareValue(parameterDescriptors[i], input, context));
                            } else {
                                parameters.put(parameterDescriptors[i].getName(), parameterDescriptors[i].getDefaultValue());
                            }
                        }
                    	
                    }
                    Result processorResult = processor.process(parameters, context);
                    input = processorResult.getResultEntries().get(resultKey);
                }
            } catch ( Exception exception ) {
                throw new ProcessException(new LocalizedError("parameterProcessingException", new String[] { simpleProcessorUri.toString(), exception.getMessage() }), exception);
            }
        }
        return input;
    }

    public static void prepareValues(ParameterDescriptor[] parameterDescriptors, Map parameters, Context context) throws Exception {
    	for ( int i = 0; i < parameterDescriptors.length; i++ ) {
    		String parameterName = parameterDescriptors[i].getName();
    		Object parameterValue = parameters.get(parameterName);
    		Object preparedValue = prepareValue(parameterDescriptors[i], parameterValue, context);
    		parameters.put(parameterName, preparedValue);
    	}
    }
    
    public static Object prepareValue(ParameterDescriptor parameterDescriptor, Object value, Context context) throws Exception {
    	Object preparedValue;
    	if ( value instanceof Any ) {
    		 value = ((Any)value).evaluate(context);
    	}
    	if ( value == null ) {
            if ( parameterDescriptor.isRequired() ) {
                throw new ValidationException(new LocalizedError("requiredParameterMissing", new String[] { parameterDescriptor.getName() }));
            } else {
                preparedValue = parameterDescriptor.getDefaultValue();
            }
    	} else {
        	preparedValue = parameterDescriptor.getConstraints().cast(value, context);
        	parameterDescriptor.getConstraints().validate(preparedValue, context);
    	}
    	return preparedValue;
    }

    public static void validateResult(ResultDescriptor[] resultDescriptors, Result result, Context context) throws Exception {
        ResultEntryDescriptor []entryDescriptors = getResultDescriptorByState(resultDescriptors, result.getState()).getResultEntryDescriptors();
        Map resultEntries = result.getResultEntries();
        String name = null;
        Object value = null;
        try {
            for ( int i = 0; i < entryDescriptors.length; i++ ) {
                ResultEntryDescriptor entryDescriptor = entryDescriptors[i];
                name = entryDescriptor.getName();
                value = resultEntries.get(name);
                if ( value == null ) throw new ValidationException(new LocalizedError("missingResultEntry", new String[] { name }));
                Constraints constraints = entryDescriptor.getConstraints();
                if ( constraints == null ) throw new ValidationException(new LocalizedError("undefinedResultEntryConstraints", new String[] { name }));
                value = constraints.cast(value, context);
                entryDescriptor.getConstraints().validate(value, context);
                resultEntries.put(name, value);
            } 
        } catch ( ValidationException exception ) {
            throw new ValidationException(new LocalizedError("invalidResultEntry", new Object[] { name, value }), exception);
        }
    }

    public static ResultDescriptor getResultDescriptorByState(ResultDescriptor[] resultDescriptors, String state) throws ProcessException {
        for ( int i = 0; i < resultDescriptors.length; i++ ) {
            if ( resultDescriptors[i].getStateDescriptor().getState().equals(state)) {
                return resultDescriptors[i];
            }
        }
        throw new ProcessException(new LocalizedError("stateNotDefined", new Object[] { state }));
    }

    public ProcessorDescriptor getProcessorDescriptor(URI uri) {
    	return (ProcessorDescriptor)processorDescriptors.get(uri);
    }

    public ProcessorDescriptor getProcessorDescriptor(Processor processor) {
        for ( Iterator i = processorDescriptors.values().iterator(); i.hasNext(); ) {
        	ProcessorDescriptor processorDescriptor = (ProcessorDescriptor)i.next();
            if ( processorDescriptor.getProcessor() == processor ) return processorDescriptor;
        }
        return null;
    }

    private void registerProcessor(URI uri, Processor processor) {
    }

    private static int countRequiredParameters(ParameterDescriptor[] descriptors) {
        int count = 0;
        for ( int i = 0; i < descriptors.length; i++ ) {
            if ( descriptors[i].isRequired() ) {
                count++;
            }
        }
        return count;
    }

    private static ParameterDescriptor getRequiredParameter(ParameterDescriptor[] descriptors) {
        for ( int i = 0; i < descriptors.length; i++ ) {
            if ( descriptors[i].isRequired() ) {
                return descriptors[i];
            }
        }
        return null;
    }

    public static ParameterDescriptor getParameterDescriptor(Processor processor, String parameterName) {
        ParameterDescriptor[] parameterDescriptors = processor.getParameterDescriptors();
        for ( int i = 0; i < parameterDescriptors.length; i++ )  {
            if ( parameterDescriptors[i].getName().equals(parameterName)) {
                return parameterDescriptors[i];
            }
        }
        return null;
    }

    public class ConfigurationHandler extends DefaultSimpleImportHandler {
    	URI applicationUri;
    	List addedProcessors = new ArrayList();
    	List removedProcessors = new ArrayList();
    		
    	public ConfigurationHandler(URI applicationUri, List installedProcessors) {
    		this.applicationUri = applicationUri;
    		removedProcessors.addAll(installedProcessors);
    	}
    	
    	public void startElement(SimplePath path, String name, AttributesImpl attributes, String leadingCDdata) {
            if (path.matches("processor")) {
            	boolean modified = false, added = false;
            	URI uri = new URIValue(attributes.getValue("uri"));
            	removedProcessors.remove(uri);
            	String configURI = attributes.getValue("config-uri");
                String processorName = attributes.getValue("name");
                String descriptionKey = attributes.getValue("description");
                String smallIcon = attributes.getValue("small-icon");
                String largeIcon = attributes.getValue("large-icon");
                String clazz = attributes.getValue("class");
                String bookmark = attributes.getValue("bookmark");
                Processor processor = (Processor)processorMap.get(uri);
                ProcessorDescriptor processorDescriptor = (ProcessorDescriptor)processorDescriptors.get(uri);
                try {
                	if ( processor == null ) {
                		processor = (Processor)processorClassLoader.loadClass(clazz).getConstructor(new Class[0]).newInstance(new Object[0]);
                		added = true;
                		modified = true;
                	}
                	if ( processorDescriptor == null ) { 
                		processorDescriptor = new ProcessorDescriptor(uri);
                		modified = true;
                	}
                	if ( processorName == null ) processorName = uri.toString();
                	if ( processorDescriptor.getName() == null || !processorDescriptor.getName().equals(processorName) ) {
                		processorDescriptor.setName(processorName);
                		modified = true;
                	}
                	if ( descriptionKey == null ) {
                		descriptionKey = "processorManager/noProcessorDescriptionAvailable";
                	}
                	LocalizedMessage description = null;
                	if ( processorDescriptor.getDescription() == null || !processorDescriptor.getDescription().getId().equals(descriptionKey) ) {
                		description = new LocalizedMessage(descriptionKey);
                		processorDescriptor.setDescription(description);
                		modified = true;
                	}
                	if ( processorDescriptor.getProcessor() != processor ) {
                		processorDescriptor.setProcessor(processor);
                		modified = true;
                	}
                	URI smallIconUri = SMALL_DEFAULT_ICON, largeIconUri = LARGE_DEFAULT_ICON;
                	if ( smallIcon != null ) smallIconUri = new URIValue(smallIcon);
                	if ( largeIcon != null ) largeIconUri = new URIValue(largeIcon);
                	if ( processorDescriptor.getSmallIcon() == null || !processorDescriptor.getSmallIcon().equals(smallIconUri) ) {
                		processorDescriptor.setSmallIcon(smallIconUri);
                		modified = true;
                	}
                	if ( processorDescriptor.getLargeIcon() == null || !processorDescriptor.getLargeIcon().equals(largeIconUri) ) {
                    	processorDescriptor.setLargeIcon(largeIconUri);
                		modified = true;
                	}
                	if ( bookmark != null ) {
                		boolean isBookmark = Boolean.valueOf(bookmark).booleanValue();
                		if ( processorDescriptor.isBookmark() != isBookmark ) {
                			processorDescriptor.setBookmark(isBookmark);
                			modified = true;
                		}
                	}
                	if ( processor instanceof ConfigurableProcessor && configURI != null ) {
                		if ( !configURI.startsWith("/") ) {
                			configURI = applicationUri.toString() + configURI;
                		}
                		URI configurationUri = new URIValue(configURI);
                		if ( processorDescriptor.getConfiguration() == null || !processorDescriptor.getConfiguration().equals(configurationUri) ) {
                			logger.log(Level.FINE, "Configuring processor with config-URI=" + configURI);
                			processorDescriptor.setConfiguration(configurationUri);
                			Streamable config = (Streamable)Projector.getRepository().getResource(configurationUri, Projector.getCredentials());
                			((ConfigurableProcessor)processor).configure(config);
                			// listen for configuration changes
                			Subscriber subscriber = new ConfigurationListener();
                			Projector.getRepository().subscribe("Update", configurationUri, 0, subscriber, Projector.getCredentials());
                			configurationListeners.put(configurationUri, subscriber);
                			configuredProcessors.put(configurationUri, processor);
                			modified = true;
                		}
                    }
                    if ( added ) {
                        logger.log(Level.FINE, "Adding processor with URI=" + uri);
                    	addedProcessors.add(uri);
                    }
                    if ( modified ) {
                        logger.log(Level.FINE, "Updating processor with URI=" + uri);
                    	processorMap.put(uri, processor);
                    	processorDescriptors.put(uri, processorDescriptor);
                    }
                } catch (Exception e) {
                    logger.log(Level.SEVERE, "Processor " + clazz + " could not be created or configured!", e);
                }
            }
        }

    	List getAddedProcessors() {
    		return addedProcessors;
    	}

    	List getRemovedProcessors() {
    		return removedProcessors;
    	}
    }

    class ConfigurationListener implements Subscriber {
    	public void notify(String uri, Map information) {
    		URI processorUri = new URIValue(uri); 
    		Processor processor = (Processor)configuredProcessors.get(processorUri);
    		logger.log(Level.FINE, "Reloading processor with configuration URI='"+uri+"'");
    		try {
    			Streamable config = (Streamable)Projector.getRepository().getResource(processorUri, Projector.getCredentials());
    			((ConfigurableProcessor)processor).configure(config);
    		} catch ( RepositoryException e ) {
    			logger.log(Level.SEVERE, "Configuration resource with URI='"+uri+"' could not be loaded!", e);
    		} catch ( ConfigurationException e ) {
    			logger.log(Level.SEVERE, "Processor with configuration URI='" + uri + "' could not be reloaded! Configuration resource might be invalid!", e);
    		}
    	}
    }
    
    class ConfiguredProcessor {
        private String configUri;
        private URI processorUri;

        public ConfiguredProcessor(String configUri, URI processorUri) {
            this.configUri = configUri;
            this.processorUri = processorUri;
        }

        public String getConfigUri() {
            return configUri;
        }

        public URI getProcessorUri() {
            return processorUri;
        }
    }
}