package eu.dnetlib.data.search.app;

import eu.dnetlib.api.data.IndexService;
import eu.dnetlib.api.data.IndexServiceException;
import eu.dnetlib.api.data.SearchService;
import eu.dnetlib.api.data.SearchServiceException;
import eu.dnetlib.api.enabling.ResultSetService;
import eu.dnetlib.common.rmi.UnimplementedException;
import eu.dnetlib.data.search.app.plan.FieldRewriteRule;
import eu.dnetlib.data.search.app.plan.Query;
import eu.dnetlib.data.search.app.plan.QueryRewriteRule;
import eu.dnetlib.data.search.transform.Transformer;
import eu.dnetlib.data.search.transform.TransformerException;
import eu.dnetlib.data.search.transform.config.SearchRegistry;
import eu.dnetlib.domain.EPR;
import eu.dnetlib.domain.data.SearchResult;
import eu.dnetlib.domain.data.SuggestiveResult;
import eu.dnetlib.utils.cql.CqlBoolean;
import eu.dnetlib.utils.cql.CqlClause;
import eu.dnetlib.utils.cql.CqlException;
import eu.dnetlib.utils.cql.CqlRelation;
import gr.uoa.di.driver.app.DriverServiceImpl;
import gr.uoa.di.driver.enabling.resultset.ResultSet;
import gr.uoa.di.driver.enabling.resultset.ResultSetFactory;
import gr.uoa.di.driver.util.ServiceLocator;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.log4j.Logger;

