package eu.dnetlib.data.utility.featureextraction.dataprovider;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;

import javax.xml.ws.wsaddressing.W3CEndpointReference;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.core.task.TaskExecutor;

import eu.dnetlib.common.interfaces.nh.IBlackboardMessage.ActionStatus;
import eu.dnetlib.common.resultset.ResultsetProperties;
import eu.dnetlib.common.utils.XMLSerializer;
import eu.dnetlib.common.ws.dataprov.DataProviderException;
import eu.dnetlib.common.ws.dataprov.IDataProviderExt;
import eu.dnetlib.common.ws.dataprov.ResultsResponse;
import eu.dnetlib.data.collective.worker.IWorkerListener;
import eu.dnetlib.data.collective.worker.log.LogProcessor;
import eu.dnetlib.data.collective.worker.log.StatusRecord;
import eu.dnetlib.data.utility.download.DownloadServiceException;
import eu.dnetlib.data.utility.download.IDownloadService;
import eu.dnetlib.data.utility.featureextraction.Feature;
import eu.dnetlib.data.utility.featureextraction.FeatureExtractionException;
import eu.dnetlib.data.utility.featureextraction.constants.FESConstants;
import eu.dnetlib.data.utility.featureextraction.extractor.ExtractorTask;
import eu.dnetlib.data.utility.featureextraction.plugin.IAbstractPlugin;
import eu.dnetlib.data.utility.featureextraction.plugin.PluginFactory;
import eu.dnetlib.enabling.resultset.rmi.ResultSetService;
import eu.dnetlib.enabling.tools.ServiceResolver;
import eu.dnetlib.enabling.tools.blackboard.BlackboardJob;

/**
 * @author jochen
 *
 */
public class FeatureExtractionDataProvider implements IDataProviderExt{

	/** The Constant log. */
	private static final Log log = LogFactory.getLog(FeatureExtractionDataProvider.class);
	
	private TaskExecutor taskExecutor;
	
	private ServiceResolver serviceResolver;

	private Map<String, Feature> featureMap = new LinkedHashMap<String, Feature>();
	
	private PluginFactory pluginFactory;

	/** The data. */
	Map<String, FeatureExtractionDataProviderProperties> data;
	
	Map<String, String> idMap; // resultsetId, bulkDataId

	protected ResultsetProperties resultsetProperties;
	
	protected XMLSerializer<StatusRecord> statusRecordSerializer;
	
	private IDownloadService downloadService;
	
	/**
	 * Instantiates a new simple store data provider.
	 */
	public FeatureExtractionDataProvider() {
		// synchronized data access
		this.data = Collections.synchronizedMap(new HashMap<String, FeatureExtractionDataProviderProperties>());
		
		this.idMap = Collections.synchronizedMap(new HashMap<String, String>());
	}
	
	public void cleanup() {
		log.debug("starting cleanup operations...");
		long currentTime = System.currentTimeMillis();
		synchronized (this.data) {
			Set<String> keySet = this.data.keySet();
			Iterator<String> keysIt = keySet.iterator();
			while (keysIt.hasNext()) {
				String currentKey = keysIt.next();
				FeatureExtractionDataProviderProperties currentProps = this.data.get(currentKey);
				if (currentTime > currentProps.getExpirationTime()) {
					log.debug("removing data prov: " + currentKey);
					keysIt.remove();
				}
			}
		}
		log.debug("cleanup operations finished");
	}
	
	@Override
	public List<String> getBulkData(String bdId, int fromPosition,
			int toPosition) throws DataProviderException {
		
		log.debug("getBulkData(" + bdId + "," + fromPosition + ","
				+ toPosition + ") call received");

		if (bdId == null)
			throw new DataProviderException("BdId cannot be null!");

		if (fromPosition > toPosition)
			throw new DataProviderException(
					"fromPosition is greater than toPosition!");

		FeatureExtractionDataProviderProperties properties = this.data.get(bdId);

		if (properties == null)
			throw new DataProviderException(
					"No DataProviderProperties found for bd_id: " + bdId);
		else if (properties.getException() != null){
			throw new DataProviderException(properties.getException());
		}

		return getExtractsBulkData((FeatureExtractionDataProviderProperties) properties,
				fromPosition, toPosition);
	}
	
