package eu.dnetlib.efg1914.commons.dao;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.UUID;

import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;

import org.apache.commons.io.IOUtils;
import org.apache.log4j.Logger;

import eu.dnetlib.efg1914.commons.domain.XMLResource;
import eu.dnetlib.efg1914.commons.store.XMLResourceBinder;
import eu.dnetlib.efg1914.commons.store.XMLStore;
import eu.dnetlib.efg1914.commons.store.XMLStoreException;
import eu.dnetlib.efg1914.commons.store.XQueryGenerator;
import eu.dnetlib.efg1914.commons.utils.CDataXMLSerializer;

/**
 * Implements a {@link GenericDAO}
 * 
 * @author kiatrop
 * 
 * @param <D>
 *            The type of element manages by this DAO
 */
public class GenericXmlDAO<D extends XMLResource> implements GenericDAO<D> {

	private static Logger logger = Logger.getLogger(GenericXmlDAO.class);

	/**
	 * The data source for the elements.
	 */
	private XMLStore store = null;
	private XMLResourceBinder<D> binder = null;
	private XQueryGenerator<D> xQueryGenerator = null;

	private static boolean indexed;

	public GenericXmlDAO(Class<D> resourceClass) throws DAOException {
		try {
			binder = new XMLResourceBinder<D>(resourceClass);
			xQueryGenerator = new XQueryGenerator<D>(resourceClass);

		} catch (JAXBException jaxbe) {
			throw new DAOException("Fail to create GenericXmlDAO of type " + resourceClass, jaxbe);
		}
	}

	public GenericXmlDAO(Class<D> resourceClass, boolean indexed) throws DAOException {
		try {
			binder = new XMLResourceBinder<D>(resourceClass);
			GenericXmlDAO.indexed = indexed;
			xQueryGenerator = new XQueryGenerator<D>(resourceClass, indexed);

		} catch (JAXBException jaxbe) {
			throw new DAOException("Fail to create GenericXmlDAO of type " + resourceClass, jaxbe);
		}
	}

	/**
	 * Saves the given item in a collection.
	 * 
	 * @param xmlResource
	 *            The item to be saved.
	 * @param collectionName
	 *            the collection where to save the item.
	 * @throws XMLStoreException
	 */
	public String save(D entity) throws DAOException {
		logger.debug("Saving new entity...");

		UUID id = null;
		FileOutputStream fos = null;

		try {
			// set creation date
			DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
			Date date = new Date();
			entity.getHeader().setCreationDate(dateFormat.format(date));
			entity.getHeader().setLastModified(dateFormat.format(date));

			// Create uuid
			id = UUID.randomUUID();
			entity.setId(id.toString());

			// Create file
			File file = File.createTempFile(entity.getClass().getSimpleName().toLowerCase() + "-" + id, "");

			// get the xml
			Marshaller jaxbMarshaller = binder.getMarshaler();
			fos = new FileOutputStream(file);
			CDataXMLSerializer cdx = new CDataXMLSerializer(fos);
			jaxbMarshaller.marshal(entity, cdx);

			String xQuery = xQueryGenerator.getSaveString(file, id.toString());
			logger.debug("Executing xquery: " + xQuery);
			store.executeCommand(xQuery);

		} catch (Exception e) {
			logger.error("Failed to save entity in collection.", e);
			throw new DAOException("Failed to save entity in collection.", e);

		} finally {
			if (fos != null) {
				try {
					fos.close();
				} catch (IOException e) {
					logger.error("Unable to close filestream.", e);
					throw new DAOException("Unable to close filestream.", e);
				}
			}
		}
		return id.toString();
	}

