package eu.dnetlib.xml.database.exist; // NOPMD

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exist.collections.CollectionConfiguration;
import org.exist.util.DatabaseConfigurationException;
import org.exist.xmldb.DatabaseImpl;
import org.exist.xmldb.DatabaseInstanceManager;
import org.exist.xmldb.XmldbURI;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.context.Lifecycle;
import org.xmldb.api.DatabaseManager;
import org.xmldb.api.base.Collection;
import org.xmldb.api.base.Database;
import org.xmldb.api.base.Resource;
import org.xmldb.api.base.ResourceSet;
import org.xmldb.api.base.XMLDBException;
import org.xmldb.api.modules.CollectionManagementService;
import org.xmldb.api.modules.XPathQueryService;

import com.google.common.collect.Lists;

import eu.dnetlib.miscutils.datetime.DateUtils;
import eu.dnetlib.xml.database.Trigger;
import eu.dnetlib.xml.database.XMLDBResultSet;
import eu.dnetlib.xml.database.XMLDatabase;

/**
 * eXist database wrapper.
 *
 * @author marko
 *
 */
public class ExistDatabase implements XMLDatabase, Lifecycle { // NOPMD by marko

	/**
	 * logger.
	 */
	private static final Log log = LogFactory.getLog(ExistDatabase.class); // NOPMD

	/**
	 * eXist collection configuration special file.
	 */
	public static final String COLLECTION_XCONF = "collection.xconf";

	/**
	 * exist xml resource type code.
	 */
	private static final String XMLRESOURCE = "XMLResource";

	/**
	 * collection name to trigger instance map.
	 *
	 * all triggers declared here will be registered at startup.
	 *
	 */
	private Map<String, Trigger> triggerConf = new HashMap<String, Trigger>();

	/**
	 * eXist database.
	 */
	private Database database;

	/**
	 * eXist collection.
	 */
	private Collection root;
	/**
	 * eXist database manager.
	 */
	private DatabaseInstanceManager manager;
	/**
	 * eXist xpath service.
	 */
	private XPathQueryService queryService;
	/**
	 * eXist collection manager.
	 */
	private CollectionManagementService colman;
	/**
	 * eXist configuration file.
	 */
	private String configFile;

	/**
	 * Directory in which backups are saved.
	 */
	private String backupDir;

	/**
	 * {@inheritDoc}
	 *
	 * @see org.springframework.context.Lifecycle#start()
	 */
	@Override
	public void start() {
		log.info("starting database");
		try {
			if (getDatabase() == null) {
				setDatabase(new DatabaseImpl());
				getDatabase().setProperty("configuration", getConfigFile());
				getDatabase().setProperty("create-database", "true");
			}

			DatabaseManager.registerDatabase(getDatabase());

			setRoot(DatabaseManager.getCollection("xmldb:exist://" + getRootCollection(), "admin", ""));
			setManager((DatabaseInstanceManager) getRoot().getService("DatabaseInstanceManager", "1.0"));
			setQueryService((XPathQueryService) getRoot().getService("XPathQueryService", "1.0"));
			setColman((CollectionManagementService) getRoot().getService("CollectionManagementService", "1.0"));

			for (final Entry<String, Trigger> entry : getTriggerConf().entrySet())
				registerTrigger(entry.getValue(), entry.getKey());

		} catch (final XMLDBException e) {
			throw new IllegalStateException("cannot open eXist database", e);
		}
	}

