package org.apache.slide.projector.application;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
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 javax.xml.parsers.ParserConfigurationException;

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.constraints.ConstraintsFactory;
import org.apache.slide.projector.constraints.ConstraintsManager;
import org.apache.slide.projector.context.ProjectorContext;
import org.apache.slide.projector.engine.ProcessorManager;
import org.apache.slide.projector.engine.ProjectorClassLoader;
import org.apache.slide.projector.engine.Scheduler;
import org.apache.slide.projector.i18n.LocalizationManager;
import org.apache.slide.projector.repository.RepositoryException;
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.SAXException;
import org.xml.sax.helpers.AttributesImpl;

public class ApplicationManager {
    private final static Logger logger = Logger.getLogger(ApplicationManager.class.getName());

    private final static String APPLICATION_CONFIG = "application.xml";
    private final static ProjectorContext context = Projector.getSystemContext();
    private final static String CLASSES_DIR = "classes/";

    private static ApplicationManager applicationManager;
    private List applicationListeners = new ArrayList();
    private Map installedApplications = new HashMap(); // URI -> Application
    // FIXME: Should be used from applications classpath
    private ProjectorClassLoader factoryClassLoader = new ProjectorClassLoader(this.getClass().getClassLoader(), new URIValue(Projector.getProjectorDir()+CLASSES_DIR));
    
    private ApplicationManager() {
        logger.log(Level.INFO, "Starting application manager");
        try {
            Projector.getRepository().subscribe("Update/newmember", new URIValue(Projector.getApplicationsDir()), 1,
                    new Subscriber() {
                public void notify(String uri, Map information) {
                    logger.log(Level.FINE, "Package manager received add event");
                    applicationManager.installApplications();
                }
            }, context.getCredentials());
            Projector.getRepository().subscribe("Delete", new URIValue(Projector.getApplicationsDir()), 1, 
                    new Subscriber() {
                public void notify(String uri, Map information) {
                    logger.log(Level.FINE, "Package manager received delete event");
                    applicationManager.installApplications();
                }
            }, context.getCredentials());
        } catch ( RepositoryException exception ) {
            logger.log(Level.SEVERE, "Failed to subscribe listener on Uri'"+Projector.getApplicationsDir()+"'", exception);
        }
        applicationListeners.add(ProcessorManager.getInstance());
        applicationListeners.add(LocalizationManager.getInstance());
    	applicationListeners.add(Scheduler.getInstance());
    	installApplications();
    }

    private synchronized void installApplications() {
        List applicationUris;
    	List applicationsToInstall = new ArrayList();
    	List applicationsToRemove = new ArrayList();
    	try {
    		List removedApplications = new ArrayList();
    		removedApplications.addAll(installedApplications.keySet());
    		applicationUris = Projector.getRepository().getChildren(new URIValue(Projector.getApplicationsDir()), context.getCredentials());
    		for ( Iterator i = applicationUris.iterator(); i.hasNext(); ) {
    			URI applicationUri = (URI)i.next();
    			if ( !applicationUri.toString().endsWith("/") ) {
    				applicationUri = new URIValue(applicationUri+"/");
    			}
    			if ( !installedApplications.containsKey(applicationUri) ) {
    				Application installedApplication = parseApplication(applicationUri);
    				if ( installedApplication != null ) {
    					applicationsToInstall.add(installedApplication);
    				}
    			} else {
    				logger.log(Level.FINE, "Application '"+applicationUri+"' already installed");
    				removedApplications.remove(applicationUri);
    			}
    		}  
    		for ( Iterator i = removedApplications.iterator(); i.hasNext(); ) {
    			Application removedApplication = (Application)installedApplications.get((URI)i.next());
    			applicationsToRemove.add(removedApplication);
    		}
    		// install applications sorted by application dependencies
    		List sortedApplications = sortApplications(applicationsToInstall);
    		for ( Iterator i = sortedApplications.iterator(); i.hasNext(); ) {
    			Application application = (Application)i.next();
    	    	Projector.getRepository().subscribe("Update", application.getUri(), 0, 
    	    			new Subscriber() {
    	    		public void notify(String uri, Map information) {
    	    			applicationManager.updateApplication(uri);
    	    		}
    	    	}, context.getCredentials());
    			install(Application.MESSAGES, application);
    			install(Application.PROCESSORS, application);
    		}
        	Scheduler.getInstance().install(new URIValue(Projector.getWorkDir() + Scheduler.JOBS), true);
    		for ( Iterator i = sortedApplications.iterator(); i.hasNext(); ) {
    			Application application = (Application)i.next();
    			install(Application.JOBS, application);
    		}
    		Scheduler.getInstance().saveJobs();
        } catch (RepositoryException e) {
            logger.log(Level.SEVERE, "Could not determine installed applications!", e);
    	}
    }	
    
