package it.mice.voila.runtime.service;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pojomatic.diff.Differences;
import org.pojomatic.diff.NoDifferences;
import org.springframework.context.support.DefaultMessageSourceResolvable;

import it.mice.voila.runtime.dao.Dao;
import it.mice.voila.runtime.entity.Entity;
import it.mice.voila.runtime.entity.EntityDiffAdapter;
import it.mice.voila.runtime.exception.BusinessException;
import it.mice.voila.runtime.util.UserMessage;
import it.mice.voila.runtime.util.UserMessages;

public abstract class GenericEntityService<T extends Entity> implements EntityService<T> {
	/**
	 * Default logger.
	 */
	private static Log logger = LogFactory.getLog(GenericEntityService.class);
	/**
	 * Data access object per l'accesso alla persistenza relativa all'entità: Entity.
	 */
	private Dao<T> dao;

	public Dao<T> getDao() {
		return dao;
	}

	public void setDao(Dao<T> dao) {
		this.dao = dao;
	}

	/**
	 * Fornisce l'elenco di tutti gli elementi di tipo Entity presenti nel DB in una
	 * collection.
	 * 
	 * @return una collection di Entity presenti nel DB
	 */
	public Collection<T> listAll() {
		return getDao().findAll();
	}
	
	/**
	 * Fornisce l'elenco di tutti gli elementi di tipo Entity presenti nel DB in una
	 * collection.
	 * 
	 * @return una collection di Entity presenti nel DB
	 */
	public Collection<T> listAll(Boolean cacheable) {
		return getDao().findAll(cacheable);
	}
	
	/**
	 * Fornisce l'elenco di tutti gli elementi di tipo Entity presenti nel DB in una
	 * collection.
	 * 
	 * @return una collection di Entity presenti nel DB
	 */
	public Collection<T> listAll(Boolean cacheable, Boolean useAcl) {
		return getDao().findAll(cacheable, useAcl);
	}

	/**
	 * Fornisce l'elenco di tutti gli elementi di tipo Entity presenti nel DB
	 * corrispondendi ai criteri di ricerca specificati. Gli entity From e To, vengono
	 * utilizzati come criterio di ricerca seguendo queste regole:
	 * <ul>
	 * <li>Per gli attributi testuali viene sempre osservata la sola presenza del valore
	 * nell'entity From e, se diverso da null, verrà utilizzato come criterio
	 * "contiene (LIKE)";</li>
	 * <li>Per gli attributi numerici e temporali:
	 * <ol>
	 * <li>se valorizzato solo l'attributo From verrà utilizzato come criterio
	 * "maggiore o uguale a";</li>
	 * <li>se valorizzato solo l'attributo To verrà utilizzato come criterio
	 * "minore o uguale a";</li>
	 * <li>se valorizzati entrambi gli attributi From e To verrà utilizzato come criterio
	 * "compreso tra";</li>
	 * </ol>
	 * </ul>
	 * 
	 * @param theEntityFrom
	 *            Entity {@link Entity} utilizzato come filtro (From).
	 * @param theEntityTo
	 *            Entity {@link Entity} utilizzato come filtro (To).
	 * @return una Collection di Entity. Attenzione, la Collection avrà size 0 se non
	 *         viene trovata alcuna occorrenza.
	 */
	public Collection<T> search(T theEntityFrom, T theEntityTo) {
		return getDao().findByFilter(theEntityFrom, theEntityTo);
	}

	/**
	 * Questo metodo estende il servizio offerto dal metodo searchEntity in quanto
	 * determina i criteri di ricerca in base ad un criteria map specificato in input. Il
	 * criteria type utilizzato per una determinata proprietà verrà ricercato nel criteria
	 * map secondo la logica dei bean properties (es. customerId,
	 * theEntityCategory.categoryDescription, ecc...). Si vedano i tipi definiti nella
	 * classe CriteriaType per un elenco completo dei criteria definiti.
	 * 
	 * @see #searchEntity(Entity, Entity)
	 * @param theEntityFrom
	 *            Entity {@link Entity} utilizzato come filtro (From).
	 * @param theEntityTo
	 *            Entity {@link Entity} utilizzato come filtro (To).
	 * @param criteriaMap
	 *            Map di search criteria.
	 * @return una Collection di Entity. Attenzione, la Collection avrà size 0 se non
	 *         viene trovata alcuna occorrenza.
	 */
	public Collection<T> searchWithCriteria(T theEntityFrom,
			T theEntityTo, Map<String, Object> criteriaMap) {
		return getDao().findByFilter(theEntityFrom, theEntityTo, criteriaMap);
	}