	public boolean update(D entity) throws DAOException {
		// set modification date
		DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss ");

		Date date = new Date();
		entity.getHeader().setLastModified(dateFormat.format(date));

		try {
			// get the xml
			Marshaller jaxbMarshaller = binder.getMarshaler();
			jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
			// write xml to StringWriter
			StringWriter sw = new StringWriter();
			jaxbMarshaller.marshal(entity, sw);
			store.executeCommand(xQueryGenerator.getUpdateString(entity.getId(), sw));

		} catch (Exception e) {
			logger.error("Failed to update entity with id " + entity.getId(), e);
			throw new DAOException("Failed to update entity with id " + entity.getId(), e);
		}

		return true;
	}

	public boolean delete(String id) throws DAOException {
		try {

			String command = xQueryGenerator.removeElement(id);
			logger.debug("executing query: " + command);
			store.executeCommand(command);

		} catch (Exception e) {
			logger.error("Failed to delete entity with id " + id, e);
			throw new DAOException("Failed to delete entity with id " + id, e);
		}

		return true;

	}

	@SuppressWarnings("unchecked")
	public D getByName(String name) throws DAOException {
		InputStream inputStream = null;
		try {
			String xQuery = xQueryGenerator.getByNameString(name);
			logger.debug("executing query: " + xQuery);

			String xml = store.getSearchResult(xQuery);
			// logger.debug("got xml: " + xml);
			if (xml != null) {
				inputStream = IOUtils.toInputStream(xml, "UTF-8");
				D entity = (D) binder.getUnmarshaller().unmarshal(inputStream);
				inputStream.close();
				return entity;
			}

		} catch (Exception e) {
			logger.error("Fail to get item with name " + name, e);
			throw new DAOException("Fail to get item with name " + name, e);

		} finally {
			try {
				if (inputStream != null) {
					inputStream.close();
				}
			} catch (IOException e) {
				logger.error("Fail to get item with name " + name, e);
				throw new DAOException("Fail to get item with name " + name, e);
			}
		}

		return null;
	}

	@Deprecated
	@SuppressWarnings("unchecked")
	public D getByIdfromIndex(String id) throws DAOException {
		InputStream inputStream = null;
		try {
			String xQuery = xQueryGenerator.getByIdIndex(id);
			logger.debug("executing query: " + xQuery);

			String xml = store.getSearchResult(xQuery);
			if (xml != null) {
				logger.debug("got xml: " + xml);
				inputStream = IOUtils.toInputStream(xml, "UTF-8");
				D entity = (D) binder.getUnmarshaller().unmarshal(inputStream);
				inputStream.close();
				return entity;
			}

		} catch (Exception e) {
			logger.error("Fail to get item with id " + id, e);
			throw new DAOException("Fail to get item with id " + id, e);

		} finally {
			try {
				if (inputStream != null) {
					inputStream.close();
				}
			} catch (IOException e) {
				logger.error("Fail to get item with id " + id, e);
				throw new DAOException("Fail to get item with id " + id, e);
			}
		}

		return null;
	}

	@SuppressWarnings("unchecked")
	public void createIndex() throws DAOException {

		try {
			String xQuery = xQueryGenerator.createIndex();
			logger.debug("executing query: " + xQuery);

			store.executeCommand(xQuery);

		} catch (Exception e) {
			logger.error("Fail to create Index ", e);
			throw new DAOException("Fail to create Index ", e);

		}

	}

