/**
 * Copyright 2008-2009 DRIVER PROJECT (ICM UW)
 * Original author: Marek Horst
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package eu.dnetlib.data.index.ws.dataprov;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import org.apache.log4j.Logger;
import org.springframework.util.StringUtils;

import edu.emory.mathcs.backport.java.util.Arrays;
import eu.dnetlib.resultset.api.ICleanable;

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.index.ws.yadda.DriverQuery;
import eu.dnetlib.data.index.ws.yadda.SearchModuleFacade;

import pl.edu.icm.yadda.service.search.SearchException;
import pl.edu.icm.yadda.service.search.searching.SearchResult;
import pl.edu.icm.yadda.service.search.searching.SearchResults;

/**
 * Simple index data provider.
 * @author mhorst
 *
 */
public class SimpleIndexDataProvider implements IIndexDataProvider, ICleanable {

	protected static final Logger log = Logger.getLogger(SimpleIndexDataProvider.class);
	
	public static final int DEFAULT_EXPIRY_TIME = 86400;
	
	Random rand;
	
	Map<String, DataProviderProperties> data;
	
	/**
	 * Default data provider expiry time (in seconds).
	 */
	private int defaultExpiryTime; 
	
	/**
	 * Yadda search module facade.
	 */
	private SearchModuleFacade searchModuleFacade;
	
	public SimpleIndexDataProvider() {
		defaultExpiryTime = SimpleIndexDataProvider.DEFAULT_EXPIRY_TIME;
		rand = new Random();
//		synchronized data access
		data = Collections.synchronizedMap(
				new HashMap<String, DataProviderProperties>());
	}
	

	/* (non-Javadoc)
	 * @see eu.dnetlib.data.index.ws.dataprov.IIndexDataProvider#createBulkData(eu.dnetlib.data.index.ws.dataprov.CreateBulkDataDTO)
	 */
	public String createBulkData(CreateBulkDataDTO bulkDataDTO) 
		throws DataProviderException {
		String bdId = generateBulkDataId();
		if (bulkDataDTO.getType()==CreateBulkDataDTO.Type.SEARCH) {
			CreateSearchBulkDataDTO searchBulkDataDTO = (CreateSearchBulkDataDTO) bulkDataDTO; 
			DataProviderSearchProperties properties = new DataProviderSearchProperties(
					defaultExpiryTime, searchBulkDataDTO.getCache());
			properties.setIndexIds(searchBulkDataDTO.getIndexIds());
			properties.setCqlQuery(searchBulkDataDTO.getCqlQuery());
			properties.setSruClauses(searchBulkDataDTO.getSruClauses());
			properties.setMdFormatId(searchBulkDataDTO.getMdFormatId());
			properties.setLayoutId(searchBulkDataDTO.getLayoutId());
			if (properties!=null)
				data.put(bdId, properties);
			else
				throw new DataProviderException("DataProviderSearchProperties cannot be null!");
			return bdId;
		} else if (bulkDataDTO.getType()==CreateBulkDataDTO.Type.BROWSE) {
			CreateBrowseBulkDataDTO browseBulkDataDTO = (CreateBrowseBulkDataDTO) bulkDataDTO; 
			DataProviderBrowseProperties properties = new DataProviderBrowseProperties(
					defaultExpiryTime, browseBulkDataDTO.getData());
			if (properties!=null)
				data.put(bdId, properties);
			else
				throw new DataProviderException("DataProviderSearchProperties cannot be null!");
			return bdId;
			
		} else
			throw new DataProviderException("Unsupported CreateBulkDataDTO type!");
	}

	/* (non-Javadoc)
	 * @see eu.dnetlib.data.index.ws.dataprov.IDataProvider#getBulkData(java.lang.String, int, int)
	 */
	@SuppressWarnings("unchecked")
	public List<String> getBulkData(String bd_id, int fromPosition,
			int toPosition) throws DataProviderException {
		return Arrays.asList(getSimpleBulkData(bd_id, fromPosition, toPosition));
	}
	