	/**
	 * Questo metodo estende il servizio offerto dal metodo searchEntity, in quanto
	 * consente la paginazione dei dati estratti tramite l'indicazione del primo record e
	 * del numero di records da estrarre.
	 * 
	 * @see #searchEntity(Entity, Entity)
	 * @param theEntityFrom
	 *            Entity {@link Entity} utilizzato come filtro (From).
	 * @param theEntityTo
	 *            Entity {@link Entity} utilizzato come filtro (To).
	 * @param firstResult
	 *            Primo record da estrarre.
	 * @param maxResult
	 *            Numero totale di records da estrarre.
	 * @return una Collection di Entity. Attenzione, la Collection avrà size 0 se non
	 *         viene trovata alcuna occorrenza.
	 */
	public Collection<T> searchWithPage(T theEntityFrom,
			T theEntityTo, int firstResult, int maxResult) {
		return getDao().findByFilter(theEntityFrom, theEntityTo, firstResult,	maxResult);
	}

	/**
	 * Questo metodo aggrega i servizi offerti dai metodi searchEntityWithCriteria e
	 * searchEntityWithPage in quanto consente sia la paginazione dei dati estratti, sia
	 * la possibilità di specificare criteri di ricerca specifici.
	 * 
	 * @see #searchEntityWithCriteria(Entity, Entity, Map)
	 * @see #searchEntityWithPage(Entity, Entity, int, int)
	 * @param theEntityFrom
	 *            Entity {@link Entity} utilizzato come filtro (From).
	 * @param theEntityTo
	 *            Entity {@link Entity} utilizzato come filtro (To).
	 * @param criteriaMap
	 *            Map di search criteria.
	 * @param firstResult
	 *            Primo record da estrarre.
	 * @param maxResult
	 *            Numero totale di records da estrarre.
	 * @return una Collection di Entity. Attenzione, la Collection avrà size 0 se non
	 *         viene trovata alcuna occorrenza.
	 */
	public Collection<T> searchWithPageAndCriteria(T theEntityFrom, T theEntityTo, Map<String, Object> criteriaMap, int firstResult, int maxResult) {
		return getDao().findByFilter(theEntityFrom, theEntityTo, criteriaMap,	firstResult, maxResult);
	}

	/**
	 * Questo metodo estende il servizio offerto dal metodo
	 * searchEntityWithPageAndCriteria, in quanto aggiunge anche la possibilità di
	 * fornire una clausola order by. Inoltre il metodo si presta ad utilizzi con liste
	 * paginate di dati in quanto restituisce un Map contenente due elementi:
	 * <ol>
	 * <li>"list" - contiene la collection dei soli elementi estratti dal DB in base ai
	 * criteri di ricerca specificati ed alla paginazione;</li>
	 * <li>"size" - riporta il numero totale di occorrenze che sarebbero state estratte
	 * senza l'utilizzo della paginazione.</li>
	 * </ol>
	 * 
	 * @see #searchEntityWithPageAndCriteria(Entity, Entity, Map, int, int)
	 * @param theEntityFrom
	 *            Entity {@link Entity} utilizzato come filtro (From).
	 * @param theEntityTo
	 *            Entity {@link Entity} utilizzato come filtro (To).
	 * @param criteriaMap
	 *            Map di search criteria.
	 * @param firstResult
	 *            Primo record da estrarre.
	 * @param maxResult
	 *            Numero totale di records da estrarre.
	 * @param orderBy
	 *            clausola order by.
	 * @return una HashMap che riporta la collection di elementi estratti per la pagina
	 *         corrente richiesta, più il conteggio totale dei records che rispondono ai
	 *         criteri di ricerca specificati, senza tener conto della paginazione.
	 */
	public Map<String, Object> searchWithCriteriaPagingAndSort(
			T theEntityFrom, T theEntityTo,
			Map<String, Object> criteriaMap, int firstResult, int maxResult,
			String orderBy) {
		
		if (criteriaMap == null) {
			criteriaMap = new HashMap<String, Object>();
		}
		java.util.Map<String, Object> retMap = new HashMap<String, Object>();
		Collection<T> list = getDao().findByFilter(theEntityFrom, theEntityTo, criteriaMap, firstResult, maxResult, orderBy);
		retMap.put("list", list);
		int totRecords = 0;
		if (maxResult > 0 && (firstResult > 0 || list.size() == maxResult)) {
			totRecords = countWithCriteria(theEntityFrom, theEntityTo, criteriaMap);
		} else {
			totRecords = list.size();
		}
		retMap.put("size", new Integer(totRecords));
		return retMap;
	}