    public static ApplicationManager getInstance() {
    	if ( applicationManager == null ) {
    		applicationManager = new ApplicationManager(); 
    	}
        return applicationManager;
    }
    
    private List sortApplications(List applicationsToInstall) {
    	List sortedApplications = new ArrayList();
		for ( Iterator i = applicationsToInstall.iterator(); i.hasNext(); ) {
			Application application = (Application)i.next();
			if ( !sortedApplications.contains(application) ) {
				logger.log(Level.FINE, "Try to install '"+application.getName()+"'");
				addRequiredApplicationsFirst(sortedApplications, applicationsToInstall, application);
			}
		}
    	return sortedApplications;
    }

	private void addRequiredApplicationsFirst(List sortedApplications, List applicationsToInstall, Application application) {
		// FIXME: Check application versions 
		logger.log(Level.FINE, "Checking for dependencies...");
		for ( Iterator i = application.getDependencies().iterator(); i.hasNext(); ) {
			Dependency dependency = (Dependency)i.next();
			logger.log(Level.FINE, "Dependency on application '"+dependency.getRequiredApplication()+"' found!");
			Application requiredApplication = getApplicationByName(applicationsToInstall, dependency.getRequiredApplication());
			if ( requiredApplication == null ) {
				// check if application is already installed
				requiredApplication = getApplicationByName(installedApplications.entrySet(), dependency.getRequiredApplication()); 
				if ( requiredApplication == null ) {
					// FIXME: Throw exception and abort startup
					logger.log(Level.SEVERE, "Required application '"+dependency.getRequiredApplication()+"' not found!");
				}
			} else {
				logger.log(Level.FINE, "Required application '"+requiredApplication.getName()+"' not installed but available, so install it first");
				addRequiredApplicationsFirst(sortedApplications, applicationsToInstall, requiredApplication);
			}
		}
		if ( !sortedApplications.contains(application) ) {
			logger.log(Level.FINE, "Adding '"+application.getName()+"' to installation process");
			sortedApplications.add(application);
		}
	}
    
    private Application getApplicationByName(Collection applications, String name) {
		for ( Iterator i = applications.iterator(); i.hasNext(); ) {
			Application application = (Application)i.next();
			if ( application.getName().equals(name)) {
				return application;
			} 
		} 
		return null;
    }
    
    private Application parseApplication(URI applicationUri) {
    	try {
    		SimpleImporter importer = new SimpleImporter();
    		URI applicationDefinition = new URIValue(applicationUri.toString()+APPLICATION_CONFIG);
    		Streamable applicationDefinitionResouce = ((Streamable)Projector.getRepository().getResource(applicationDefinition, context.getCredentials())); 
    		if ( applicationDefinitionResouce != null ) {
    			InputStream configuration = applicationDefinitionResouce.getInputStream();
    			ConfigurationHandler handler = new ConfigurationHandler(applicationUri);
    			importer.addSimpleImportHandler(handler);
    			importer.parse(new InputSource(configuration));
    			return handler.getApplication(); 
    		} else {
    			logger.log(Level.SEVERE, "Application definition (application.xml) not found in directory '"+applicationUri+"'. Application will not be installed!");
    		}
    	} catch (ParserConfigurationException e) {
    		logger.log(Level.SEVERE, "Exception while parsing application configuration. Skipping installation...", e);
    	} catch (SAXException e) {
    		logger.log(Level.SEVERE, "Exception while parsing application configuration. Skipping installation...", e);
        } catch (RepositoryException e) {
            logger.log(Level.SEVERE, "Could not get application information. Skipping installation...", e);
    	} catch (IOException e) {
    		logger.log(Level.SEVERE, "Could not get application information. Skipping installation...", e);
		}				
		return null;
    }
    
    private Application getApplication(List applications, URI applicationUri) {
    	for ( Iterator i = applications.iterator(); i.hasNext(); ) {
    		Application application = (Application)i.next();
    		if ( application.getUri().equals(applicationUri) ) return application; 
    	}
    	return null;
    }
    