	/* (non-Javadoc)
	 * @see eu.dnetlib.data.index.ws.dataprov.IDataProvider#getSimpleBulkData(java.lang.String, int, int)
	 */
	public String[] getSimpleBulkData(String bd_id, int fromPosition,
			int toPosition) throws DataProviderException {
		log.debug("getBulkData("+bd_id+","+fromPosition+","+toPosition+") call received");
		if (bd_id==null)
			throw new DataProviderException("BdId cannot be null!");
		if (fromPosition>toPosition)
			throw new DataProviderException("fromPosition is greater than toPosition!");
		DataProviderProperties properties = data.get(bd_id);
		if (properties==null)
			throw new DataProviderException("No DataProviderProperties found for bd_id: "+bd_id);
		if (properties.getType() == DataProviderProperties.Type.SEARCH) {
			return getSearchBulkData((DataProviderSearchProperties) properties, 
					fromPosition, toPosition);
		} else if (properties.getType() == DataProviderProperties.Type.BROWSE) {
			return getBrowseBulkData((DataProviderBrowseProperties) properties, 
					fromPosition, toPosition);
		} else throw new DataProviderException("Unsupported DataProviderProperties type!");
			
	}
	
	/**
	 * Returns bulkData for search query.
	 * @param properties
	 * @param fromPosition
	 * @param toPosition
	 * @return bulkData for search query
	 * @throws DataProviderException
	 */
	private String[] getSearchBulkData(DataProviderSearchProperties properties, 
			int fromPosition, int toPosition) throws DataProviderException {
		if (fromPosition > properties.getNumberOfResults()) {
			log.debug("fromPosition: "+ fromPosition + 
					" is greagter than number of results: "+properties.getNumberOfResults() +
					", returning empty results!");
			return new String[0];
		}
		if (toPosition > properties.getNumberOfResults())
			toPosition = properties.getNumberOfResults();
		SearchResults currentSearchResults;
		if (fromPosition==1) {
//			getting values stored in cache
			if (properties.getCache()!=null && 
					properties.getCache().getFirst()==0 &&
					properties.getCache().getSize() == (toPosition-fromPosition+1)) {
					log.info("Getting results from cache for range: " +
							"fromPosition: "+fromPosition+", toPosition: "+toPosition);
					currentSearchResults = properties.getCache();
					properties.setCache(null);
				
			} else {
				log.info("Can't get results from cache! Range: " +
						"fromPosition: "+fromPosition+", toPosition: "+toPosition);
				log.info("Using internal search module...");
				currentSearchResults = search(properties, 
						fromPosition-1, toPosition-fromPosition+1);
			}
		} else {
			currentSearchResults = search(properties, 
					fromPosition-1, toPosition-fromPosition+1);
		}
		
		if (currentSearchResults==null)
			throw new DataProviderException("Got null search results from YIS! " +
					"query: " + properties.getCqlQuery() +
					", first indexId: " + properties.getIndexIds()[0] +
					", mdFormatId: " + properties.getMdFormatId() + 
					", layoutId: " + properties.getLayoutId());
		return prepareSearchBulkData(currentSearchResults.getResults());
	}
	
	/**
	 * Returns bulkData for browse query.
	 * @param properties
	 * @param fromPosition
	 * @param toPosition
	 * @return bulkData for browse query
	 * @throws DataProviderException
	 */
	private String[] getBrowseBulkData(DataProviderBrowseProperties properties, 
			int fromPosition, int toPosition) throws DataProviderException {
		return properties.getPartOfData(fromPosition, toPosition);
	}
	
	/**
	 * YIS search method caller.
	 * @param properties
	 * @param startPosition
	 * @param size
	 * @throws DataProviderException
	 * @return SearchResults
	 */
	private SearchResults search(DataProviderSearchProperties properties, 
			int startPosition, int size) 
			throws DataProviderException {
		if (properties==null || properties.getIndexIds()==null) {
			throw new DataProviderException(
					"Neither properties nor indexIds can be null!");
		}
		DriverQuery query = new DriverQuery();
		query.setCqlQuery(properties.getCqlQuery());
		query.setSruClauses(properties.getSruClauses());
		query.setMdFormatId(properties.getMdFormatId());
		query.setLayoutId(properties.getLayoutId());
		query.setStartPosition(startPosition);
		query.setSize(size);
		
		try {
			if (properties.getIndexIds().length==1 &&
					properties.getIndexIds()[0].equals(DataProviderSearchProperties.INDEX_IDS_ALL))
				return searchModuleFacade.search(query);
			else
				return searchModuleFacade.search(query,properties.getIndexIds());
		} catch (SearchException e) {
			throw new DataProviderException("Exception occured when using YIS module for query: " + 
					query.getCqlQuery() + ", startPosition: " + startPosition + ", size: " + size + 
					", indexIds: " + StringUtils.arrayToCommaDelimitedString(properties.getIndexIds()) + 
					"; YIS error message: "+e.getMessage(), e);
		}
	}