	/**
	 * Questo metodo deve restituisce il conteggio dei records qualificati in base ai
	 * criteri di ricerca specificati.
	 * 
	 * @param theEntityFrom
	 *            Entity {@link Entity} utilizzato come filtro (From).
	 * @param theEntityTo
	 *            Entity {@link Entity} utilizzato come filtro (To).
	 * @return Numero totale di records trovati.
	 */
	public int count(T theEntityFrom, T theEntityTo) {
		return getDao().countByFilter(theEntityFrom, theEntityTo);
	}

	/**
	 * Questo metodo estende il servizio offerto dal metodo countEntity, in quanto
	 * determina i criteri di ricerca e conteggio in base ad un criteria map specificato
	 * in input. Il criteria type da utilizzare per una determinata proprietà verrà
	 * ricercato nel criteria map secondo la logica dei bean properties (es. customerId,
	 * theEntityCategory.categoryDescription, ecc...). Si vedano i tipi definiti nella
	 * classe CriteriaType per un elenco completo dei criteria definiti.
	 * 
	 * @see #countEntity(Entity, Entity)
	 * @param theEntityFrom
	 *            Entity {@link Entity} utilizzato come filtro (From).
	 * @param theEntityTo
	 *            Entity {@link Entity} utilizzato come filtro (To).
	 * @param criteriaMap
	 *            Map di search criteria.
	 * @return Numero totale di records trovati.
	 */
	public int countWithCriteria(T theEntityFrom, T theEntityTo, Map<String, Object> criteriaMap) {
		return getDao().countByFilter(theEntityFrom, theEntityTo, criteriaMap);
	}

	/**
	 * Questo metodo costruisce un nuovo Entity.
	 * 
	 * @return Un oggetto Entity inizializzato.
	 */
	public abstract T newEntity();

	/**
	 * Questo metodo carica e restituisce i dati di dettaglio di un Entity accedendo per
	 * chiave primaria.<br>
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} valorizzato della sola chiave primaria.
	 *            <p>
	 * @return il riferimento all'oggetto fornito in input, oppure null se non trovato.
	 */
	public T findByObjectKey(Object objectKey) {
		T theEntity = newEntity();
		theEntity.setObjectKey(objectKey.toString());
		return detail(theEntity);
	}

	/**
	 * Questo metodo carica e restituisce i dati di dettaglio di un Entity accedendo per
	 * chiave primaria.<br>
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} valorizzato della sola chiave primaria.
	 *            <p>
	 * @return il riferimento all'oggetto fornito in input, oppure null se non trovato.
	 */
	public T detail(T theEntity) {
		return detail(theEntity, true);
	}

	/**
	 * Questo metodo carica e restituisce i dati di dettaglio di un Entity accedendo per
	 * chiave primaria.<br>
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} valorizzato della sola chiave primaria.
	 *            <p>
	 * @return il riferimento all'oggetto fornito in input, oppure null se non trovato.
	 */
	public T detail(T theEntity, boolean useAcl) {
		return getDao().findByPrimaryKey(theEntity, useAcl);
	}

