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.*;
import java.util.Map.Entry;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
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.EXistResource;
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.*;
import org.xmldb.api.base.Collection;
import org.xmldb.api.modules.CollectionManagementService;
import org.xmldb.api.modules.XPathQueryService;

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

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

	private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

	private final Lock readLock = rwl.readLock();

	private final Lock writeLock = rwl.writeLock();

	/**
	 * 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 {
		readLock.lock();
		try {
			if (!collection.startsWith("/db"))
				throw new XMLDBException(0, "collection path should begin with /db");
			return database.getCollection("exist://" + collection, "admin", "");
		}finally {
			readLock.unlock();
		}
	}

	/**
	 * {@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 {
		writeLock.lock();
		try {
			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);

			((EXistResource) res).freeResources();
			col.close();
		}finally {
			writeLock.unlock();
		}
	}

	/**
	 * {@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 {
		writeLock.lock();
		try {
			final Collection col = getCollection(collection);

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

			col.removeResource(res);
			col.close();
			return true;
		} finally {
			writeLock.unlock();
		}
	}

	/**
	 * {@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 {
		writeLock.lock();
		try{
			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);
			((EXistResource) res).freeResources();
			col.close();
		} finally {
			writeLock.unlock();
		}

	}

	/**
	 * {@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 {
		readLock.lock();
		try {
			Resource res = null;
			final Collection coll = getCollection(collection);
			try {
				if (coll == null)
					return null;
				res = coll.getResource(name);
				if (res != null)
					return (String) res.getContent();
				return null;
			} finally {
				if (res != null)
					((EXistResource) res).freeResources();
				coll.close();
			}
		}finally {
			readLock.unlock();
		}
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#xquery(java.lang.String)
	 */
	@Override
	public Iterator<String> xquery(final String query) throws XMLDBException {
		readLock.lock();
		try {
			final ResourceSet result = getQueryService().query(query);
			if (result == null)
				return null;
			final ResourceIterator iterator = result.getIterator();
			return new Iterator<String>() {
				@Override
				public boolean hasNext() {
					try {
						return iterator.hasMoreResources();
					} catch (XMLDBException e) {
						throw new RuntimeException("Error while getting next element", e);
					}
				}

				@Override
				public String next() {
					Resource res = null;
					try {
						res = iterator.nextResource();
						return (String) res.getContent();
					} catch (XMLDBException e) {
						throw new RuntimeException("Error while getting next element", e);
					} finally {
						if (res != null)
							try {
								((EXistResource) res).freeResources();
							} catch (XMLDBException e) {
								log.error("error on free resource");
							}
					}
				}
			};
		} finally {
			readLock.unlock();
		}
	}


	@Override
	public void xupdate(final String query) throws XMLDBException {
		writeLock.lock();
		try{
			getQueryService().query(query);
		} finally {
			writeLock.unlock();
		}


	}


	/**
	 * {@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 {
		Collection col = null;
		try{
			col = getCollection(collection);
			return col != null;
		} finally {
			if (col!=null)
				col.close();
		}

	}

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



	private 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 {
		writeLock.lock();
		try {
			getColman().removeCollection(collection);
		}finally {
			writeLock.unlock();
		}
	}

	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() {
		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 {
		readLock.lock();
		try {
			final Collection col = getCollection(collection);
			if (col == null)
				return new ArrayList<>();
			return Arrays.asList(col.listChildCollections());
		} finally {
			readLock.unlock();
		}
	}

	/**
	 * {@inheritDoc}
	 *
	 * @see eu.dnetlib.xml.database.XMLDatabase#list(java.lang.String)
	 */
	@Override
	public List<String> list(final String collection) throws XMLDBException {
		readLock.lock();
		try {
			final Collection col = getCollection(collection);
			if(col == null)
				return new ArrayList();
			return Arrays.asList(col.listResources());
		} finally {
			readLock.unlock();
		}
	}

	/**
	 * 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/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, Arrays.asList("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...");
		readLock.lock();
		try {
			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);
		}
		finally {
			readLock.unlock();
		}
	}


	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 {
		readLock.lock();
		logFile.write("COLLECTION: " + coll + "\n");
		log.info("Backup of collection " + coll);
		try {
			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);
			}
		}finally {
			readLock.unlock();
		}

	}
}