	/* (non-Javadoc)
	 * @see eu.dnetlib.data.index.ws.dataprov.IDataProvider#getNumberOfResults(java.lang.String)
	 */
	public ResultsResponse getNumberOfResults(String bd_id)
			throws DataProviderException {
		if (bd_id==null)
			throw new DataProviderException("BdId cannot be null!");
		
		if (!data.containsKey(bd_id))
			throw new DataProviderException("Non existing BdId!");
		
		DataProviderProperties properties = data.get(bd_id);
		ResultsResponse result = new ResultsResponse();
		result.setStatus(IDataProviderExt.STATUS_CLOSED);
		if (properties==null) {
			log.debug("getNumberOfResults("+bd_id+") call, results: 0"); 
			result.setTotal(0);
			return result;
		} else {
			log.debug("getNumberOfResults("+bd_id+") call, results: " 
					+ properties.getNumberOfResults());
			result.setTotal(properties.getNumberOfResults());
			return result;
		}
	}
	
	private String generateBulkDataId() {
		return "bdid-" + System.currentTimeMillis() + 
			"-" + rand.nextInt(1000);
	}

	/**
	 * Converts search results to bulk data.
	 * @param searchResults
	 * @return parsed String[] of search results
	 */
	private String[] prepareSearchBulkData(List<SearchResult> searchResults) {
		if (searchResults==null || searchResults.size()==0)
			return new String[0];
		String[] result = new String[searchResults.size()];
		for (int i = 0; i < result.length; i++) {
			result[i] = parseSearchBulkData(searchResults.get(i));
		}
		return result;
	}
	
	/**
	 * Converts single search result to bulk data entry.
	 * @param searchResult
	 * @return parsed String of search result
	 */
	private String parseSearchBulkData(SearchResult searchResult) {
		return parseResultFieldWithRank(searchResult);
	}
	
	/**
	 * Parses result field and encapsulates its content in record element with rank attribute.
	 * @param searchResult
	 * @return result field content encapsulated in record element with rank attribute
	 */
	public static String parseResultFieldWithRank(SearchResult searchResult) {
		String resultContent = parseResultField(searchResult);
		StringBuffer strBuff = new StringBuffer();
		strBuff.append("<record rank=\"");
		strBuff.append(searchResult.getScore());
		strBuff.append("\">");
		if (resultContent!=null)
			strBuff.append(resultContent);
		strBuff.append("</record>");
		return strBuff.toString();
	}
	
	/**
	 * Parsing Driver 1.1.0 result type.
	 * @param searchResult
	 * @return result content
	 */
	public static String parseResultField(SearchResult searchResult) {
		if (searchResult==null || searchResult.getFields()==null
				|| searchResult.getFields().size()==0
				|| searchResult.getFields().get(0)==null
				|| searchResult.getFields().get(0).getValues()==null
				|| searchResult.getFields().get(0).getValues().length==0)
			return null;
		else
			return searchResult.getFields().get(0).getValues()[0];
		
	}
	
	/**
	 * Returns internal search module facade.
	 * @return internal search module facade
	 */
	public SearchModuleFacade getSearchModuleFacade() {
		return searchModuleFacade;
	}

	/**
	 * Sets internal search module.
	 * @param searchModuleFacade
	 */
	public void setSearchModuleFacade(SearchModuleFacade searchModuleFacade) {
		this.searchModuleFacade = searchModuleFacade;
	}

	/**
	 * Returns default data provider expiry time (in seconds).
	 * @return default data provider expiry time
	 */
	public int getDefaultExpiryTime() {
		return defaultExpiryTime;
	}

	/**
	 * Sets default data provider expiry time (in seconds).
	 * @param defaultExpiryTime
	 */
	public void setDefaultExpiryTime(int defaultExpiryTime) {
		this.defaultExpiryTime = defaultExpiryTime;
	}

	/* (non-Javadoc)
	 * @see eu.dnetlib.data.ICleanable#cleanup()
	 */
	public void cleanup() {
		log.debug("starting cleanup operations...");
		long currentTime = System.currentTimeMillis();
		synchronized(data) {
			Set<String> keySet = data.keySet();
			Iterator<String> keysIt = keySet.iterator();
			while (keysIt.hasNext()) {
				String currentKey = keysIt.next();
				DataProviderProperties currentProps = data.get(currentKey);
				if (currentTime > currentProps.getExpirationTime()) {
					log.debug("removing data prov: "+currentKey);
					keysIt.remove();
				}
			}
		}
		log.debug("cleanup operations finished");
	}

}