	/**
	 * Questo metodo carica e restituisce i dati di dettaglio di un Entity accedendo per
	 * chiave primaria.<br>
	 * A differenza del metodo findByPrimaryKey, esso evita la registrazione nella hibernate cache dell'oggetto
	 * appena letto, consentendo il suo utilizzo per dei check senza per questo incorrere nella noiosissima
	 * eccezione NonUniqueObjectException di hibernate.
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} valorizzato della sola chiave primaria.
	 *            <p>
	 * @return il riferimento all'oggetto fornito in input, oppure null se non trovato.
	 */
	public T detailNoCache(T theEntity) {
		return getDao().findByPrimaryKeyNoCache(theEntity);
	}

	/**
	 * Questo metodo controlla l'esistenza di un Entity accedendo per chiave primaria.
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} valorizzato della sola chiave primaria.
	 *            <p>
	 * @return <code>true</code> se l'oggetto è stato trovato;<br>
	 *         <code>false</code> se l'oggetto non è stato trovato.
	 */
	public boolean exist(T theEntity) {
		return getDao().existByPrimaryKey(theEntity);
	}

	/**
	 * Questo metodo deve effettua l'inserimento di un Entity in base dati.
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} completamente valorizzato.
	 * @return l'Entity {@link Entity} inserita in stato caricato.
	 */
	public T insert(T theEntity) {
		return insert(theEntity, null);
	}

	/**
	 * Questo metodo deve effettua l'inserimento di un Entity in base dati.
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} completamente valorizzato.
	 * @param userId
	 *            oggetto user.
	 * @return l'Entity {@link Entity} inserita in stato caricato.
	 */
	public T insert(T theEntity, String userId) {
		return insert(theEntity, userId, null);
	}

	/**
	 * Questo metodo deve effettua l'inserimento di un Entity in base dati.
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} completamente valorizzato.
	 * @param userId
	 *            oggetto user.
	 * @return l'Entity {@link Entity} inserita in stato caricato.
	 */
	public T insert(T theEntity, String userId, EntityDiffAdapter diffAdapter) {
		return insert(theEntity, userId, diffAdapter, true, true);
	}

	/**
	 * Questo metodo deve effettua l'inserimento di un Entity in base dati.
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} completamente valorizzato.
	 * @param userId
	 *            oggetto user.
	 * @return l'Entity {@link Entity} inserita in stato caricato.
	 */
	public T insert(T theEntity, String userId, EntityDiffAdapter diffAdapter, boolean checkParentRelation, boolean checkDuplicate) {
		if (checkParentRelation) {
			checkParentRelation(theEntity);
		}
		if (checkDuplicate) {
			checkDuplicate(theEntity);
		}

		Differences diffs = NoDifferences.getInstance();
		if (diffAdapter != null) {
			diffs = computeDifferences(theEntity);
		}
		
		T result = getDao().insert(theEntity);
		
		if (!diffs.areEqual()) {
			diffAdapter.manageDiff(diffs);
		}
		return result;
	}

	/**
	 * Questo metodo effettua l'aggiornamento di un Entity in base dati.
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} completamente valorizzato.
	 * @return <code>true</code> se l'oggetto è stato aggiornato correttamente;<br>
	 *         <code>false</code> se l'oggetto non è stato aggiornato correttamente.
	 */
	public T update(T theEntity) {
		return update(theEntity, null);
	}

	/**
	 * Questo metodo effettua l'aggiornamento di un Entity in base dati.
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} completamente valorizzato.
	 * @param userId
	 *            oggetto user.
	 * @return <code>true</code> se l'oggetto è stato aggiornato correttamente;<br>
	 *         <code>false</code> se l'oggetto non è stato aggiornato correttamente.
	 */
	public T update(T theEntity, String userId) {
		return update(theEntity, userId, null);
	}

	/**
	 * Questo metodo effettua l'aggiornamento di un Entity in base dati.
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} completamente valorizzato.
	 * @param userId
	 *            oggetto user.
	 * @return <code>true</code> se l'oggetto è stato aggiornato correttamente;<br>
	 *         <code>false</code> se l'oggetto non è stato aggiornato correttamente.
	 */
	public T update(T theEntity, String userId, EntityDiffAdapter diffAdapter) {
		return update(theEntity, userId, diffAdapter, true);
	}

