package com.taxonic.carml.engine.template;

import com.google.common.collect.ImmutableList;
import com.taxonic.carml.rdf_mapper.util.ImmutableCollectors;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.mutable.MutableInt;

class CarmlTemplate implements Template {

	abstract static class Segment {

		private String value;

		Segment(String value) {
			this.value = value;
		}

		public String getValue() {
			return value;
		}
	}

	static class Text extends Segment {

		Text(String value) {
			super(value);
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) return true;
			if (obj == null) return false;
			if (getClass() != obj.getClass()) return false;
			Text other = (Text) obj;
			if (getValue() == null) {
				if (other.getValue() != null) return false;
			}
			else if (!getValue().equals(other.getValue())) return false;
			return true;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((getValue() == null) ? 0 : getValue().hashCode());
			return result;
		}

		@Override
		public String toString() {
			return "Text [getValue()=" + getValue() + "]";
		}
	}

	static class ExpressionSegment extends Segment {

		private int id;

		ExpressionSegment(int id, String value) {
			super(value);
			this.id = id;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) return true;
			if (obj == null) return false;
			if (getClass() != obj.getClass()) return false;
			ExpressionSegment other = (ExpressionSegment) obj;
			if (id != other.id) return false;
			if (getValue() == null) {
				if (other.getValue() != null) return false;
			}
			else if (!getValue().equals(other.getValue())) return false;
			return true;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + id;
			result = prime * result + ((getValue() == null) ? 0 : getValue().hashCode());
			return result;
		}

		@Override
		public String toString() {
			return "ExpressionSegment [getValue()=" + getValue() + "]";
		}
	}

	private static class ExpressionImpl implements Expression {

		private int id;
		String value;

		ExpressionImpl(int id, String value) {
			this.id = id;
			this.value = value;
		}

		@Override
		public String getValue() {
			return value;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + id;
			result = prime * result + ((value == null) ? 0 : value.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) return true;
			if (obj == null) return false;
			if (getClass() != obj.getClass()) return false;
			ExpressionImpl other = (ExpressionImpl) obj;
			if (id != other.id) return false;
			if (value == null) {
				if (other.value != null) return false;
			}
			else if (!value.equals(other.value)) return false;
			return true;
		}

		@Override
		public String toString() {
			return "ExpressionImpl [id=" + id + ", value=" + value + "]";
		}
	}

	private class Builder implements Template.Builder {

		private Map<Expression, Function<Expression, Optional<Object>>> bindings = new LinkedHashMap<>();

		@Override
		public Template.Builder bind(Expression expression, Function<Expression, Optional<Object>> templateValue) {
			bindings.put(expression, templateValue);
			return this;
		}

		private Optional<Object> getExpressionValue(Expression expression) {
			if (!bindings.containsKey(expression))
				throw new RuntimeException("no binding present for expression [" + expression + "]");
			return bindings.get(expression).apply(expression);
		}

		private Optional<Object> getExpressionSegmentValue(ExpressionSegment segment) {
			Expression expression = expressionSegmentMap.get(segment);
			if (expression == null)
				throw new RuntimeException("no Expression instance present corresponding to segment " + segment); // (should never occur)
			return getExpressionValue(expression);
		}

		private void checkBindings() {
			if (!new LinkedHashSet<>(bindings.keySet()).equals(expressions))
				throw new RuntimeException("set of bindings [" + bindings.keySet() +
					"] does NOT match set of expressions in template [" + expressions + "]");
		}

		private List<String> getValuesExpressionEvaluation(Object evalResult) {
			if (evalResult instanceof Collection<?>) {
				return
						((Collection<?>) evalResult).stream()
							.map(v -> (String) v)
							.collect(ImmutableCollectors.toImmutableList());
			} else {
				return ImmutableList.of((String)evalResult);
			}
		}

		private boolean exprValueResultsHasOnlyFilledLists(Map<Segment, List<String>> indexedExprValues) {
			for (List<String> list : indexedExprValues.values()) {
				if (list.isEmpty())
					return false;
			}
			return true;
		}

		private int checkExprValueResultsOfEqualSizeAndReturn(Map<Segment, List<String>> indexedExprValues) {
			int size = -1;
			for (List<String> list : indexedExprValues.values()) {
				if (size == -1) {
					size = list.size();
				} else {
					if (list.size() != size) {
						throw new RuntimeException(
								String.format("Template expressions do not lead to an equal amount of values: %s",
										indexedExprValues.keySet()));
					}
				}
			}
			return size;
		}

		@Override
		public Optional<Object> create() {
			checkBindings();
			List<String> result = new ArrayList<>();
			Map<Segment, List<String>> indexedExprValues = new HashMap<>();

			// single out expression segments
			List<ExpressionSegment> exprSegs = segments.stream()
				.filter(s -> s instanceof ExpressionSegment)
				.map(s -> (ExpressionSegment) s)
				.collect(Collectors.toList());

			if (!exprSegs.isEmpty()) {

				// map segment to list of it's evaluation results
				for (ExpressionSegment exprSeg: exprSegs) {
					Optional<Object> evalResult = getExpressionSegmentValue(exprSeg);
					indexedExprValues.put(
							exprSeg,
							evalResult
								.map(this::getValuesExpressionEvaluation)
								.orElse(ImmutableList.of())
					);
				}

				// if there is an expression that doesn't result in a value,
				// the template should yield no result, following the RML rules.
				if (!exprValueResultsHasOnlyFilledLists(indexedExprValues)) {
					return Optional.empty();
				}

				// make sure that, if there are multiple segments, that they lead
				// an equal amount of values. If this is not the case, we cannot
				// know which values belong together over multiple segments.
				int indexedExprValSize = checkExprValueResultsOfEqualSizeAndReturn(indexedExprValues);
				for (int i = 0; i < indexedExprValSize; i++) {
					StringBuilder str = new StringBuilder();
					for (Segment s : segments) {
						if (s instanceof Text) {
							str.append(s.getValue());
						} else if (s instanceof ExpressionSegment) {
							String exprValue = indexedExprValues.get(s).get(i);
							if (exprValue == null) {
								result.add(null);
								continue;
							}
							str.append(exprValue);
						}
					}
					result.add(str.toString());
				}
			// if there are no expression segments, continue building value
			} else {
				StringBuilder str = new StringBuilder();
				segments.forEach(s -> str.append(s.getValue()));
				result.add(str.toString());
			}

			return Optional.of(result);
		}
	}

	private static Map<ExpressionSegment, Expression> createExpressionSegmentMap(List<Segment> segments) {
		MutableInt id = new MutableInt();
		return segments.stream()
			.filter(s -> s instanceof ExpressionSegment)
			.map(s -> (ExpressionSegment) s)
			.collect(Collectors.toMap(
				e -> e,
				e -> new ExpressionImpl(id.getAndIncrement(), e.getValue())
			));
	}

	static CarmlTemplate build(List<Segment> segments) {
		Map<ExpressionSegment, Expression> expressionSegmentMap = createExpressionSegmentMap(segments);
		Set<Expression> expressions = new LinkedHashSet<>(expressionSegmentMap.values());
		return new CarmlTemplate(
			segments,
			expressions,
			expressionSegmentMap
		);
	}

	private List<Segment> segments;
	private Set<Expression> expressions;
	private Map<ExpressionSegment, Expression> expressionSegmentMap;

	CarmlTemplate(
		List<Segment> segments,
		Set<Expression> expressions,
		Map<ExpressionSegment, Expression> expressionSegmentMap
	) {
		this.segments = segments;
		this.expressions = expressions;
		this.expressionSegmentMap = expressionSegmentMap;
	}

	@Override
	public Set<Expression> getExpressions() {
		return expressions;
	}

	@Override
	public Template.Builder newBuilder() {
		return new Builder();
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((expressionSegmentMap == null) ? 0 : expressionSegmentMap.hashCode());
		result = prime * result + ((expressions == null) ? 0 : expressions.hashCode());
		result = prime * result + ((segments == null) ? 0 : segments.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) return true;
		if (obj == null) return false;
		if (getClass() != obj.getClass()) return false;
		CarmlTemplate other = (CarmlTemplate) obj;
		if (expressionSegmentMap == null) {
			if (other.expressionSegmentMap != null) return false;
		}
		else if (!expressionSegmentMap.equals(other.expressionSegmentMap)) return false;
		if (expressions == null) {
			if (other.expressions != null) return false;
		}
		else if (!expressions.equals(other.expressions)) return false;
		if (segments == null) {
			if (other.segments != null) return false;
		}
		else if (!segments.equals(other.segments)) return false;
		return true;
	}

	@Override
	public String toString() {
		return "CarmlTemplate [segments=" + segments + ", expressions=" + expressions + ", expressionSegmentMap="
			+ expressionSegmentMap + "]";
	}

	@Override
	public String toTemplateString() {
		return segments.stream()
				.map(s -> {
					if (s instanceof ExpressionSegment) {
						return String.format("{%s}", s.getValue());
					}
					return s.getValue();
				})
				.collect(Collectors.joining());
	}

}
