package org.apache.slide.projector.engine;

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.Timer;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.contract.Context;
import org.apache.commons.contract.Processor;
import org.apache.commons.xmlio.out.XMLStringWriter;
import org.apache.commons.xmlio.out.XMLWriter;
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.context.ProjectorContext;
import org.apache.slide.projector.processor.process.Process;
import org.apache.slide.projector.processor.process.Step;
import org.apache.slide.projector.repository.RepositoryException;
import org.apache.slide.projector.value.DocumentValue;
import org.apache.slide.projector.value.Streamable;
import org.apache.slide.projector.value.Text;
import org.apache.slide.projector.value.URI;
import org.apache.slide.projector.value.URIValue;
import org.apache.webdav.lib.Subscriber;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.xpath.XPath;

public class Scheduler implements ApplicationListener {
    private final static Logger logger = Logger.getLogger(Scheduler.class.getName());

    public final static String JOBS = "jobs.xml";
    public final static String JOB_DEFINITIONS = "jobDefinitions.xml";

    private final static String STARTUP = "startup";
    private final static String SESSION = "session";
    private final static String REQUEST = "request";
    private final static String STARTUP_IDENTIFIER = "startup:";

    private final ProjectorContext context = Projector.getSystemContext();

    private static Scheduler scheduler = new Scheduler();
    private static ThreadLocal threadContext = new ThreadLocal();
    private static Timer timer = new Timer(true);

    private List jobs = new ArrayList();
	private List sessionJobs = new ArrayList();
	private List requestJobs = new ArrayList();
    private List restoredJobs = new ArrayList();
    private List installedJobNames = new ArrayList();

    private Map installedJobs = new HashMap();
    
    private Scheduler() {
    }

    public static Scheduler getInstance() {
        return scheduler;
    }

    public void install(String type, URI applicationUri, URI configurationUri) {
    	if ( type == Application.JOBS ) {
    		install(configurationUri, false);
    	}
    }
    
    public void uninstall(String type, URI applicationUri, URI configurationUri) {
    	if ( type == Application.JOBS ) {
    		uninstall(configurationUri);
    	}
    }
    
    public void update(String type, URI applicationUri, URI configurationUri) {
    	if ( type == Application.JOBS ) {
    		update(configurationUri);
    	}
    }
    
    public void install(URI jobsUri, boolean restoreRunningJobs) {
    	logger.log(Level.FINE, "Installing scheduled jobs of application '"+jobsUri+"'");
    	try {
    		List applicationJobs = new ArrayList();
    		Streamable jobsResource = (Streamable)Projector.getRepository().getResource(jobsUri, Projector.getCredentials());
    		// FIXME: Beautify the code...
    		if ( jobsResource != null ) {
    			DocumentValue documentResource = new DocumentValue(jobsResource);
    			Document document = documentResource.getDocument();
    			Element rootElement = document.getRootElement();
    			List jobElements = XPath.newInstance("/jobs/job").selectNodes(rootElement);
    			List startupJobs = new ArrayList();
    			for ( Iterator i = jobElements.iterator(); i.hasNext(); ) {
    				Element jobElement = (Element)i.next();
    				Step job = new Step();
    				job.configure(jobElement);
    				String trigger = jobElement.getAttributeValue("trigger");
    				if ( trigger == null || trigger.equals(STARTUP) ) {
    					if ( !installedJobNames.contains(STARTUP_IDENTIFIER+job.getName()) ) {
    						startupJobs.add(job);
    						installedJobNames.add(job.getName());
    					}
    				} else if ( trigger.equals(REQUEST) ) {
    					requestJobs.add(job);
    				} else if ( trigger.equals(SESSION) ) {
    					sessionJobs.add(job);
    				}
    			}
    			// Note: starting jobs after reading all, to avoid writing to configuration files while reading...
    			for ( Iterator i = startupJobs.iterator(); i.hasNext(); ) { 
    				Step job = (Step)i.next();
    				if ( job.getName().startsWith(STARTUP_IDENTIFIER) ) {
    					context.setStep(job.getName());
    				} else {
    					context.setStep(STARTUP_IDENTIFIER+job.getName());
    				}
    				launchJob(job, context);
    				if ( !restoreRunningJobs ) {
    					Projector.getRepository().subscribe("Update", jobsUri, 0,
								new Subscriber() {
    						public void notify(String uri, Map information) {
    							update(new URIValue(uri));
    						}
    					}, Projector.getCredentials());
    				}
    			}
    		} else {
    			logger.log(Level.FINE, "Configured jobs resource '"+jobsUri+"' not found!");
    		}
    		// FIXME: How to handle deinstallation of running jobs belonging to an application
    		installedJobs.put(jobsUri, applicationJobs);
        } catch (Exception exception) {
            logger.log(Level.SEVERE, "Error while parsing messages", exception);
        }
	}
	
