/**
 * TemplateLoaderService.java
 * Created Jul 6, 2007
 * Copyright (c) Tandberg Television 2007
 */
package com.tandbergtv.workflow.driver.template;

import static com.tandbergtv.workflow.core.service.ServiceEvents.STARTED;
import static com.tandbergtv.workflow.core.service.ServiceEvents.STOPPED;
import static com.tandbergtv.workflow.driver.template.event.TemplateEvents.CACHED;
import static com.tandbergtv.workflow.driver.template.event.TemplateEvents.CREATED;
import static com.tandbergtv.workflow.driver.template.event.TemplateEvents.DELETED;
import static com.tandbergtv.workflow.driver.template.event.TemplateEvents.UPDATED;
import static java.io.File.separator;

import java.io.File;
import java.io.Serializable;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable;

import org.apache.log4j.Logger;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;

import com.tandbergtv.workflow.core.Selector;
import com.tandbergtv.workflow.core.WorkflowTemplate;
import com.tandbergtv.workflow.core.event.ColleaguePriority;
import com.tandbergtv.workflow.core.event.DefaultMediator;
import com.tandbergtv.workflow.core.event.IColleague;
import com.tandbergtv.workflow.core.event.IMediator;
import com.tandbergtv.workflow.core.event.WorkflowEvent;
import com.tandbergtv.workflow.core.service.ServiceEvent;
import com.tandbergtv.workflow.core.service.cache.CacheService;
import com.tandbergtv.workflow.core.service.cache.ICacheService;
import com.tandbergtv.workflow.core.service.thread.ISchedulerService;
import com.tandbergtv.workflow.core.service.thread.Scheduler;
import com.tandbergtv.workflow.driver.DriverException;
import com.tandbergtv.workflow.driver.DriverRuntimeException;
import com.tandbergtv.workflow.driver.service.ITemplateLoaderService;
import com.tandbergtv.workflow.driver.template.event.TemplateEvent;
import com.tandbergtv.workflow.driver.template.event.TemplateEvents;

/**
 * Default template loader service implementation. Uses Hibernate for ORM.
 * 
 * @author Sahil Verma
 */
public class TemplateLoaderService implements ITemplateLoaderService, IColleague {

	private static final String SERVICE_NAME = "Template Loader Service";
	
	private ICacheService<WorkflowTemplate> cache;
	
	private IArchivalService archiver;
	
	private ISchedulerService<Void> scheduler;
	
	private int max;
	
	private SessionFactory factory;
	
	private static final String REJECTED = "rejected";
	
	private static final String IMPORTED = "imported";
	
	private static final Logger logger = Logger.getLogger(TemplateLoaderService.class);
	