public class SearchServiceImpl extends DriverServiceImpl
		implements SearchService {
	
	private static Logger logger = Logger.getLogger(SearchServiceImpl.class);
	private static Logger tlogger = Logger.getLogger("eu.dnetlib.data.search.app.Timer");
	
	private String mdFormat = "DMF";
	
	private ServiceLocator<IndexService> indexLocator = null;
	private ServiceLocator<ResultSetService> resultSetLocator = null;
	private ResultSetFactory rsFactory = null;
	private SearchRegistry transformerFactory = null;
	private List<QueryRewriteRule> queryRules = null;
	private Map<String, FieldRewriteRule> fieldRules = null;
	private boolean enableBrowseCache = false;
	
	public String getMdFormat() {
		return mdFormat;
	}

	public void setMdFormat(String mdFormat) {
		this.mdFormat = mdFormat;
	}

	public ServiceLocator<IndexService> getIndexLocator() {
		return indexLocator;
	}

	public void setIndexLocator(ServiceLocator<IndexService> indexLocator) {
		this.indexLocator = indexLocator;
	}

	public ServiceLocator<ResultSetService> getResultSetLocator() {
		return resultSetLocator;
	}

	public void setResultSetLocator(
			ServiceLocator<ResultSetService> resultSetLocator) {
		this.resultSetLocator = resultSetLocator;
	}

	public ResultSetFactory getRsFactory() {
		return rsFactory;
	}

	public void setRsFactory(ResultSetFactory rsFactory) {
		this.rsFactory = rsFactory;
	}

	public Collection<FieldRewriteRule> getFieldRules() {
		return fieldRules.values();
	}

	public void setFieldRules(Collection<FieldRewriteRule> fieldRules) {
		this.fieldRules = new HashMap<String, FieldRewriteRule>();
		for (FieldRewriteRule rule : fieldRules) {
			String key = rule.getFieldName();
			if (this.fieldRules.containsKey(key)) {
				logger.warn("Multiple rules for field " + key);
				logger.warn("Keeping last rule " + rule.getName());
			}
			this.fieldRules.put(key, rule);
		}
	}

	public List<QueryRewriteRule> getQueryRules() {
		return queryRules;
	}

	public void setQueryRules(List<QueryRewriteRule> queryRules) {
		this.queryRules = queryRules;
	}
	
	public boolean isEnableBrowseCache() {
		return enableBrowseCache;
	}

	public void setEnableBrowseCache(boolean enableBrowseCache) {
		this.enableBrowseCache = enableBrowseCache;
	}

	@Override
	public void init() {
		super.init();
	}
	
	
	
	Query rewrite(Query query) throws SearchServiceException {		
		if (logger.isDebugEnabled()) {
			logger.debug("Apply query rules on " + query + " ...");
		}
		
		if (queryRules != null) {
			for (QueryRewriteRule queryRule: queryRules) {
				try {
					if (logger.isDebugEnabled()) {
						logger.debug("Apply rule " + query);
					}
					query = queryRule.apply(query);
					if (logger.isDebugEnabled()) {
						logger.debug("Rewritten query is " + query);
					}
					
				} catch (CqlException cqle) {
					// log and continue with other rules if one fails
					logger.warn("Rule failed: " + cqle.getMessage(), cqle);
				}
			}
		}
		
		if (logger.isDebugEnabled()) {
			logger.debug("Apply field rules on " + query + " ...");
		}
		
		CqlClause root = rewrite(query.getCqlQuery().getRoot());
		try {
			query = new Query(root.toCqlString());
		} catch (CqlException cqle) {
			logger.warn("Malformed CQL: " + root.toCqlString(), cqle);
			throw new SearchServiceException(
					"Malformed CQL: " + root.toCqlString());
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Rewrite yields " + query);
		}
		
		return query;
	}
	
	CqlClause rewrite(CqlClause clause) {
		logger.debug("Field rules are : " + fieldRules.toString());
		if (fieldRules == null) {
			return clause;
		}
		
		switch (clause.type) {
		case BOOLEAN:
			CqlBoolean bool = (CqlBoolean) clause;
			clause = new CqlBoolean(rewrite(bool.getLeft()),
					bool.getOperator(), rewrite(bool.getRight()));
			break;
		case TERM:
			// no rewrite
			break;
		case RELATION:
			CqlRelation relation = (CqlRelation) clause;
			FieldRewriteRule rule = fieldRules.get(relation.getIndex());
			if (rule != null) {
				try {
					if (logger.isDebugEnabled()) {
						logger.debug("Apply rule " + rule.getName()
								+ " on " + clause.toCqlString() + " ...");
					}
					clause = rule.apply(relation);
					if (logger.isDebugEnabled()) {
						logger.debug("New clause is " + clause.toCqlString());
					}
				} catch (CqlException cqle) {
					// log and continue with other rules if one fails
					logger.warn("Rule failed: " + cqle.getMessage(), cqle);
				}
			}
			break;
		default:
			throw new IllegalArgumentException("Unknown CQL clause type.");
		}
		
		return clause; 
	}
	
	@Override
	public SuggestiveResult suggestiveSearch(String query)
			throws SearchServiceException {
		
		throw new UnimplementedException();		
	/*	logger.debug("running suggestive search for " + query);
		
		SuggestiveResult suggestiveResult = new SuggestiveResult();		
		IndexService index = getIndexLocator().getService();
		try {
			Hint hint = index.suggestiveSearch("all", "query=" + query.toString(), mdFormat, "index", "SimpleRatioHeuristics");
			String alternateTerm = hint.getAlternateTerm();
			boolean autofollow = hint.isAutoFollowHint();
			logger.debug("alternateTerm " + alternateTerm + " autofollow " + autofollow);
			
			if (alternateTerm != null) {
				suggestiveResult.setAlternativeTerm(alternateTerm);
				suggestiveResult.setAutofollow(autofollow);
				
				if (autofollow) {
					logger.debug("getting epr for " + alternateTerm);
					suggestiveResult.setEpr(search(alternateTerm));
									
				} else {
					logger.debug("getting epr for " + query);
					suggestiveResult.setEpr(search(query));
				}
				
			} else {
				logger.debug("getting epr for " + query);
				suggestiveResult.setEpr(search(query));
			}
			
 		} catch (IndexServiceException ise) {
			logger.error("Error calling index", ise);
			throw new SearchServiceException("Error calling index.");
		}
		return suggestiveResult; */
	}

	public SearchRegistry getTransformerFactory() {
		return transformerFactory;
	}

	public void setTransformerFactory(SearchRegistry transformerFactory) {
		this.transformerFactory = transformerFactory;
	}

	@Override
	public SearchResult search(String text, String transformer, String format, String locale, int page, int size) throws SearchServiceException {
		logger.debug("Received Query: " + text);
		tlogger.debug("Recieved query " + text);

		IndexService index = getIndexLocator().getService();
		EPR epr = null;		
		Query query = null;

		List<String> searchResults = null;

		try {
			long time = System.currentTimeMillis();
			
			query = rewrite(new Query(text));
	
			if (logger.isDebugEnabled()) {
				logger.debug("index lookup (all, query=" + query.toString() + ", " + mdFormat + ", index)");
			}

			/**
			 * Ask index for search results
			 **/
			
			// TODO: remove query= part			
			epr = index.indexLookup("all", "query=" + query.toString(), mdFormat, "index");
			
			if (logger.isDebugEnabled()) {
				logger.debug("epr = " + epr);
			}
			
			time = System.currentTimeMillis() - time;
			logger.debug("index query lasted " + time + " msec");
			tlogger.debug("Got index response for query " + query);
			
		} catch (IndexServiceException ise) {
			logger.error("Error calling index.", ise);
			throw new SearchServiceException("Error calling index.");
			
		} catch (CqlException cqle) {
			logger.error("Bad CQL query.", cqle);
			throw new SearchServiceException("Error calling index.");
		}
		
		if (epr == null) {
			throw new SearchServiceException("Index returned null result set id.");
		}
		
		//get the locale
		StringTokenizer tokenizer = new StringTokenizer(locale, "_");
		Locale requestLocale = new Locale(tokenizer.nextToken(), tokenizer.nextToken());
		
		/**
		 * Read ResultSet for search
		 */
		ResultSet<String> rs = rsFactory.createResultSet(epr);

		try {
			int rsSize = rs.size();
			int from = (page-1)*size + 1;
			int to = (page*size < rsSize) ? page*size : rsSize;
			
			List<String> xmls = rs.getElements(from, to);
			
			Transformer tr = transformerFactory.getTransformer(transformer, requestLocale);
			searchResults = new ArrayList<String>();			
			/**
			 * Transform the search records xmls
			 */
			for (String xml:xmls) {
					searchResults.add(tr.transform(xml));
			}

		} catch (TransformerException te) {
			logger.error("Error transforming search results.", te);
			throw new SearchServiceException("Error transforming search results.", te);
		}
		logger.debug("Search results for query "+ query.getText()+ " created.");
		String correctLocale = null;
		if (locale == null)
			correctLocale = transformerFactory.getConfig().getDefaultLocale().getLanguage() + "_" + transformerFactory.getConfig().getDefaultLocale().getCountry();
		else
			correctLocale = locale;

		logger.debug("Returned results for query " + query);
		return new SearchResult(query.getText(), format, correctLocale, rs.size(), page, size, searchResults);
	}
	
	@Override
	public SearchResult refine(String text, String transformer, String format, String locale, Collection<String> fields) throws SearchServiceException {
		IndexService index = getIndexLocator().getService();
		EPR epr = null;		
		Query query = null;

		List<String> browseResults = null;
		ResultSet<String> rs = null;
				
		/**
		 * For refine
		 */
		try {
			long time = System.currentTimeMillis(); 
			
			query = rewrite(new Query(text));
			StringBuffer buffer = new StringBuffer();
			buffer.append("query=").append(query.toString()).append("&groupby=");
			for (Iterator<String> iter = fields.iterator(); iter.hasNext();) {
				String field = (String) iter.next();
				buffer.append(field);
				if (iter.hasNext()) {
					buffer.append(",");
				}
			}
			
			if (logger.isDebugEnabled()) {
				logger.debug("index refine (" + buffer.toString()
						+ ", all, " + mdFormat + ", index)");
			}

			epr = index.getBrowsingStatistics(buffer.toString(), "all", mdFormat, "index");
			
			time = System.currentTimeMillis() - time;
			logger.debug("index query lasted " + time + " msec");
			
		} catch (CqlException cqle) {
			logger.error("Bad CQL query.", cqle);
			throw new SearchServiceException("Error calling index.");
		} catch (IndexServiceException ise) {
			logger.error("Error getting refine results.", ise);
			throw new SearchServiceException("Error getting refine results.", ise);
		}
	
		if (epr == null) {
			throw new SearchServiceException("Index returned null result set id.");
		}

		//get the locale
		StringTokenizer tokenizer = new StringTokenizer(locale, "_");
		Locale requestLocale = new Locale(tokenizer.nextToken(), tokenizer.nextToken());
		
		try{
			rs = rsFactory.createResultSet(epr);

			if (logger.isDebugEnabled()) {
				logger.debug("EPR : " + epr.getEpr());
			}
			
			List<String> list = rs.getElements(1, rs.size());
			logger.debug("record list size " + list.size());
			
			/**
			 * Transform each refine row 
			 */
			Transformer tr = transformerFactory.getTransformer("results_openaire_browse", requestLocale);
			browseResults = new ArrayList<String>();
			for (String row: rs.getElements(1, rs.size())) {
				browseResults.add(tr.transform(row));				
			}
			
		} catch (TransformerException te) {
			logger.error("Error transforming refine results.", te);
			throw new SearchServiceException("Error transforming refine results.", te);
		}
		String correctLocale = null;
		if(locale == null)
			correctLocale = transformerFactory.getConfig().getDefaultLocale().getLanguage() + "_" + transformerFactory.getConfig().getDefaultLocale().getCountry();
		else
			correctLocale = locale;
		return new SearchResult(text, format, correctLocale, fields, browseResults);
	}
	
	@Override
	public SearchResult searchNrefine(String text, String searchTransformer, String browseTransformer, String format, 
			String locale, int page, int size, Collection<String> fields) throws SearchServiceException {
		logger.info("Received Query: " + text);

		IndexService index = getIndexLocator().getService();
		EPR epr = null;		
		Query query = null;

		//get the locale
		StringTokenizer tokenizer = new StringTokenizer(locale, "_");
		Locale requestLocale = new Locale(tokenizer.nextToken(), tokenizer.nextToken());
		
		/** 
		 * search 
		 * **/
		List<String> searchResults = null;

		try {
			long time = System.currentTimeMillis();
			
			query = rewrite(new Query(text));
	
			if (logger.isDebugEnabled()) {
				logger.debug("index lookup (all, query=" + query.toString() + ", " + mdFormat + ", index)");
			}

			/**
			 * Ask index for search results
			 **/
			
			// TODO: remove query= part			
			epr = index.indexLookup("all", "query=" + query.toString(), mdFormat, "index");
			
			if (logger.isDebugEnabled()) {
				logger.debug("epr = " + epr);
			}
			
			time = System.currentTimeMillis() - time;
			logger.debug("index query lasted " + time + " msec");

		} catch (IndexServiceException ise) {
			logger.error("Error calling index.", ise);
			throw new SearchServiceException("Error calling index.");
			
		} catch (CqlException cqle) {
			logger.error("Bad CQL query.", cqle);
			throw new SearchServiceException("Error calling index.");
		}
		
		if (epr == null) {
			throw new SearchServiceException("Index returned null result set id.");
		}
		
		int rsSize = -1;
		/**
		 * Read ResultSet for search
		 */
		ResultSet<String> rs = rsFactory.createResultSet(epr);
		try {
			rsSize = rs.size();
			int from = (page-1)*size + 1;
			int to = (page*size < rsSize) ? page*size : rsSize;
			
			List<String> xmls = rs.getElements(from, to);
			
			Transformer tr = transformerFactory.getTransformer(searchTransformer, requestLocale);
			searchResults = new ArrayList<String>();			
			/**
			 * Transform the search records xmls
			 */
			for (String xml:xmls) {
					searchResults.add(tr.transform(xml));
			}

		} catch (TransformerException te) {
			logger.error("Error transforming search results.", te);
			throw new SearchServiceException("Error transforming search results.", te);
		}
		/**
		 * refine
		 */
		List<String> browseResults = null;
				
		try {
			long time = System.currentTimeMillis();
			
			query = rewrite(new Query(text));
			StringBuffer buffer = new StringBuffer();
			buffer.append("query=").append(query.toString()).append("&groupby=");
			for (Iterator<String> iter = fields.iterator(); iter.hasNext();) {
				String field = (String) iter.next();
				buffer.append(field);
				if (iter.hasNext()) {
					buffer.append(",");
				}
			}
			
			if (logger.isDebugEnabled()) {
				logger.debug("index refine (" + buffer.toString()
						+ ", all, " + mdFormat + ", index)");
			}

			epr = index.getBrowsingStatistics(buffer.toString(), "all", mdFormat, "index");
			
			time = System.currentTimeMillis() - time;
			logger.debug("index query lasted " + time + " msec");
		
		} catch (CqlException cqle) {
			logger.error("Bad CQL query.", cqle);
			throw new SearchServiceException("Error calling index.");
		} catch (IndexServiceException ise) {
			logger.error("Error getting refine results.", ise);
			throw new SearchServiceException("Error getting refine results.", ise);
		}
	
		if (epr == null) {
			throw new SearchServiceException("Index returned null result set id.");
		}
		
		try{
			rs = rsFactory.createResultSet(epr);

			if (logger.isDebugEnabled()) {
				logger.debug("EPR : " + epr.getEpr());
			}
			
			List<String> list = rs.getElements(1, rs.size());
			logger.debug("record list size " + list.size());
			
			Transformer tr = transformerFactory.getTransformer(browseTransformer, requestLocale);
			browseResults = new ArrayList<String>();
			for (String row: rs.getElements(1, rs.size())) {
				browseResults.add(tr.transform(row));				
			}
			
		} catch (TransformerException te) {
			logger.error("Error transforming refine results.", te);
			throw new SearchServiceException("Error transforming refine results.", te);
			
		}
		String correctLocale = null;
		if(locale == null)
			correctLocale = transformerFactory.getConfig().getDefaultLocale().getLanguage() + "_" + transformerFactory.getConfig().getDefaultLocale().getCountry();
		else
			correctLocale = locale;
		return new SearchResult(query.getText(), format, correctLocale, rsSize, page, size, searchResults, browseResults, fields);
		
	}
	
	public List<String> getSearchResultsFromIndex(){
		return null;
	}
	
	public List<String> getRefineResultsFromIndex(){
		return null;
	}
	
}
