package gr.uoa.di.webui.search;

import static gr.uoa.di.webui.search.FieldCriterion.Qualifier.ALL;
import static gr.uoa.di.webui.search.FieldCriterion.Qualifier.ANY;
import static gr.uoa.di.webui.search.FieldCriterion.Qualifier.DATE;
import eu.dnetlib.domain.enabling.Vocabulary;
import eu.dnetlib.domain.functionality.DateSearchable;
import eu.dnetlib.utils.cql.Cql;
import eu.dnetlib.utils.cql.CqlBoolean;
import eu.dnetlib.utils.cql.CqlClause;
import eu.dnetlib.utils.cql.CqlException;
import eu.dnetlib.utils.cql.CqlQuery;
import eu.dnetlib.utils.cql.CqlRelation;
import eu.dnetlib.utils.cql.CqlTerm;
import gr.uoa.di.webui.search.FieldCriterion.Qualifier;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

/**
 * Does all the conversions between criteria, cql, url and description of
 * a query
 * @author kiatrop
 *
 */
public class CriteriaManager {

	private static Logger logger = Logger.getLogger(CriteriaManager.class);
	public static final String ALL_FIELD = "all";
	
	public enum Target { CQL, URL, HISTORY };
	
	private LayoutManager layoutManager = null;
	private Map<String,Vocabulary> vocabularyMap = null;

	public CriteriaManager(LayoutManager layoutManager,
			Map<String, Vocabulary> vocabularyMap) {

		this.layoutManager = layoutManager;
		this.vocabularyMap = vocabularyMap;
	}
	
	public LayoutManager getLayoutManager() {
		return layoutManager;
	}

	public Map<String, Vocabulary> getVocabularyMap() {
		return vocabularyMap;
	}
	
	public String convert(Query query, Target target){		
		if(target.equals(Target.HISTORY)){
			return convert(query.baseCriteria, target);
		} 
		
		return convert(query.getCriteria(), target);
	}
	
	public String convert(List<FieldCriterion> criteria, Target target) {
		
		if (logger.isDebugEnabled()) {
			logger.debug("Convert criteria " + criteria + " to " + target);
		}

		StringBuffer queryBuffer = new StringBuffer("(");
		for (int i = 0; i < criteria.size(); i++) {
			appendField(criteria.get(i), target, queryBuffer);
			if (i < criteria.size() - 1) {
				queryBuffer.append(" AND ");
			}
		}
		queryBuffer.append(")");

		String query = queryBuffer.toString();
		if (logger.isDebugEnabled()) {
			logger.debug("Converted criteria to " + query);
		}
		return query;
	}
	
	private void appendField(
			FieldCriterion criterion, Target target, StringBuffer buffer) {

		logger.debug("append field " + criterion);
		
		String value = criterion.getValue();
		if (value == null || value.equals("")) {
			if (logger.isDebugEnabled()) {
				logger.debug("Skip field " + criterion.getName()
						+ " with empty value: " + value);
			}
			return;
		}
		
		Vocabulary voc = null;
		String[] fields = null;
		if (isAllField(criterion.getName())) {
			fields = new String[] { null };
		} else {
			String names = null;
			switch (target) {
			case CQL:
				String vname = layoutManager.getWebLayoutManager().
						getLabelMap().get(criterion.getName()).getVocabulary();
				if (vname != null) {
					voc = getVocabularyMap().get(vname);
				}
				names = layoutManager.getIndexTypeFromName(criterion.getName());
				break;
			case URL:
				names = criterion.getName();
				break;
			case HISTORY:
				names = layoutManager.getWebLayoutManager().getLabelMap().
						get(criterion.getName()).getShortDescription();
				break;
			}
			if (names == null) {
				if (logger.isDebugEnabled()) {
					logger.debug("Skip field " + criterion.getName()
							+ " because mapping is missing");
				}
				return;
			}
			fields = names.split(",");
		}
		
		List<String> terms = new ArrayList<String>(); 
		switch (criterion.getQualifier()) {
		case ALL:
			if (fields.length == 1) {
				appendSimpleTerms(fields[0],
						parseQueryText(value, voc, terms), "AND", buffer);
			} else {
				appendMultipleTerms(fields,
						parseQueryText(value, voc, terms), "AND", buffer);
			}
			break;
		case ANY:
			buffer.append("(");
			if (fields.length == 1) {
				appendSimpleTerms(fields[0],
						parseQueryText(value, voc, terms), "OR", buffer);
			} else {
				appendMultipleTerms(fields,
						parseQueryText(value, voc, terms), "OR", buffer);
			}
			buffer.append(")");
			break;
		case EXACT:
			if (fields.length == 1) {
				appendSimpleTerm(fields[0], parseQuotedTerm(value, voc), buffer);
			} else {
				appendMultipleTerm(fields, parseQuotedTerm(value, voc), buffer);
			}
			break;
		case DATE:
			appendDateTerm(fields[0], parseQuotedTerm(value, voc), buffer);
		}
	}