	/**
	 * Questo metodo effettua l'aggiornamento di un Entity in base dati.
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} completamente valorizzato.
	 * @param userId
	 *            oggetto user.
	 * @return <code>true</code> se l'oggetto è stato aggiornato correttamente;<br>
	 *         <code>false</code> se l'oggetto non è stato aggiornato correttamente.
	 */
	public T update(T theEntity, String userId, EntityDiffAdapter diffAdapter, boolean checkParentRelation) {
		if (checkParentRelation) {
			checkParentRelation(theEntity);
		}

		Differences diffs = NoDifferences.getInstance();
		if (diffAdapter != null) {
			diffs = computeDifferences(theEntity);
		}
		
		T result = getDao().update(theEntity);

		if (!diffs.areEqual()) {
			diffAdapter.manageDiff(diffs);
		}
		return result;
	}

	/**
	 * Questo metodo effettua la cancellazione di un Entity in base dati
	 * 
	 * @param theEntity
	 *            Entity {@link Entity} valorizzato della sola chiave primaria.
	 *            <p>
	 * @return <code>true</code> se l'oggetto è stato cancellato correttamente;<br>
	 *         <code>false</code> se l'oggetto non è stato cancellato correttamente.
	 */
	public boolean delete(T theEntity) {
		return getDao().delete(theEntity);
	}

	/**
	 * Metodo che controlla l'esistenza di tutte le entita' padre relazione con l'elemento
	 * da inserire/aggiornare in base dati. Esso va specializzato da ogni singolo service
	 * definito.
	 * 
	 * @param entity
	 *            implementazione dell'entity di cui controllare i riferimenti a padri.
	 */
	public void checkParentRelation(T entity) {
		// Nothing to check since this is a master entity.
	}


	public void ensureExist(T theEntity, UserMessages userMessages, String entityName) {
		if (theEntity != null && !theEntity.isPkNull()) {
			if (!exist(theEntity)) {
				userMessages.add(new UserMessage("message.genericNotFound", entityName, theEntity.getObjectTitle()));
			}
		}
	}

	/**
	 * Questo metodo controlla l'esistenza in base dati di Entity fornito in input,
	 * sollevando una BusinessException in caso affermativo.
	 * 
	 * @param theEntity
	 *            Entity di cui controllarne l'esistenza.
	 * @throws BusinessException
	 *             nel caso in cui Entity esiste già in base dati.
	 */
	public void checkDuplicate(T theEntity) {
		if (theEntity.isPkNotNull() && exist(theEntity)) {
			UserMessages userMessages = new UserMessages();
			
			UserMessage msg = new UserMessage("message.genericDupKey", new DefaultMessageSourceResolvable("label.the" + getEntityName(theEntity)), theEntity.getObjectTitle());
			userMessages.add(msg);
			throw new BusinessException(userMessages);
		}
	}

	/* (non-Javadoc)
	 * @see it.mice.voila.runtime.service.EntityService#firstElement(java.util.Collection)
	 */
	public T firstElement(Collection<T> list) {
		if (list == null || list.size() == 0) {
			return null;
		}
		return list.iterator().next();
	}

	/* (non-Javadoc)
	 * @see it.mice.voila.runtime.service.EntityService#firstElement(java.util.Map)
	 */
	public T firstElement(Map<String, Object> map) {
		if (map == null || map.get("list") == null) {
			return null;
		}
		Collection<T> list = (Collection<T>)map.get("list");
		return firstElement(list);
	}

	private String getEntityName(T theEntity) {
		return theEntity.getClass().getSimpleName();
	}

	public Differences computeDifferences(T theEntity) {
		if ((theEntity.getEntityStateDump() != null && theEntity.isChangedEntity()) || (theEntity.getEntityStateDump() == null && theEntity.isNewEntity())) {
			T theOldEntity = newEntity();
			if (theEntity.getEntityStateDump() != null) {
				theOldEntity.setObjectDump(theEntity.getEntityStateDump());
			}
			return theEntity.diff(theOldEntity);
		}
		return NoDifferences.getInstance();
	}
}
