package eu.dnetlib.functionality.cql.parse;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.queryparser.classic.QueryParserBase;

public class TermNode extends Node {

	public static final String dnetDefaultField = "__all";

	private String field;
	private Relation rel;
	private String value;
	private Map<String, List<String>> options = Maps.newHashMap();
	private BiMap<String, String> aliases = HashBiMap.create();
	private Map<String, String> weights = Maps.newHashMap();
	private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

	public TermNode(final String field, final Relation rel, final String value) {
		this.field = field;
		this.rel = rel;
		this.value = value;
		simpleDateFormat.setLenient(false);
	}

	public TermNode(final String field, final Relation rel, final String value, final Map<String, List<String>> options, final BiMap<String, String> aliases,
			final Map<String, String> weights) {
		this(field, rel, value);
		this.options = options;
		this.aliases = aliases;
		this.weights = weights;
	}

	@Override
	public String toString() {
		return TermNode.class.getSimpleName() + "(" + field + " " + rel + " " + value + ")";
	}

	@Override
	public String toLucene() {
		return _toLucene().trim();
	}

	private String _toLucene() {
		final Stream<String> termTokenizer = Splitter.on(" ").omitEmptyStrings().splitToList(value).stream();
		final Stream<String> weightTokenizer = Splitter.on(" ").omitEmptyStrings().splitToList(value).stream();
		switch (rel) {
		case EXACT:
			final String lucene = getFieldName() + ":" + "\"" + value + "\"";
			return StringUtils.isNotBlank(weight()) ? lucene + weight() + " " + expand(value) : lucene;
		case EQUAL:
		case ALL:
		case ANY:
			return Joiner.on(" ").skipNulls().join(handleTokens(termTokenizer), expandTokens(weightTokenizer)).trim();
		case NOT:
			return "NOT " + field + ":" + "\"" + value + "\"";
		case LT:
			if (isDate(value)) {
				value = checkDate(value);
			}
			return field + ":" + "{* TO " + value + "}" + weight();
		case GT:
			if (isDate(value)) {
				value = checkDate(value);
			}
			return field + ":" + "{" + value + " TO *}" + weight();
		case LTE:
			if (isDate(value)) {
				value = checkDate(value);
			}
			return field + ":" + "[* TO " + value + "]" + weight();
		case GTE:
			if (isDate(value)) {
				value = checkDate(value);
			}
			return field + ":" + "[" + value + " TO *]" + weight();
		case WITHIN:
			String lowerValue = value.split(" ")[0];
			String upperValue = value.split(" ")[1];
			if (isDate(lowerValue)) {
				lowerValue = checkDate(lowerValue);
			}
			if (isDate(upperValue)) {
				upperValue = checkDate(upperValue);
			}
			return field + ":[" + lowerValue + " TO " + upperValue + "]" + weight();
		default:
			throw new RuntimeException("unable to serialize: " + toString());
		}
	}

	private String getFieldName() {
		return aliases.get(field) != null ? aliases.get(field) : field;
	}

	private String weight() {
		return (weights != null) && (weights.get(field) != null) ? "^" + weights.get(field) : "";
	}

	private String expandTokens(final Stream<String> tokens) {
		final String res = tokens
				.filter(s -> field.equals(dnetDefaultField.toLowerCase()))
				.map(s -> expand(s))
				.collect(Collectors.joining(""))
				.trim();
		return res;
	}

	private String expand(final String token) {
		String ret = "";
		if (!weights.keySet().contains(field)) {
			for (Entry<String, String> e : weights.entrySet()) {
				ret += e.getKey() + ":\"" + token + "\"^" + e.getValue() + " ";
			}
		}
		return ret.trim();
	}

	private String handleTokens(final Stream<String> tokens) {
		//final String separator = " " + op + " ";
		final String separator = " ";
		final String res = tokens
				.map(t -> t + weight())
				.collect(Collectors.joining(separator))
				.trim();
		return field + ":\"" + res + "\"";
	}

	private String checkEscaping(String token) {
		boolean isWildcard = token.contains("*") || token.contains("?");
		boolean isWildcardEnabled = ((options.get("wildcard") != null) && options.get("wildcard").contains("true")) || token.equals("*");

		if (!(isWildcard & isWildcardEnabled)) {
			token = QueryParserBase.escape(token);
		}
		return token;
	}

	private boolean isDate(final String aPossibleDate) {
		try {
			simpleDateFormat.parse(aPossibleDate);
		} catch (ParseException pe) {
			return false;
		}
		return true;
	}

	private String checkDate(final String date) {
		if (!date.endsWith("Z")) return date + "T00:00:00Z";
		return date;
	}

	public String getField() {
		return field;
	}

	public Relation getRel() {
		return rel;
	}

	public String getValue() {
		return value;
	}

}