	/**
	 * helper method.
	 *
	 * @param collection
	 *            collection name
	 * @return an eXist collection
	 * @throws XMLDBException
	 *             happens
	 */
	protected Collection getCollection(final String collection) throws XMLDBException {
		if (!collection.startsWith("/db"))
			throw new XMLDBException(0, "collection path should begin with /db");
		return database.getCollection("exist://" + collection, "admin", "");
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#create(java.lang.String, java.lang.String, java.lang.String)
	 */
	@Override
	public void create(final String name, final String collection, final String content) throws XMLDBException {

		if("".equals(name))
			throw new XMLDBException(0, "cannot create a xml file with an empty file name");

		Collection col = getCollection(collection);

		if (col == null) {
			// create parent collections
			createCollection(collection, true);
			col = getCollection(collection);
		}

		final Resource res = col.createResource(name, XMLRESOURCE);
		res.setContent(content);
		col.storeResource(res);
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#remove(java.lang.String, java.lang.String)
	 */
	@Override
	public boolean remove(final String name, final String collection) throws XMLDBException {
		final Collection col = getCollection(collection);

		final Resource res = col.getResource(name);
		if (res == null)
			return false;

		col.removeResource(res);
		return true;
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#update(java.lang.String, java.lang.String, java.lang.String)
	 */
	@Override
	public void update(final String name, final String collection, final String content) throws XMLDBException {

		final Collection col = getCollection(collection);

		final Resource res = col.getResource(name);
		if (res == null) {
			throw new XMLDBException(0, "resource doesn't exist");
		}
		res.setContent(content);
		col.storeResource(res);

	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#read(java.lang.String, java.lang.String)
	 */
	@Override
	public String read(final String name, final String collection) throws XMLDBException {
		final Collection coll = getCollection(collection);
		if (coll == null)
			return null;
		final Resource res = coll.getResource(name);
		if (res != null)
			return (String) res.getContent();
		return null;
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#xquery(java.lang.String)
	 */
	@Override
	public XMLDBResultSet xquery(final String xquery) throws XMLDBException {
		final ResourceSet res = getQueryService().query(xquery);
		return createResultSet(res);
	}

	/**
	 * override if you need to return a custom resultset subclass.
	 *
	 * @param res
	 *            resource set
	 * @return exist resultset
	 */
	protected ExistResultSet createResultSet(final ResourceSet res) {
		return new ExistResultSet(res);
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see org.springframework.context.Lifecycle#stop()
	 */
	@Override
	public void stop() {
		// no operation
		try {
			getManager().shutdown();
			DatabaseManager.deregisterDatabase(database);
		} catch (final XMLDBException e) {
			log.fatal("cannot close database", e);
		}
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#collectionExists(java.lang.String)
	 */
	@Override
	public boolean collectionExists(final String collection) throws XMLDBException {
		final Collection col = getCollection(collection);
		return col != null;
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#createCollection(java.lang.String)
	 */
	@Override
	public void createCollection(final String collection) throws XMLDBException {
		createCollection(collection, false);
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#createCollection(java.lang.String, boolean)
	 */
	@Override
	public void createCollection(final String collection, final boolean recursive) throws XMLDBException {
		if (recursive) {
			final XmldbURI uri = XmldbURI.create(collection).removeLastSegment();
			if (!collectionExists(uri.toString()))
				createCollection(uri.toString(), true);
		}

		getColman().createCollection(collection);
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#removeCollection(java.lang.String)
	 */
	@Override
	public void removeCollection(final String collection) throws XMLDBException {
		getColman().removeCollection(collection);
	}

	public String getConfigFile() {
		return configFile;
	}

	public void setConfigFile(final String configFile) {
		this.configFile = configFile;
	}


	@Override
	public String getBackupDir() {
		return backupDir;
	}

	@Required
	public void setBackupDir(final String backupDir) {
		this.backupDir = backupDir;
	}



	/**
	 * {@inheritDoc}
	 *
	 * @see org.springframework.context.Lifecycle#isRunning() useless contract with spring.
	 *
	 */
	@Override
	public boolean isRunning() {
		return false;
	}

	protected Database getDatabase() {
		return database;
	}

	protected void setDatabase(final Database database) {
		this.database = database;
	}

	protected Collection getRoot() {
		return root;
	}

	protected void setRoot(final Collection root) {
		this.root = root;
	}

	protected DatabaseInstanceManager getManager() {
		return manager;
	}

	protected void setManager(final DatabaseInstanceManager manager) {
		this.manager = manager;
	}

	protected XPathQueryService getQueryService() {
		//		try {
		//			return (XPathQueryService) getRoot().getService("XPathQueryService", "1.0");
		//		} catch (XMLDBException e) {
		//			throw new IllegalStateException("cannot obtain xpath query service", e);
		//		}
		return queryService;
	}

	protected void setQueryService(final XPathQueryService queryService) {
		this.queryService = queryService;
	}

	protected CollectionManagementService getColman() {
		return colman;
	}

	protected void setColman(final CollectionManagementService colman) {
		this.colman = colman;
	}

	@Override
	public String getRootCollection() {
		return "/db";
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#listChildCollections(java.lang.String)
	 */
	@Override
	public List<String> listChildCollections(final String collection) throws XMLDBException {
		final Collection col = getCollection(collection);
		if(col == null)
			return Lists.newArrayList();
		return Lists.newArrayList(col.listChildCollections());
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#list(java.lang.String)
	 */
	@Override
	public List<String> list(final String collection) throws XMLDBException {
		final Collection col = getCollection(collection);
		if(col == null)
			return Lists.newArrayList();
		return Lists.newArrayList(col.listResources());
	}

	/**
	 * sets an underlying eXist trigger class for a given collection.
	 *
	 * @param triggerClass
	 *            exist trigger class
	 * @param collection
	 *            collection name
	 * @param events
	 *            list of event names
	 * @param parameters
	 *            parameter map
	 * @throws XMLDBException
	 *             happens
	 */
	void setExistTrigger(final Class<?> triggerClass, final String collection, final List<String> events, final Map<String, String> parameters)
	throws XMLDBException {

		// Arrays.asList(new String[] { "store", "update", "delete" }

		final StringBuilder conf = new StringBuilder();
		conf.append("<exist:collection xmlns:exist=\"http://exist-db.org/collection-config/1.0\"><exist:triggers>");

		final String className = triggerClass.getCanonicalName(); // PMD

		conf.append("<exist:trigger event=\"store,update,remove\" class=\"" + className + "\">");
		if (parameters != null)
			for (final Entry<String, String> entry : parameters.entrySet())
				conf.append("<exist:parameter name=\"" + entry.getKey() + "\" value=\"" + entry.getValue() + "\"/>");
		conf.append("</exist:trigger>");

		conf.append("</exist:triggers></exist:collection>");

		log.info(conf.toString());

		//		createCollection("/db/system");
		//		createCollection("/db/system/config");
		//		createCollection("/db/system/config/db");
		//		createCollection("/db/system/config/db/testCollection");
		createCollection("/db/system/config" + collection, true);
		create(CollectionConfiguration.DEFAULT_COLLECTION_CONFIG_FILE_URI.toString(), "/db/system/config" + collection, conf.toString());
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#registerTrigger(eu.dnetlib.xml.database.Trigger, java.lang.String)
	 */
	@Override
	public void registerTrigger(final Trigger trigger, final String collection) throws XMLDBException {
		final Map<String, String> params = new HashMap<String, String>();
		params.put("triggerName", trigger.getName());

		ExistTriggerRegistry.defaultInstance().registerTrigger(trigger.getName(), trigger);

		setExistTrigger(DelegatingDiffTrigger.class, collection, Lists.newArrayList("store", "update", "delete" ), params);
	}

	public Map<String, Trigger> getTriggerConf() {
		return triggerConf;
	}

	public void setTriggerConf(final Map<String, Trigger> triggerConf) {
		this.triggerConf = triggerConf;
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#backup()
	 */
	@Override
	public String backup() throws XMLDBException, DatabaseConfigurationException {
		log.info("Starting backup...");
		try {
			/*			final Configuration config = new Configuration(getConfigFile());
				final BrokerPool pool = BrokerPool.getInstance();
				final DBBroker broker = BrokerFactory.getInstance(pool, config);

				final Properties properties = new Properties();
				properties.put("output", backupDir);
				properties.put("backup", "yes");
				properties.put("incremental", "yes");

				final ConsistencyCheckTask task = new ConsistencyCheckTask();
				task.configure(config, properties);
				task.execute(broker);
			 */

			//String seq = "20101021-1339";
			
			
			verifyBackupDir();

			String seq = (new SimpleDateFormat("yyyyMMdd-HHmm")).format(new Date());

			ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(backupDir + "/data-" + seq + ".zip")); 

			FileWriter logFile = new FileWriter(backupDir + "/report-" + seq + ".log");
			logFile.write("Backup started at: " + DateUtils.now_ISO8601() + "\n\n");

			backup(getRoot().getName(), zip, logFile);

			logFile.write("\nBackup finished at: " + DateUtils.now_ISO8601() + "\n");

			logFile.flush();
			logFile.close();

			zip.flush();
			zip.close();

			log.info("Backup finished");
			return backupDir;
		} catch (final Exception e) {
			log.error("Backup failed", e);
			throw new XMLDBException(0, "cannot backup", e);
		}
	}


	private void verifyBackupDir() {
		File d = new File(backupDir);
		if (!d.exists()) d.mkdirs();
	}

	private void backup(String coll, ZipOutputStream zip, FileWriter logFile) throws XMLDBException, IOException {
		logFile.write("COLLECTION: " + coll + "\n");
		log.info("Backup of collection " + coll);

		for (String file : list(coll)) {
			zip.putNextEntry(new ZipEntry(coll + "/" + file + ".xml"));
			Resource resource = getCollection(coll).getResource(file);
			zip.write(resource.getContent().toString().getBytes());
			zip.closeEntry();
		}

		for (String c : listChildCollections(coll)) {
			backup(coll+"/"+c, zip, logFile);
		}
	} 


}