	// TODO : rename to splitXXXX
	private static String[] parseSimpleTerm(
			String value, Vocabulary vocabulary) {
		
		logger.debug("parsing simple term: " + value);
		System.out.println("parsing simple term: " + value);
		// special terms to ignore
		Pattern p = Pattern.compile(
				"([-~|`,.:;#$%^&*\\\\!@/<>{}\\(\\)\\\"\\'|\\=_?\\]]|" +
				"(\\b(when|and|or|not|if|that|of|the))\\b)",
				Pattern.CASE_INSENSITIVE);
		// replace special chars with whitespace...
		// ...and trim to make sure no ending space
		String[] terms = p.matcher(value).replaceAll(" ").trim().split(" ");

		return terms;
	}

	// TODO : rename to splitXXXX
	private static String parseQuotedTerm(String value, Vocabulary vocabulary) {

		logger.debug("parsing quote term: " + value);

		value = value.trim();
		if (vocabulary != null) {
			if (value.startsWith("\"") && value.endsWith("\"")) {
				value = value.split("\"")[1];
			}
			value = vocabulary.getEncoding(value);
		}
		
		if (value != null && value.startsWith("\"") && value.endsWith("\"")) {
			return value;
		} else {
			return "\"" + value + "\"";
		}
	}
	
	private static void appendSimpleTerms(String field,
			String[] terms, String operator, StringBuffer cql) {
		
		String prefix = (isAllField(field))? "" : field + " = ";
		
		for (int i = 0; i < terms.length; i++) {
			cql.append(prefix).append(terms[i]);
			if (i < terms.length - 1) { // no space at the end
				cql.append(" ").append(operator).append(" ");
				logger.debug("appending simple term: " + terms[i]);
				logger.debug("cql sp far: " + cql);
			}
		}
	}
	
	private static void appendMultipleTerms(String[] fields,
			String[] terms, String operator, StringBuffer cql) {
		
		String[] prefixes = new String[fields.length];
		for (int i = 0; i < prefixes.length; i++) {
			prefixes[i] = (isAllField(fields[i]))? "" : fields[i] + " = ";
		} 
		
		for (int i = 0; i < terms.length; i++) {
			cql.append("(");
			for (int j = 0; j < prefixes.length; j++) {
				cql.append(prefixes[j]).append(terms[i]);
				if (j < prefixes.length -1) {
					cql.append(" OR ");
				}
			}
			cql.append(")");
			if (i < terms.length - 1) { // no space at the end
				cql.append(" ").append(operator).append(" ");
			}
		}
		
		logger.debug("cql so far: " + cql);
	}
	
	private static void appendSimpleTerm(String field,
			String term, StringBuffer cql) {
		String prefix = (isAllField(field))? "" : field + " = ";
		cql.append(prefix).append(term);
	}
	
	private static void appendDateTerm(String field,
			String term, StringBuffer cql) {
		String prefix = field + " within ";
		cql.append(prefix).append(term);
	}
	
	private static void appendMultipleTerm(String[] fields,
			String term, StringBuffer cql) {
		
		cql.append("(");
		for (int i = 0; i < fields.length; i++) {
			String prefix = (isAllField(fields[i]))? "" : fields[i] + " = ";
			cql.append(prefix).append(term);
			if (i < fields.length -1) {
				cql.append(" OR ");
			}
		}
		cql.append(")");
	
	}
	
	private static boolean isAllField(String field) {
		return field == null || field.toLowerCase().equals(ALL_FIELD);
	}

	private static boolean isQuotedValue(String value) {
		return value.startsWith("\"") && value.endsWith("\"");
	}

	
	public String convert2DocumentQuery(Query query) {
		return convert(query, Target.CQL);
	}

	
	public List<FieldCriterion> convert(String query, Target target)
			throws CqlException {
	
		if (logger.isDebugEnabled()) {
			logger.debug("Convert query " + query + " to criteria," +
					"assuming target " + target + ".");
		}

		// Parsing a CQL query
		CqlQuery cql = Cql.parse(query);
		if (cql == null) {
			logger.warn("Produced null criteria for query " + query);
        	return null;
        }
		
        List<FieldCriterion> criteria = new ArrayList<FieldCriterion>();
        buildCriteria(cql.getRoot(), true, true, target, criteria);

        if (criteria.size() == 0) {
        	logger.debug("Adding default empty criteria.");
        	criteria.add(buildEmptyCriterion());
        }

        if (logger.isDebugEnabled()) {
			logger.debug("Converted query to " + criteria + ".");
		}

		return criteria;
	}

