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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exist.EXistException;
import org.exist.collections.CollectionConfiguration;
import org.exist.storage.BrokerFactory;
import org.exist.storage.BrokerPool;
import org.exist.storage.ConsistencyCheckTask;
import org.exist.storage.DBBroker;
import org.exist.util.Configuration;
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 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()
	 */
	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)
	 */
	public void create(final String name, final String collection, final String content) throws XMLDBException {

		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)
	 */
	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)
	 */
	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)
	 */
	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)
	 */
	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()
	 */
	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)
	 */
	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)
	 */
	public void createCollection(final String collection) throws XMLDBException {
		createCollection(collection, false);
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#createCollection(java.lang.String, boolean)
	 */
	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)
	 */
	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;
	}


	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.
	 *
	 */
	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;
	}

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

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#listChildCollections(java.lang.String)
	 */
	public List<String> listChildCollections(final String collection) throws XMLDBException {
		final Collection col = getCollection(collection);
		return new ArrayList<String>(Arrays.asList(col.listChildCollections()));
	}

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

		return new ArrayList<String>(Arrays.asList(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)
	 */
	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, Arrays.asList(new String[] { "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()
	 */
	public String backup() throws XMLDBException, DatabaseConfigurationException {


		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);

			return backupDir;
		} catch (final EXistException e) {
			throw new XMLDBException(0, "cannot backup", e);
		}
	}


}