	@Override
	public String[] getSimpleBulkData(String bd_id, int fromPosition,
			int toPosition) throws DataProviderException {
		return this.getBulkData(bd_id, fromPosition, toPosition).toArray(new String[0]);
	}
	
	@Override
	public ResultsResponse getNumberOfResults(String bdId)
			throws DataProviderException {
		if (bdId == null)
			throw new DataProviderException("BdId cannot be null!");
		FeatureExtractionDataProviderProperties properties = this.data.get(bdId);
		ResultsResponse result = new ResultsResponse();
		
		if (properties == null) {
			log.debug("invalid bulk data id: " + bdId);
			result.setTotal(0);
			return result;
		} else {
			log.debug("getNumberOfResults(" + bdId + ") call, results: "
					+ properties.getTotalNumberOfResults());
			result.setTotal(properties.getNumberOfCacheResults());
			if (properties.getNumberOfCacheResults() < properties.getTotalNumberOfResults()){
				result.setStatus(IDataProviderExt.STATUS_OPEN);	
			} else{
				result.setStatus(IDataProviderExt.STATUS_CLOSED);
			}

			return result;
		}
	}

	public W3CEndpointReference createResultSet(
			Feature aFeature, W3CEndpointReference aSourceEpr, ResultSetService aResultsetService, IWorkerListener<BlackboardJob> aListener) 
			throws FeatureExtractionException {
		
		if ("fulltext".equals(aFeature.getName())) {
			try {
				aSourceEpr = downloadService.downloadURLsFromXML(aSourceEpr, FESConstants.XPATH_FOR_OBJECT_URI);
			} catch (DownloadServiceException e) {
				throw new FeatureExtractionException("Download Service problem: " + e.getMessage());
			}
		}
		
		SourceDataProvider sourceDataProvider = new SourceDataProviderImpl(aSourceEpr, serviceResolver);
		FeatureExtractionDataProviderProperties properties = new FeatureExtractionDataProviderProperties(this.resultsetProperties.getExpiryTime());
		
		W3CEndpointReference epr = null;
		if ("fulltext".equals(aFeature.getName())) {
			epr = setupResultsetFT(aFeature, aSourceEpr, aResultsetService, properties, sourceDataProvider);
		} else {
			epr = setupResultset(aFeature, aSourceEpr, aResultsetService, properties, sourceDataProvider);
		}
		
		Timer logTimer = new Timer();
		LogProcessor<BlackboardJob> logProcessor = new LogProcessor<BlackboardJob>();
		logProcessor.setWorkerListener(aListener);
		logTimer.scheduleAtFixedRate(logProcessor, 60000, 60000); // start after 60 sec, then send logs all 60 sec.			

		ExtractorTask task = new ExtractorTask();
		IAbstractPlugin plugin = pluginFactory.getPlugin(properties.getFeature().getName() + "_plugin");
		plugin.setProperties(properties);
		plugin.setSourceDataProvider(sourceDataProvider);
		properties.setDao(plugin.getDao());
		task.setPlugin(plugin);
		task.setTaskListener(aListener);
		taskExecutor.execute(task);

		return epr;
	}
	
	/**
	 * Sets the up data provider.
	 * 
	 * @param bulkDataDTO the bulk data dto
	 * @param resultsNumber the results number
	 * 
	 * @return the w3 c endpoint reference
	 * 
	 * @throws DataAccessServiceException the data access service exception
	 * @throws FeatureExtractionException the feature extraction exception
	 */
	public W3CEndpointReference createResultSet(
			Feature aFeature, W3CEndpointReference aSourceEpr, ResultSetService resultsetService ) 
			throws FeatureExtractionException {
		
		if ("fulltext".equals(aFeature.getName())) {
			try {
				aSourceEpr = downloadService.downloadURLsFromXML(aSourceEpr, FESConstants.XPATH_FOR_OBJECT_URI);
			} catch (DownloadServiceException e) {
				throw new FeatureExtractionException("Download Service problem: " + e.getMessage());
			}
		}
		log.debug(aSourceEpr);	
		SourceDataProvider sourceDataProvider = new SourceDataProviderImpl(aSourceEpr, serviceResolver);
		FeatureExtractionDataProviderProperties properties = 
			new FeatureExtractionDataProviderProperties(this.resultsetProperties.getExpiryTime());
		
		W3CEndpointReference epr = null;
		if ("fulltext".equals(aFeature.getName())) {
			epr = setupResultsetFT(aFeature, aSourceEpr, resultsetService, properties, sourceDataProvider);
		} else {
			epr = setupResultset(aFeature, aSourceEpr, resultsetService, properties, sourceDataProvider);
		}
		
		ExtractorTask task = new ExtractorTask();
		IAbstractPlugin plugin = pluginFactory.getPlugin(properties.getFeature().getName() + "_plugin");
		plugin.setProperties(properties);
		plugin.setSourceDataProvider(sourceDataProvider);
		properties.setDao(plugin.getDao());
		task.setPlugin(plugin);
		taskExecutor.execute(task);

		return epr;
	}
	