	@SuppressWarnings("unchecked")
	public D getById(String id) throws DAOException {
		InputStream inputStream = null;
		try {
			String xQuery = xQueryGenerator.getByIdString(id);
			logger.debug("executing query: " + xQuery);

			String xml = store.getSearchResult(xQuery);
			if (xml != null) {
				logger.debug("got xml: " + xml);
				inputStream = IOUtils.toInputStream(xml, "UTF-8");
				D entity = (D) binder.getUnmarshaller().unmarshal(inputStream);
				inputStream.close();
				return entity;
			}

		} catch (Exception e) {
			logger.error("Fail to get item with id " + id, e);
			throw new DAOException("Fail to get item with id " + id, e);

		} finally {
			try {
				if (inputStream != null) {
					inputStream.close();
				}
			} catch (IOException e) {
				logger.error("Fail to get item with id " + id, e);
				throw new DAOException("Fail to get item with id " + id, e);
			}
		}

		return null;
	}
    public D getByIdOrAlias(String alias) throws DAOException {
        InputStream inputStream = null;
        try {
            String xQuery = xQueryGenerator.getByAliasString(alias);
            logger.debug("executing query: " + xQuery);

            String xml = store.getSearchResult(xQuery);
            if (xml != null) {
                logger.debug("got xml: " + xml);
                inputStream = IOUtils.toInputStream(xml, "UTF-8");
                D entity = (D) binder.getUnmarshaller().unmarshal(inputStream);
                inputStream.close();
                return entity;
            }

        } catch (Exception e) {
            logger.error("Fail to get item with alias " + alias, e);
            throw new DAOException("Fail to get item with alias " + alias, e);

        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (IOException e) {
                logger.error("Fail to get item with alias " + alias, e);
                throw new DAOException("Fail to get item with alias " + alias, e);
            }
        }

        return null;
    }
	@SuppressWarnings("unchecked")
	public ArrayList<D> getAll() throws DAOException {
		InputStream inputStream = null;
		D entity = null;

		try {
			String xQuery = xQueryGenerator.getAll();
			logger.debug("executing query: " + xQuery);

			ArrayList<D> entities = new ArrayList<D>();
			for (String xml : store.getSearchResults(xQuery)) {
				logger.debug("got xml: " + xml);

				// TODO added logger for last
				// correct entry
				inputStream = IOUtils.toInputStream(xml, "UTF-8");

				entity = ((D) binder.getUnmarshaller().unmarshal(inputStream));
				entities.add(entity);

				inputStream.close();
			}

			return entities;

		} catch (Exception e) {
			logger.error("Fail to get components  ", e);
			throw new DAOException("Fail to get component : " + entity.getId(), e);

		} finally {
			try {
				if (inputStream != null) {
					inputStream.close();
				}
			} catch (IOException e) {
				logger.error("Fail to get components  ", e);
				throw new DAOException("Fail to get components  ", e);
			}
		}
	}
@Override
	public String getAllAlias() throws XMLStoreException {
		String xQuery = xQueryGenerator.getAllAlias();
		logger.debug("executing query: " + xQuery);
		return store.getSearchResult(xQuery);
	}

	@Override
	@SuppressWarnings("unchecked")
	public ArrayList<D> getAllAfterTime(String time) throws DAOException {
		InputStream inputStream = null;
		D entity = null;

		try {
			String xQuery = xQueryGenerator.getAfterTime(time);
			logger.debug("executing query: " + xQuery);

			ArrayList<D> entities = new ArrayList<D>();
			for (String xml : store.getSearchResults(xQuery)) {
				logger.debug("got xml: " + xml);

				// TODO added logger for last
				// correct entry
				inputStream = IOUtils.toInputStream(xml, "UTF-8");

				entity = ((D) binder.getUnmarshaller().unmarshal(inputStream));
				entities.add(entity);

				inputStream.close();
			}

			return entities;

		} catch (Exception e) {
			logger.error("Fail to get components  ", e);
			throw new DAOException("Fail to get component : " + entity.getId(), e);

		} finally {
			try {
				if (inputStream != null) {
					inputStream.close();
				}
			} catch (IOException e) {
				logger.error("Fail to get components  ", e);
				throw new DAOException("Fail to get components  ", e);
			}
		}
	}

	@SuppressWarnings("unchecked")
	public List<D> search(String query) throws DAOException {
		logger.debug("Execulting query: " + query);
		List<D> results = new ArrayList<D>();

		try {
			for (String xml : store.getSearchResults(query)) {
				logger.debug("got xml: " + xml);
				results.add((D) binder.getUnmarshaller().unmarshal(IOUtils.toInputStream(xml, "UTF-8")));
			}

		} catch (Exception e) {
			logger.error("Fail to get results for query: " + query, e);
			throw new DAOException("Fail to execute query: " + query, e);

		}
		return results;
	}

