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.SearchableDate;
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.driver.web.utils.LocaleDescriptionUtil;
import gr.uoa.di.webui.search.FieldCriterion.Qualifier;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
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 LocaleVocabularyMap vocabularyMap = null;
	private LocaleVocabularyMap resultvocabularyMap = null;

	public CriteriaManager(LayoutManager layoutManager,
			LocaleVocabularyMap vocabularyMap,
			LocaleVocabularyMap resultVocabularyMap) {

		this.layoutManager = layoutManager;
		this.vocabularyMap = vocabularyMap;
		this.resultvocabularyMap = resultVocabularyMap;
	}

	public LocaleVocabularyMap getResultvocabularyMap() {
		return resultvocabularyMap;
	}

	public LayoutManager getLayoutManager() {
		return layoutManager;
	}

	public LocaleVocabularyMap getVocabularyMap() {
		return vocabularyMap;
	}

	public String convert(Query query, Target target, Locale locale) {
		return convert(query.getCriteria(), target, locale);
	}

	public String convert(List<FieldCriterion> criteria, Target target, Locale locale) {

		if (logger.isDebugEnabled()) {
			logger.debug("Convert criteria " + criteria + " to " + target);
		}

		if (criteria.isEmpty()){
			return null;
		}
		
		StringBuffer queryBuffer = new StringBuffer("(");
		for (int i = 0; i < criteria.size(); i++) {
			appendField(criteria.get(i), target, queryBuffer, locale);
			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, Locale locale) {

		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 index = layoutManager.getWebLayoutManager().getLabelMap()
											.get(criterion.getName()).getIndexType();
				voc = resultvocabularyMap.getVocabulary(index, locale);
				
				names = layoutManager.getIndexTypeFromName(criterion.getName());
				break;
			case URL:
				index = layoutManager.getWebLayoutManager().getLabelMap().get(criterion.getName()).getIndexType();
				voc = resultvocabularyMap.getVocabulary(index, locale);
				names = criterion.getName();
				break;
			case HISTORY:
				names = LocaleDescriptionUtil.getDescription(layoutManager.getWebLayoutManager().getLabelMap().get(criterion.getName()).getShortDescriptionMap(), locale);
				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);
		// 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);

		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);
	}

	/* Please do not remove. Method called in checkbox.jsp */
	public static boolean isQuotedValue(String value) {
		return value.startsWith("\"") && value.endsWith("\"");
	}

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

	public List<FieldCriterion> convert(String query, Target source,
			Target target, Locale locale) throws CqlException {

		if (logger.isDebugEnabled()) {
			logger.debug("Convert query " + query + " to criteria,"
					+ "assuming target " + target + ".");
		}
		
		CqlQuery cql = null;
		// Parsing a CQL query
		if(query!=null && !query.isEmpty()) {
			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, source, target, criteria, locale);

		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 fields", "", 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 source, Target target,
			List<FieldCriterion> criteria, Locale locale) {

		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, source, target,
						criteria, locale);
				buildCriteria(bool.getRight(), true, true, source, target,
						criteria, locale);

			} 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, source, target, tmp, locale);
				buildCriteria(bool.getRight(), true, false, source, target, tmp, locale);
				criteria.addAll(tmp);

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

			} 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 {
				String value = null;
				String index = null;
				
				if (source.equals(Target.CQL)) {
					String vocabularyName = layoutManager.
					getWebLayoutManager()
							.getIndexMap().
							get(relation.getIndex().toLowerCase()).
							getSearchVocabulary();
					
					if (vocabularyName != null && !vocabularyName.isEmpty()) {
						Vocabulary vocabulary = vocabularyMap.getVocabulary(relation.getIndex(), locale);
						
						if (vocabulary != null) {
							value = "\"" + vocabulary.getEnglishName(trimQuotes(relation.getValue())) + "\"";

						}
					} else {
						value = trimQuotes(relation.getValue());
					}
					
					index = layoutManager.getWebLayoutManager().getIndexMap().get(relation.getIndex().toLowerCase()).getName();


					
				} else if (source.equals(Target.URL)) {				
					String fieldName = layoutManager.getWebLayoutManager().getLabelMap().get(relation.getIndex()).getIndexType();
					Vocabulary vocabulary = vocabularyMap.getVocabulary(fieldName, locale);
					index = layoutManager.getWebLayoutManager().getLabelMap().get(relation.getIndex()).getName();
					if (vocabulary != null) {
						value = "\"" + vocabulary.getEnglishName(trimQuotes(relation.getValue())) + "\"";
						
					} else {
						value = relation.getValue();
					}
					
					
				} else {
					value = relation.getValue();
					index = relation.getIndex();
				}

				
				buildQualifiedCriterion(index, relation.getOperator(), value, 
					mustHold, conjunction, source, target,
						criteria, locale);
			} 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 source,
			Target target, List<FieldCriterion> criteria, Locale locale) 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, locale);

		if (rel.equalsIgnoreCase("within")) {
			if (!source.equals(Target.CQL)) {
				value = trimQuotes(value);
			}
			constraint = DATE;
			SearchableDate dateSearchable = (SearchableDate) this.layoutManager
					.getWebLayoutManager().getLabelMap().get(field);

			selected = new DateCriterion(field, display, value, LocaleDescriptionUtil.getRanges(dateSearchable, locale));
			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(criteria.size() - 1).setQualifier(constraint);
			criteria.get(criteria.size() - 1).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("Parsing " + 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("")) {
					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]);

	}

}