	private W3CEndpointReference setupResultset(Feature aFeature, W3CEndpointReference aSourceEpr, ResultSetService resultsetService, FeatureExtractionDataProviderProperties properties, SourceDataProvider sourceDataProvider) 
	throws FeatureExtractionException {

		properties.setFeature(aFeature);
		properties.setInputRsEpr(aSourceEpr);
		try {
			
			this.data.put(properties.getId(), properties);
			
			if (sourceDataProvider == null){
				throw new FeatureExtractionException("initalization phase failed");
			}
			properties.setTotalNumberOfResults(sourceDataProvider.getSize());
			
		} catch (Throwable e) {
			throw new FeatureExtractionException("Problem by " +
					"creating BulkData container: " + e.getLocalizedMessage());
		}
		
		W3CEndpointReference epr = resultsetService. createPullRS(
					this.resultsetProperties.getDataProviderServiceAddress(),
				    properties.getId(), 
				    this.resultsetProperties.getInitialPageSize(), 
				    this.resultsetProperties.getExpiryTime(), 
				    "", 
					this.resultsetProperties.getKeepAliveTime(), 
					properties.getTotalNumberOfResults());

		if (epr == null) {
			String errorContent = "Exception occured " +
					"when creating pull result set for " +
					"bdId " + properties.getId();
			log.error(errorContent);
			throw new FeatureExtractionException(errorContent);	
		}
		String rsId = serviceResolver.getResourceIdentifier(epr);
		this.idMap.put(rsId, properties.getId());
		return epr;
	}


	private W3CEndpointReference setupResultsetFT(Feature aFeature, W3CEndpointReference aSourceEpr, 
			ResultSetService resultsetService, FeatureExtractionDataProviderProperties properties, 
			SourceDataProvider sourceDataProvider) throws FeatureExtractionException {

		properties.setFeature(aFeature);
		properties.setInputRsEpr(aSourceEpr);
		properties.setKeepAliveTime(this.resultsetProperties.getKeepAliveTime());
		properties.setTotalNumberOfResults(sourceDataProvider.getSize());
		properties.setMaxPackageSizeForPushRS(this.resultsetProperties.getMaxPackageSizeForPushRS());
		
		try {
			
			this.data.put(properties.getId(), properties);
			
			if (sourceDataProvider == null){
				throw new FeatureExtractionException("initalization phase failed");
			}
			
		} catch (Throwable e) {
			throw new FeatureExtractionException("Problem by " +
					"creating BulkData container: " + e.getLocalizedMessage());
		}
		
		W3CEndpointReference epr = resultsetService.createPullRS(
				this.resultsetProperties.getDataProviderServiceAddress(),
				properties.getId(), 
				this.resultsetProperties.getInitialPageSize(), 
				this.resultsetProperties.getExpiryTime(), 
				"OPEN", 
				this.resultsetProperties.getKeepAliveTime(), null);
		
		if (epr == null) {
			String errorContent = "Exception occured " +
					"when creating pull result set for " +
					"bdId " + properties.getId();
			log.error(errorContent);
			throw new FeatureExtractionException(errorContent);	
		}
		String rsId = serviceResolver.getResourceIdentifier(epr);
		this.idMap.put(rsId, properties.getId());
		return epr;
	}
	