	@SuppressWarnings("unchecked")
	public List<D> search(String query, LinkedHashMap<String, String> bindings) throws DAOException {
		logger.debug("Execulting query " + query + " with bindings for " + bindings.keySet());

		List<D> results = new ArrayList<D>();
		try {
			for (String xml : store.getSearchResults(query, bindings)) {
				results.add((D) binder.getUnmarshaller().unmarshal(IOUtils.toInputStream(xml, "UTF-8")));
			}

		} catch (Exception e) {
			logger.error("Fail to get results for query  " + query + " with bindings for " + bindings.keySet(), e);
			throw new DAOException("Fail to get results for query  " + query + " with bindings for " + bindings.keySet(), e);

		}

		return results;
	}

	// TODO check this
	@SuppressWarnings("unchecked")
	@Override
	public ArrayList<D> getByUser(String userId) throws DAOException {
		InputStream inputStream = null;
		D entity = null;

		try {
			String xQuery = xQueryGenerator.getByUser(userId);
			logger.debug("executing query: " + xQuery);

			ArrayList<D> entities = new ArrayList<D>();
			for (String xml : store.getSearchResults(xQuery)) {
				logger.debug("got xml: " + xml);
				inputStream = IOUtils.toInputStream(xml, "UTF-8");

				entity = ((D) binder.getUnmarshaller().unmarshal(inputStream));
				entities.add(entity);

				inputStream.close();
			}

			return entities;

		} catch (Exception e) {
			logger.error("Fail to get components  ", e);
			throw new DAOException("Fail to get component : " + entity.getId(), e);

		} finally {
			try {
				if (inputStream != null) {
					inputStream.close();
				}
			} catch (IOException e) {
				logger.error("Fail to get components  ", e);
				throw new DAOException("Fail to get components  ", e);
			}
		}
	}

	// TODO check this
	@SuppressWarnings("unchecked")
	@Override
	public ArrayList<D> getByOtherUsers(String userId) throws DAOException {
		InputStream inputStream = null;
		D entity = null;

		try {
			String xQuery = xQueryGenerator.getComponentsExceptForUsers(userId);
			logger.debug("executing query: " + xQuery);

			ArrayList<D> entities = new ArrayList<D>();
			for (String xml : store.getSearchResults(xQuery)) {
				logger.debug("got xml : " + xml);
				inputStream = IOUtils.toInputStream(xml, "UTF-8");

				entity = ((D) binder.getUnmarshaller().unmarshal(inputStream));
				entities.add(entity);

				inputStream.close();
			}

			return entities;

		} catch (Exception e) {
			logger.error("Fail to get components  ", e);
			throw new DAOException("Fail to get component : " + entity.getId(), e);

		} finally {
			try {
				if (inputStream != null) {
					inputStream.close();
				}
			} catch (IOException e) {
				logger.error("Fail to get components  ", e);
				throw new DAOException("Fail to get components  ", e);
			}
		}
	}

	public XMLResourceBinder<D> getBinder() {
		return binder;
	}

	public void setBinder(XMLResourceBinder<D> binder) {
		this.binder = binder;
	}

	public XQueryGenerator<D> getxQueryGenerator() {
		return xQueryGenerator;
	}

	public void setxQueryGenerator(XQueryGenerator<D> xQueryGenerator) {
		this.xQueryGenerator = xQueryGenerator;
	}

	public void setStore(XMLStore store) {
		this.store = store;
	}

	public XMLStore getStore() {
		return store;
	}

	public void setIndexed(boolean isIndexed) {
		GenericXmlDAO.indexed = isIndexed;
	}

	public boolean isIndexed() {
		return GenericXmlDAO.indexed;
	}
}