	private synchronized void updateApplication(String uri) {
		URI applicationUri = new URIValue(uri);
        logger.log(Level.FINE, "Updating application '"+applicationUri+"'");
        // Compare newly parsed application with previously installed and send diffs
        Application installedApplication = (Application)installedApplications.get(applicationUri);
        Application updatedApplication = parseApplication(applicationUri);
        for ( Iterator i = installedApplication.getContent().entrySet().iterator(); i.hasNext(); ) {
        	Map.Entry entry = (Map.Entry)i.next();
            List removed = new ArrayList();
            removed.addAll((List)entry.getValue());
            List updated = updatedApplication.getContent((String)entry.getKey());
            if ( updated != null ) {
            	removed.removeAll(updated);
            }
    		for ( Iterator j = removed.iterator(); j.hasNext(); ) {
    			for ( Iterator k = applicationListeners.iterator(); k.hasNext(); ) {
    				((ApplicationListener)k.next()).uninstall((String)entry.getKey(), updatedApplication.getUri(), (URI)j.next());
    			}
    		}
        }
        for ( Iterator i = updatedApplication.getContent().entrySet().iterator(); i.hasNext(); ) {
        	Map.Entry entry = (Map.Entry)i.next();
            List added = new ArrayList();
            added.addAll((List)entry.getValue());
            List installed = installedApplication.getContent((String)entry.getKey());
            if ( installed != null ) {
            	added.removeAll(installed);
            }
    		for ( Iterator j = added.iterator(); j.hasNext(); ) {
    			for ( Iterator k = applicationListeners.iterator(); k.hasNext(); ) {
    				((ApplicationListener)k.next()).install((String)entry.getKey(), updatedApplication.getUri(), (URI)j.next());
    			}
    		}
        }
	}	

	private void install(String type, Application application) {
        logger.log(Level.FINE, "Installing "+type+" of application '"+application.getUri()+"'");
    	List contents = (List)application.getContent().get(type);
    	if ( contents != null ) {
    		for ( Iterator j = contents.iterator(); j.hasNext(); ) {
    			URI uri = (URI)j.next();
    			for ( Iterator k = applicationListeners.iterator(); k.hasNext(); ) {
    				((ApplicationListener)k.next()).install(type, application.getUri(), uri);
    			}
    		}
    	}
    	installedApplications.put(application.getUri(), application);
	}
	
	private void uninstall(String type, Application application) {
        logger.log(Level.FINE, "Uninstall "+type+" of application '"+application.getUri()+"'");
		for ( Iterator i = application.getContent().entrySet().iterator(); i.hasNext(); ) {
			Map.Entry entry = (Map.Entry)i.next();
			for ( Iterator j = ((List)entry.getValue()).iterator(); j.hasNext(); ) {
				URI uri = (URI)j.next();
				for ( Iterator k = applicationListeners.iterator(); k.hasNext(); ) {
					((ApplicationListener)k.next()).uninstall((String)entry.getKey(), application.getUri(), uri);
				}
			}
		}
		// FIXME: Remove subscriber
		installedApplications.remove(application.getUri());
	}
	
    private final class ConfigurationHandler extends DefaultSimpleImportHandler {
		private Application application;
		private URI applicationUri;
		
		private ConfigurationHandler(URI applicationUri) {
			this.applicationUri = applicationUri;
		}

		private Application getApplication() {
			return application;
		}
		
		public void startElement(SimplePath path, String name, AttributesImpl attributes, String leadingCDdata) {
            if (path.matches("application")) {
				application = new Application(applicationUri);
            } else if ( path.matches("application/name") ) {
				application.setName(leadingCDdata);			
            } else if ( path.matches("application/display-name") ) {
				application.setDisplayName(leadingCDdata);			
            } else if ( path.matches("application/vendor") ) {
				application.setVendor(leadingCDdata);			
            } else if ( path.matches("application/description") ) {
				application.setDescription(leadingCDdata);			
            } else if ( path.matches("application/version") ) {
				application.setVersion(leadingCDdata);			
            } else if ( path.matches("application/dependencies/requires") ) {
            	Dependency dependency = new Dependency(attributes.getValue("application"), attributes.getValue("version"));
				application.addDependency(dependency);			
            } else if ( path.matches("application/resource-types/resource-type") ) {
				String resourceTypeName = attributes.getValue("name");
				String clazz = attributes.getValue("class");
				try {
					ConstraintsFactory descriptorFactory = (ConstraintsFactory)factoryClassLoader.loadClass(clazz).getConstructor(new Class[0]).newInstance(new Object[0]);
					ConstraintsManager.getInstance().registerDescriptorFactory(descriptorFactory);
					logger.log(Level.FINE, "Successfully registered descriptor factory " + clazz);
                } catch (Exception e) {
                    logger.log(Level.SEVERE, "Descriptor factory " + clazz + " could not loaded!", e);
                }
            } else if ( path.matches("application/content/processors") ) {
            	String uri = attributes.getValue("uri"); 
            	application.addContent(Application.PROCESSORS, new URIValue(applicationUri+attributes.getValue("uri")));			
            } else if ( path.matches("application/content/messages") ) {
            	application.addContent(Application.MESSAGES, new URIValue(applicationUri+attributes.getValue("uri")));			
            } else if ( path.matches("application/content/jobs") ) {
            	application.addContent(Application.JOBS, new URIValue(applicationUri+attributes.getValue("uri")));			
            }
        }
    }
}