	/**
	 * Returns bulkData for search query.
	 * 
	 * @param properties the properties
	 * @param fromPosition the from position
	 * @param toPosition the to position
	 * 
	 * @return bulkData for search query
	 * 
	 * @throws DataProviderException the data provider exception
	 */
	private List<String> getExtractsBulkData(FeatureExtractionDataProviderProperties properties,
			int fromPosition, int toPosition) throws DataProviderException {

		if (properties.getDao() == null){
			throw new DataProviderException("dao is null for bdId: " + properties.getId());
		}
		
		if (fromPosition > properties.getDao().getNumberOfElements()) {
			log.warn("fromPosition: " + fromPosition
					+ " is greater than the number of current results: "
					+ properties.getDao().getNumberOfElements()
					+ ", returning empty results!");
			return new LinkedList<String>();
		}

		if (toPosition > properties.getDao().getNumberOfElements())
			toPosition = properties.getDao().getNumberOfElements();
		
		log.info("Getting results for the first page: Range: "
				+ "fromPosition: " + fromPosition + ", toPosition: "
				+ toPosition);
		return properties.getDao().getResults(fromPosition, toPosition);
	}

	public String getStatusInformation(String id) throws FeatureExtractionException{
		try {
			if (!this.idMap.containsKey(id)) throw new FeatureExtractionException("unknown id: " + id);
			// TODO status info
			FeatureExtractionDataProviderProperties properties = this.data.get(this.idMap.get(id));
			StatusRecord r = new StatusRecord();
			r.setId(id);
			if (properties.getException() != null){
				r.setStatus(ActionStatus.FAILED);
				r.setError(properties.getException().getLocalizedMessage());
			}else if (properties.getNumberOfCacheResults() < properties.getTotalNumberOfResults()){
				r.setStatus(ActionStatus.ONGOING);
			}else{
				r.setStatus(ActionStatus.DONE);
			}
			return statusRecordSerializer.getAsXml(r);
		} catch (Exception e) {
			throw new FeatureExtractionException(e);
		}
	}
	


	/**
	 * @param featureMap the featureMap to set
	 */
	public void setFeatureMap(Map<String, Feature> featureMap) {
		this.featureMap = featureMap;
	}

	/**
	 * @return the featureMap
	 */
	public Map<String, Feature> getFeatureMap() {
		return featureMap;
	}
	
	public List<Feature> getFeatures() {
		return new LinkedList<Feature>(featureMap.values());
	}

	/**
	 * @param pluginFactory the pluginFactory to set
	 */
	public void setPluginFactory(PluginFactory pluginFactory) {
		this.pluginFactory = pluginFactory;
	}

	/**
	 * @return the pluginFactory
	 */
	public PluginFactory getPluginFactory() {
		return pluginFactory;
	}

	public ResultsetProperties getResultsetProperties() {
		return resultsetProperties;
	}
	
	public void setResultsetProperties(ResultsetProperties resultsetProperties) {
		this.resultsetProperties = resultsetProperties;
	}
	
	/**
	 * @param serviceResolver the serviceResolver to set
	 */
	@Required
	public void setServiceResolver(ServiceResolver serviceResolver) {
		this.serviceResolver = serviceResolver;
	}

	/**
	 * @return the serviceResolver
	 */
	public ServiceResolver getServiceResolver() {
		return serviceResolver;
	}

	/**
	 * @return the taskExecutor
	 */
	public TaskExecutor getTaskExecutor() {
		return taskExecutor;
	}

	/**
	 * @param taskExecutor the taskExecutor to set
	 */
	public void setTaskExecutor(TaskExecutor taskExecutor) {
		this.taskExecutor = taskExecutor;
	}

	public XMLSerializer<StatusRecord> getStatusRecordSerializer() {
		return statusRecordSerializer;
	}

	public void setStatusRecordSerializer(
			XMLSerializer<StatusRecord> statusRecordSerializer) {
		this.statusRecordSerializer = statusRecordSerializer;
	}
	
	/**
	 * @return the downloadService
	 */
	public IDownloadService getDownloadService() {
		return downloadService;
	}
	/**
	 * @param downloadService the downloadService to set
	 */
	public void setDownloadService(IDownloadService downloadService) {
		this.downloadService = downloadService;
	}
}