	private static FieldCriterion buildEmptyCriterion() {
		return new FieldCriterion("all", "all searchables", "", ALL);
	}
	
	/**
	 * Update <code>criteria</code> to match the query <code>root</code>.
	 * If <code>mustHold</code> is set, then the query is processed as is;
	 * otherwise, the query negation is assumed.
	 *
	 * @param root	The query used to build the criteria.
	 * @param mustHold	If <code>true</code> the query is processed as is,
	 * 					otherwise the query negation is considered.
	 * @param criteria	The criteria object to update.
	 * @throws ParseException
	 */
	private void buildCriteria(CqlClause root,
			boolean mustHold, boolean conjunction,
			Target target, List<FieldCriterion> criteria) {

		if (logger.isDebugEnabled()) {
			logger.debug("Called built criteria with " + root);
			logger.debug("criteria " + criteria);
		}

		if (root == null) {
			// nothing to do
			logger.warn("Null query in building document search criteria.");

		} else if (root instanceof CqlBoolean) {
			logger.debug("instance of cqlBoolean");
			// boolean operator
			CqlBoolean bool = (CqlBoolean) root;
			if (bool.getOperator().equalsIgnoreCase("AND")) {
				// hold = true, conjunction = true
				buildCriteria(bool.getLeft(), true, true, target, criteria);
				buildCriteria(bool.getRight(), true, true, target, criteria);

			} else if (bool.getOperator().equalsIgnoreCase("OR")) {
				// TODO check OR clause combines same index
				// hold = true, conjunction = false
				List<FieldCriterion> tmp = new ArrayList<FieldCriterion>();
				buildCriteria(bool.getLeft(), true, false, target, tmp);
				buildCriteria(bool.getRight(), true, false, target, tmp);
				criteria.addAll(tmp);

			} else if (bool.getOperator().equalsIgnoreCase("NOT")) {
				// hold = true, conjunction = true
				buildCriteria(bool.getLeft(), true, true, target, criteria);
				// hold = false, conjunction = true
				buildCriteria(bool.getRight(), false, true, target, criteria);

			} else {
				// unsupported operator
				logger.warn("Operator " + bool.getOperator() +
						" is not supported in \"" + bool +
						"\". Ignoring operands!");
			}

		} else if (root instanceof CqlRelation) {
			logger.debug("instance of cqlrelation");
			// qualified term
			CqlRelation relation = (CqlRelation) root;
			try {
				buildQualifiedCriterion(
						relation.getIndex(), relation.getOperator(),
						relation.getValue(), mustHold, conjunction,
						target, criteria);
			} catch (ParseException e) {
				// TODO reconsider catch clause;
				logger.error("Could not parse date criterion ", e);
			}

		} else if (root instanceof CqlTerm) {
			logger.debug("instance of cqlTerm");
			// single term
			CqlTerm term = (CqlTerm) root;
			buildFreeCriterion(
					term.getTerm(), mustHold, conjunction, target, criteria);

		} else {
			// should not be here!
			logger.error("Query clause \"" + root +
					"\" has unsupported class " + root.getClass().getName());
		}
	}

	/**
	 * Add qualified criterion of the form
	 * <code>index</code> <code>rel</code> <code>value</code> to the given
	 * <code>criteria</code> object. If <code>mustHold</code> is true, then
	 * process the criterion as is, otherwise assume the negation of the
	 * criterion.
	 *
	 * @param index	The criterion index.
	 * @param rel	The criterion relation.
	 * @param value	The criterion value.
	 * @param mustHold	<code>true</code> if the criterion holds as is,
	 * 					otherwise the criterion negation is assumed.
	 * @param criteria	The criteria object to append the new criterion.
	 * @throws ParseException
	 */
	private void buildQualifiedCriterion(
			String index, String rel, String value, 
			boolean mustHold, boolean conjunction,
			Target target, List<FieldCriterion> criteria) throws ParseException {

		if (logger.isDebugEnabled()) {
			logger.debug("Called qual with index " + index  + " and rel " +
					rel + " and value " + value);
		}
		
		FieldCriterion selected = null;
		Qualifier constraint;

		String field = null;
		switch (target) {
		case URL:
			field = index;
			break;
		case CQL:
			field = getLayoutManager().getNameFromIndexType(index); 
			break;
		case HISTORY:
			field = getLayoutManager().getNameFromShortDesc(index);
			break;
		}

		String display = getLayoutManager().getDisplayFromName(field);
		
		if (rel.equalsIgnoreCase("within")) {
			value = trimQuotes(value);
			constraint = DATE;
			DateSearchable dateSearchable = 
				(DateSearchable) this.layoutManager.getWebLayoutManager().getLabelMap().get(field);
			
			selected = new DateCriterion(field, display, value, dateSearchable.getRanges());
			criteria.add(selected);
			return;
			
		} else {
			
			constraint = (conjunction) ? ALL : ANY;

		/*	if (isQuotedValue(value)) {
				constraint = EXACT;
				value = value.split("\"")[1];
			} else { 
				constraint = (conjunction) ? ALL : ANY;
			}		
		*/			
			// find matching criterion and add value
			for (FieldCriterion c : criteria) {
				if (c.getName().equalsIgnoreCase(field)) {
					if (c.getQualifier() == constraint) {
						selected = c;
						break;
					}
				}
			}
		}	

		if (selected == null) {
			criteria.add(new FieldCriterion(field, display, value, constraint));
		} else {
			value = selected.getValue() + " " + value;
			selected.setValue(value);
		}
	
	}