	/**
	 * Creates a {@link TemplateLoaderService}
	 */
	public TemplateLoaderService(Properties properties, SessionFactory factory) {
		this.max = Integer.parseInt(properties.getProperty("template.max.count"));
		this.factory = factory;
		ResourceBundle bundle = 
			ResourceBundle.getBundle(this.getClass().getPackage().getName() + ".service");
		int size = Integer.parseInt(bundle.getString("template.cache.size"));
		String name = bundle.getString("template.cache.name");
		
		cache = new CacheService<WorkflowTemplate>(name, size);
		scheduler = new Scheduler<Void>("template", 1, 1);
		archiver = new ArchivalService(scheduler);
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.ITemplateLoaderService#getAllProcessDefinitionNames()
	 */
	public List<String> getAllTemplateNames() {
		List<WorkflowTemplate> templates = getAllTemplates();
		Collections.sort(templates, new TemplateComparator());
		List<String> names = new ArrayList<String>();
		Map<String, Integer> map = new HashMap<String, Integer>();
		
		for (WorkflowTemplate template : templates) {
			String name = template.getName();
			
			if (!map.containsKey(name)) {
				map.put(name, template.getVersion());
				names.add(template.getFullName());
			}
		}
		
		return names;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.ITemplateLoaderService#getAllProcessDefinitions()
	 */
	public List<WorkflowTemplate> getAllTemplates() {
		List<WorkflowTemplate> templates = new ArrayList<WorkflowTemplate>();
		
		for (Serializable key : getCache().getKeys())
			templates.add(getCache().get(key));
		
		return templates;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.ITemplateLoaderService#getProcessDefinition(java.io.Serializable)
	 */
	public WorkflowTemplate getTemplate(Serializable id) {
		/* FIXME License check */
		return getCache().get(id);
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.ITemplateLoaderService#getProcessDefinitionByName(java.lang.String)
	 */
	public WorkflowTemplate getTemplateByFullName(String name) {
		List<WorkflowTemplate> templates = getAllTemplates();
		Collections.sort(templates, new TemplateComparator());
		
		for (WorkflowTemplate template : templates) {
			if (name.equals(template.getFullName()))
				return template;
		}
		
		return null;
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see com.tandbergtv.workflow.driver.service.ITemplateLoaderService#getProcessDefinitionByName(java.lang.String)
	 */
	public WorkflowTemplate getTemplateByName(String name) {
		List<WorkflowTemplate> templates = getAllTemplates();
		Collections.sort(templates, new TemplateComparator());

		for (WorkflowTemplate template : templates) {
			if (name.equals(template.getName()))
				return template;
		}

		return null;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.ITemplateLoaderService#getLatestTemplates()
	 */
	public List<WorkflowTemplate> getLatestTemplates() {
		List<WorkflowTemplate> templates = getAllTemplates();
		Collections.sort(templates, new TemplateComparator());
		List<WorkflowTemplate> list = new ArrayList<WorkflowTemplate>();
		Set<String> names = new HashSet<String>();
		
		for (WorkflowTemplate template : templates) {
			String name = template.getName();
			
			if (!names.contains(name)) {
				names.add(name);
				list.add(template);
			}
		}
		
		return list;
	}
	
	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.ITemplateLoaderService#getPreviousTemplates(com.tandbergtv.workflow.core.WorkflowTemplate)
	 */
	public List<WorkflowTemplate> getPreviousVersions(WorkflowTemplate template) {
		List<WorkflowTemplate> templates = new LinkedList<WorkflowTemplate>();
		int version = template.getVersion();
		
		for (WorkflowTemplate t : getAllTemplates()) {
			if (t.getName().equals(template.getName()) && t.getVersion() < version)
				templates.add(t);
		}
		
		return templates;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.ITemplateLoaderService#getTemplateBySelector(java.lang.String)
	 */
	public WorkflowTemplate getTemplateBySelectorKey(String selectorKey) {
		for (WorkflowTemplate template : getLatestTemplates()) {
			for (Selector selector : template.getSelectorKeys()) {
				if (selector.getSelectionKey().equals(selectorKey))
					return template;
			}
		}
		
		throw new DriverRuntimeException("Failed to find template for selector key " + selectorKey);
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.ITemplateLoaderService#save(com.tandbergtv.workflow.core.WorkflowTemplate)
	 */
	public void save(WorkflowTemplate template) throws DriverException {
		checkVersion(template);
		checkLicense(template);
		
		WorkflowTemplate previous = getLatestTemplate(template.getName());

		Session session = null;
		Transaction t = null;

		try {
			session = getSession();
			t = session.beginTransaction();
			
			if (previous != null) {
				for (Selector selector : previous.getSelectorKeys())
					template.addSelectorKey(new Selector(selector.getSelectionKey(), template));
				
				previous.removeSelectorKeys();
				session.saveOrUpdate(previous);
				
				/*
				 * This explicitly deletes the selector keys, otherwise we'll get unique constraint
				 * violation when we create the new template
				 */
				session.flush();
			}
			
			session.saveOrUpdate(template);
			t.commit();
			
			logger.info("Saved template " + template);
			
			fireEvent(template, CREATED);
			
			cache(template);
			archiver.run(template);
		} catch (Exception e) {
			if (t != null)
				t.rollback();
			throw new DriverException("Failed to create template " + template, e);
		} finally {
			closeSession(session);
		}
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.ITemplateLoaderService#update(com.tandbergtv.workflow.core.WorkflowTemplate)
	 */
	public void update(WorkflowTemplate template) throws DriverException {
		Session session = null;
		Transaction t = null;
		
		try {
			session = getSession();
			t = session.beginTransaction();
			session.saveOrUpdate(template);
			t.commit();
			logger.debug("Updated template " + template);
			
			fireEvent(template, UPDATED);
		} catch (Exception e) {
			if (t != null)
				t.rollback();
			throw new DriverException("Failed to update template " + template, e);
		} finally {
			closeSession(session);
		}
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.ITemplateLoaderService#delete(com.tandbergtv.workflow.core.WorkflowTemplate)
	 */
	public void delete(final WorkflowTemplate template) throws DriverException {
		logger.info("Deleting template " + template);
		Session session = null;
		Transaction t = null;
		
		try {
			session = getSession();
			t = session.beginTransaction();
			Statement statement = session.connection().createStatement(); 
			
			/* UGLY */
			statement.executeUpdate(
				"update JBPM_PROCESSDEFINITION set PROCESSDEFINITIONTYPEID = 1 where ID_ = " + template.getId());
			t.commit();
			remove(template);

			fireEvent(template, DELETED);
		} catch (Exception e) {
			if (t != null)
				t.rollback();
			throw new DriverException("Failed to delete template " + template, e);
		} finally {
			closeSession(session);
		}
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.ITemplateLoaderService#addSelector(java.io.Serializable, com.tandbergtv.workflow.core.Selector)
	 */
	public void addSelector(Serializable id, Selector selector) throws DriverException {
		for (WorkflowTemplate t : getAllTemplates()) {
			for (Selector s : t.getSelectorKeys()) {
				if (s.getSelectionKey().equals(selector.getSelectionKey()))
					throw new SelectorExistsException("Key " + selector.getSelectionKey() + 
						" is mapped to template " + t.getFullName());
			}
		}
		
		WorkflowTemplate template = getTemplate(id);
		
		if (template == null)
			throw new DriverException("Failed to find template [" + id + "]");
		
		template.addSelectorKey(selector);
		update(template);
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.driver.service.ITemplateLoaderService#removeSelector(java.io.Serializable, com.tandbergtv.workflow.core.Selector)
	 */
	public void removeSelector(Serializable id, Selector selector) throws DriverException {
		WorkflowTemplate template = getTemplate(id);
		
		selector.setTemplate(template);
		template.removeSelectorKey(selector);
		update(template);
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.Service#getServiceName()
	 */
	public String getServiceName() {
		return SERVICE_NAME;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#start()
	 */
	public void start() {
		cache.start();
		
		DefaultMediator.getInstance().register(this);
		
		scheduler.schedule(new Callable<Void>() {
			@Override
			public Void call() throws Exception {
				try {
					logger.debug("Loading cache...");
					Collection<WorkflowTemplate> templates = getAll();
				
					for (WorkflowTemplate template : templates)
						cache(template);
					
					logger.info("Loaded " + cache.count() + " templates");
				
					DefaultMediator.getInstance().sendAsync(
						new ServiceEvent(TemplateLoaderService.this, STARTED));
				} catch (Exception e) {
					logger.error("Failed to load templates", e);
				}
				
				archiver.start();
				
				return (Void) null;
			}
		});
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.service.ServiceLifecycle#stop()
	 */
	public void stop() {
		cache.stop();
		archiver.stop();
		scheduler.stop();
		
		IMediator mediator = DefaultMediator.getInstance();
		
		mediator.unregister(this);
		mediator.sendAsync(new ServiceEvent(this, STOPPED));
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.event.IColleague#getColleagueName()
	 */
	@Override
	public String getColleagueName() {
		return getServiceName();
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.event.IColleague#getColleaguePriority()
	 */
	@Override
	public ColleaguePriority getColleaguePriority() {
		return ColleaguePriority.LOW;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.core.event.IColleague#receive(com.tandbergtv.workflow.core.event.WorkflowEvent)
	 */
	@Override
	public void receive(WorkflowEvent event) {
		if (!(event instanceof TemplateEvent))
			return;
		
		TemplateEvent e = TemplateEvent.class.cast(event);
		
		logger.debug(e.getTemplate() + ", " + e.getType());
		
		switch (e.getType()) {
			case COMPILED : 
				ingest((File) e.getSource(), e.getTemplate());
				break;
			case COMPILE_ERROR : 
				File file = (File) e.getSource();
				String dir = file.getParentFile().getParentFile() + separator + REJECTED;
				
				move(dir, file);
				break;
			default : break;
		}
	}
	
	/**
	 * Adds the template to the cache. Make sure the write lock is held.
	 * 
	 * @param template
	 */
	private void cache(WorkflowTemplate template) {
		logger.debug("Adding to cache " + template);
		getCache().add(template.getId(), template);
		
		fireEvent(template, CACHED);
	}
	
	private void remove(WorkflowTemplate template) {
		logger.debug("Removing from cache " + template);
		getCache().remove(template.getId());
	}
	
	/**
	 * Gets the cache of templates
	 * 
	 * @return
	 */
	private ICacheService<WorkflowTemplate> getCache() {
		return this.cache;
	}
	
	/**
	 * Checks if a template with the same or newer version exists
	 * 
	 * @param template
	 * @throws DriverException
	 */
	private void checkVersion(WorkflowTemplate template) throws DriverException {
		WorkflowTemplate latest = getLatestTemplate(template.getName());
		
		if (latest != null && latest.getVersion() >= template.getVersion())
			throw new DriverException("Template " + template + " - version must exceed " + latest.getVersion());
	}
	
	/**
	 * Ensures that adding the specified template will not cause a license violation
	 * 
	 * @param template
	 * @throws DriverException
	 */
	private void checkLicense(WorkflowTemplate template) throws DriverException {
		int count = getLicenseCount();

		for (WorkflowTemplate t : getLatestTemplates()) {
			/* If older versions of this template already exist, we're fine */
			if (t.getName().equals(template.getName()))
				return;
		}
		
		if (getLatestTemplates().size() >= count)
			throw new DriverException("License unavailable for template " + template.getFullName() 
				+ ", limit = " + count);
	}
	
	/**
	 * Retrieves a template with the specified name that has the largest version number
	 * 
	 * @param name
	 * @return
	 */
	private WorkflowTemplate getLatestTemplate(String name) {
		List<WorkflowTemplate> templates = getAllTemplates();
		Collections.sort(templates, new TemplateComparator());
		
		for (WorkflowTemplate template : templates) {
			if (name.equals(template.getName()))
				return template;
		}
		
		return null;
	}
	
	/**
	 * Loads all the templates from the persistent store
	 * 
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private Collection<WorkflowTemplate> getAll() {
		List<WorkflowTemplate> list = new ArrayList<WorkflowTemplate>();
		Session session = null;
		
		try {
			session = getSession();
			list = session.createCriteria(WorkflowTemplate.class).list();
		} finally {
			closeSession(session);
		}
		
		Collection<WorkflowTemplate> templates = new TreeSet<WorkflowTemplate>(new Comparator<WorkflowTemplate>() {
			public int compare(WorkflowTemplate o1, WorkflowTemplate o2) {
				return o1.getCreateDate().compareTo(o2.getCreateDate());
			}
		});
		
		templates.addAll(list);
		
		return templates;
	}
	
	/**
	 * Ingests the specified template. The JPDL XML file is moved to a different folder.
	 * 
	 * @param file
	 * @param template
	 */
	private void ingest(final File file, final WorkflowTemplate template) {
		scheduler.schedule(new Callable<Void>() {
			@Override
			public Void call() throws Exception {
				String path = file.getAbsolutePath();
				
				try {
					save(template);
					
					String dir = file.getParentFile().getParentFile() + File.separator + IMPORTED;
					
					if (!move(dir, file))
						logger.warn("Failed to move file " + file.getAbsolutePath() + " to " + dir);			
				} catch (Exception e) {
					logger.error("Failed to ingest template " + path, e);
					String dir = file.getParentFile().getParentFile() + File.separator + REJECTED;

					if (!move(dir, file))
						logger.warn("Failed to move file " + path + " to " + dir);
				}
				
				return null;
			}
		});
	}
	
	/**
	 * Renames a file into the specified directory path with the current timestamp
	 * 
	 * @param dir
	 * @param file
	 * @return
	 */
	private boolean move(String dir, File file) {
		String time = new SimpleDateFormat("yyyy-MM-dd-HHmmSS").format(new Date());
		
		return file.renameTo(new File(dir + separator + file.getName().replace(".", "-") + "-" + time + ".xml"));
	}
	
	private void fireEvent(WorkflowTemplate template, TemplateEvents e) {
		DefaultMediator.getInstance().sendAsync(new TemplateEvent(this, template, e));
	}
	
	/**
	 * Gets the license limit for templates. If a limit is not defined, returns Integer.MAX_VALUE.
	 * 
	 * @return
	 */
	private int getLicenseCount() {
		return this.max;
	}
	
	private Session getSession() {
		return factory.openSession();
	}
	
	private void closeSession(Session session) {
		if (session != null)
			session.close();
	}
	
	/*
	 * Comparator class that compares the names of ProcessDefinitions
	 */
	class TemplateComparator implements Comparator<WorkflowTemplate> {
		
		/* (non-Javadoc)
		 * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
		 */
		public int compare(WorkflowTemplate t1, WorkflowTemplate t2) {
			/* Lower versions are at the top */
			if (t1.getName().equals(t2.getName()))
				return t2.getVersion() - t1.getVersion();
			
			return t1.getName().compareTo(t2.getName());
		}
	}
}