	public void uninstall(URI jobsUri) {
        logger.log(Level.FINE, "Uninstalling jobs '"+jobsUri+"'");
		Collection jobKeys = (Collection)installedJobs.get(jobs);
		for ( Iterator i = jobKeys.iterator(); i.hasNext(); ) {
			String jobKey = (String)i.next();
			jobs.remove(jobKey);
            logger.log(Level.FINE, "Removing job '"+jobKey+"'");
		}
		installedJobs.remove(jobsUri);
	}

	public void update(URI jobsUri) {
		uninstall(jobsUri);
		install(jobsUri, true);
	}	
	
	public void launchSessionJobs(Context context) throws Exception {
		for ( Iterator i = sessionJobs.iterator(); i.hasNext(); ) {
			Step job = (Step)i.next();
			launchJob(job, context);
		}
	}
	
	public void launchRequestJobs(Context context) throws Exception {
		for ( Iterator i = requestJobs.iterator(); i.hasNext(); ) {
			Step job = (Step)i.next();
			launchJob(job, context);
		}
	}
	
	private void launchJob(Step job, Context context) throws Exception {
        logger.log(Level.FINE, "Launching job: '"+job.getName()+"'");
		Processor processor = ProcessorManager.getInstance().getProcessor(job.getProcessorURI());
		Map processorParameters = Process.loadParameters(job, processor, context);
		ProcessorManager.process(processor, processorParameters, context);
	}

	/**
	 * @return Returns the timer.
	 */
	public static Timer getTimer() {
		return timer;
	}

	public void notify(final Job job) {
		if ( job.getRemainingCondition().evaluate() ) {
			job.launch();
			jobs.remove(job);
			if ( job.repeat() ) {
				job.restart();
				registerJob(job);
            }
		}
        if ( job.isPersistent() ) {
           	saveJobs();
        }
	}

    public void registerJob(Job job) {
    	synchronized ( jobs ) {
            jobs.add(job);
        	job.activate();
        }
    }

    public static void setContext(Context context) {
       threadContext.set(context);
    }

    public static Context getContext() {
        if ( threadContext.get() == null ) {
            threadContext.set(Projector.getSystemContext());
        }
        return (Context)threadContext.get();
    }

   	public void saveJobs() {
        XMLStringWriter writer = XMLStringWriter.create();
        writer.writeXMLDeclaration();
        writer.writeStartTag(XMLWriter.createStartTag("jobs"));
        synchronized ( jobs ) {
        	for ( Iterator i = jobs.iterator(); i.hasNext(); ) {
                Job job = (Job)i.next();
                if (job.isPersistent()) {
                	job.save(writer);
                }
            }
        }
        writer.writeEndTag(XMLWriter.createEndTag("jobs"));
        try {
            Projector.getRepository().setResource(new URIValue(Projector.getWorkDir() + JOBS), new Text(writer.toString()), Projector.getCredentials());
        } catch (RepositoryException exception) {
            logger.log(Level.SEVERE, "Exception occured while writing jobs to repository", exception);
        }
    }
}