	/**
	 * Add an "unqualified criterion", or "single term" to the given
	 * <code>criteria</code> object. If <code>mustHold</code> is true, then
	 * process the criterion as is, otherwise assume the negation of the
	 * criterion.
	 *
	 * @param value	The criterion term.
	 * @param mustHold	<code>true</code> if the criterion holds as is,
	 * 					otherwise the criterion negation is assumed.
	 * @param criteria	The criteria object to append the new criterion.
	 */
	private static void buildFreeCriterion(
			String value, boolean mustHold, boolean conjunction,
			Target target, List<FieldCriterion> criteria) {

		Qualifier constraint = (conjunction) ? ALL : ANY;
	/*	if (isQuotedValue(value)) {
			constraint = EXACT;
		} else {
			constraint = (conjunction) ? ALL : ANY;
		}
*/		
		// find matching criterion and add value
		FieldCriterion selected = null;
		for (FieldCriterion c : criteria) {
			if (c.getName().equalsIgnoreCase(ALL_FIELD)) {
				if (c.getQualifier() == constraint) {
					selected = c;
					break;
				}
			}
		}

		if (selected == null) {
			criteria.add(buildEmptyCriterion());
			criteria.get(0).setQualifier(constraint);
			criteria.get(0).setValue(value);

		} else {
			value = selected.getValue() + " " + value;
			selected.setValue(value);
		}
	}
	
	private String trimQuotes(String text) {
		
		boolean trimStart = text.startsWith("\"");
		boolean trimEnd = text.endsWith("\"");
		
		if (trimStart || trimEnd) {
			int n = text.length();
			return text.substring((trimStart)?1:0, (trimEnd)?n-1:n);
		} else {
			return text;
		}
	}
	
	private static String[] parseQueryText(String value, Vocabulary vocabulary, List<String> terms) {

		System.out.println(value);
		
		String firstPart = null;
		String secondPart = null;
		
		int first = value.indexOf("\"");
		
		if (first == -1){
			String[] array = parseSimpleTerm(value, vocabulary);
			for (int i = 0; i < array.length; i++) {
				if(!array[i].trim().equals("")){
					System.out.println(">" + array[i]);
					terms.add(array[i]);
				}
			}
			
		} else if (first > 0 ){
			firstPart = value.substring(0,first);
			String[] array = parseSimpleTerm(firstPart, vocabulary);
				
			for (int i = 0; i < array.length; i++) {
				if(!array[i].trim().equals("")){
					terms.add(array[i]);
				}
			}
			
			secondPart = value.substring(first, value.length());
			parseQueryText(secondPart, vocabulary, terms);
			
		} else if (first == 0) {
			firstPart = value.substring(1, value.length());
			int second = firstPart.indexOf("\"");

			if (second == -1){
				logger.error("ERROR parsing terms");
				
			} else {
				String term = parseQuotedTerm(firstPart.substring(0, second), vocabulary);
				if(!term.trim().equals("")){
					terms.add(term);
				}
				
				if (second < value.length()){
					secondPart = firstPart.substring(second+1,value.length()-1);
					parseQueryText(secondPart, vocabulary, terms);
				}
				
			}
			
		}
		
		return terms.toArray(new String[0]);
	
	}
	
	public static void main(String[] args) {
		
		String[] array = "\"Albanian's\" \"Greek\"".split("\"");
		
		System.out.println("----");
		for (int i = 0; i < array.length; i++) {
			System.out.println(array[i].trim().isEmpty());
		}
	}
	
}
