package models.terms.meta;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import exceptions.IllegalTypeException;
import exceptions.SubstituteFailedException;
import exceptions.SyntaxException;
import lombok.Getter;
import models.algebra.Expression;
import models.algebra.Symbol;
import models.algebra.Variable;
import models.terms.Dependency;
import models.terms.DependencyTerm;
import models.terms.EvaluatableTerm;
import models.terms.LinearRightNormalizedType;
import models.terms.RDLTerm;
import models.terms.Resource;
import models.terms.meta.MetaDependencyTermGenerator.TermPair;
import utils.Permutation;

public class MetaRDLTerm extends RDLTerm {

	@Getter
	protected TermType termType;
	@Getter
	protected LinearRightNormalizedType linearRightNormalizedType = LinearRightNormalizedType.UNDEFINED;
	
	private boolean isDynamic = false;
	private MetaDependencyTermGenerator metaDependencyTermGenerator;
	private MetaDependencyGenerator metaDependencyGenerator;
	
	protected MetaRDLTerm(Symbol symbol, TermType termType, int size) {
		super(symbol, -1, size);
		this.termType = termType;
		if (isResourceVariable()) {
			linearRightNormalizedType = LinearRightNormalizedType.LINEAR_RIGHT_NORMALIZED;
		}
	}
	
	//dependency
	public MetaRDLTerm(MetaRDLTerm dependingTerm, Set<MetaRDLTerm> dependedTerms) {
		super(new Symbol(":", 1 + dependedTerms.size()), -1, -1);
		int size = dependingTerm.getSize();
		addChild(dependingTerm);
		for (MetaRDLTerm dependedTerm: new TreeSet<>(dependedTerms)) {
			addChild(dependedTerm);
			size += dependedTerm.getSize();
		}
		this.size = size;
		this.termType = TermType.META_DEPENDENCY;
		this.metaDependencyGenerator = (i, j) -> (RDLTerm) this.getChild(i + 1);
	}
	
	public MetaRDLTerm(MetaRDLTerm dependingTerm, MetaRDLTerm dependedTerm) {
		this(dependingTerm, new TreeSet<>(Set.of(dependedTerm)));
	}
	
	public MetaRDLTerm(MetaRDLTerm dependingTerm, MetaDependencyGenerator generator) {
		super(new Symbol(":", 1), -1, -1);
		addChild(dependingTerm);
		this.metaDependencyGenerator = generator;
		this.termType = TermType.META_DEPENDENCY;
		this.isDynamic = true;
	}
	
	//dependency term
	public MetaRDLTerm(MetaRDLTerm dependingTerm, List<MetaRDLTerm> terms) {
		super(new Symbol(":", terms.size() + 1), -1, -1);
		if (! EvaluatableTerm.class.isAssignableFrom(dependingTerm.getTermType().getBaseTermClass())) {
			throw new IllegalTypeException();
		}
		if (terms.size() % 2 != 0) {
			throw new SyntaxException("");
		}
		int size = dependingTerm.getSize();
		addChild(dependingTerm);
		TreeMap<MetaRDLTerm, MetaRDLTerm> sortedTerms = new TreeMap<>();
		for (int i = 0; i < terms.size() / 2; i++) {
			MetaRDLTerm dependedTerm = terms.get(2 * i);
			MetaRDLTerm argTerm = terms.get(2 * i + 1);
			size += dependedTerm.getSize();
			size += argTerm.getSize();
			if (! EvaluatableTerm.class.isAssignableFrom(dependedTerm.getTermType().getBaseTermClass())) {
				throw new IllegalTypeException();
			}
			if (! EvaluatableTerm.class.isAssignableFrom(argTerm.getTermType().getBaseTermClass())) {
				throw new IllegalTypeException();
			}
			sortedTerms.put(dependedTerm, argTerm);
		}
		this.size = size;
		for (MetaRDLTerm dependedTerm: sortedTerms.keySet()) {
			MetaRDLTerm argTerm = sortedTerms.get(dependedTerm);
			addChild(dependedTerm);
			addChild(argTerm);
 		}
		this.termType = TermType.META_DEPENDENCY_TERM;
		this.metaDependencyTermGenerator = (i, j) -> new TermPair((RDLTerm) this.getChild(i * 2 + 1), (RDLTerm) this.getChild(i * 2 + 2));
	}
	
	public MetaRDLTerm(MetaRDLTerm dependingTerm, MetaRDLTerm ...terms) {
		this(dependingTerm, Arrays.asList(terms));
	}
	
	public MetaRDLTerm(MetaRDLTerm dependingTerm, MetaDependencyTermGenerator generator) {
		super(new Symbol(":", 1), -1, -1);
		addChild(dependingTerm);
		this.metaDependencyTermGenerator = generator;
		this.termType = TermType.META_DEPENDENCY_TERM;
		this.isDynamic = true;
	}

	public RDLTerm substitute(Map<Variable, RDLTerm> binding) {
		if (isDependency()) {
			RDLTerm dependingTerm = (RDLTerm) getChild(0);
			RDLTerm dependedVariable = (RDLTerm) getChild(1);
			if (dependingTerm instanceof MetaRDLTerm) {
				dependingTerm = ((MetaRDLTerm) dependingTerm).substitute(binding);
			}
			if (dependedVariable instanceof MetaRDLTerm) {
				dependedVariable = ((MetaRDLTerm) dependedVariable).substitute(binding);
			}
			if (dependedVariable instanceof Resource) {
				return new Dependency(dependingTerm, (Resource) dependedVariable);
			}
		}
		else if (isDependencyTerm()) {
			RDLTerm dependingTerm = (RDLTerm) getChild(0);
			if (dependingTerm instanceof MetaRDLTerm metaDependingTerm) {
				dependingTerm = metaDependingTerm.substitute(binding);
			}
			List<EvaluatableTerm> terms = new ArrayList<>();
			for (int i = 1; i < getArity(); i++) {
				RDLTerm term = (RDLTerm) getChild(i);
				if (term instanceof MetaRDLTerm metaTerm) {
					term = metaTerm.substitute(binding);
				}
				terms.add((EvaluatableTerm) term);
			}
			return new DependencyTerm((EvaluatableTerm) dependingTerm, terms);
		}
		throw new SubstituteFailedException();
	}
	
	public boolean isVariable() {
		return false;
	}
	
	public Set<MatchConstraint> isMatchedBy(RDLTerm another) {
		return isMatchedBy(another, new MatchConstraint(new HashMap<>(), new HashMap<>()));
	}
	
	public Set<MatchConstraint> isMatchedBy(RDLTerm another, Set<MatchConstraint> constraints) {
		Set<MatchConstraint> result = new HashSet<>();
		for (MatchConstraint constraint : constraints) {
			result.addAll(isMatchedBy(another, constraint));
		}
		return result;
	}
	
	public Set<MatchConstraint> isMatchedBy(RDLTerm another, MatchConstraint constraint) {
		Set<MatchConstraint> result = new HashSet<>();
		if (! another.getClass().isAssignableFrom(this.termType.getBaseTermClass())) {
			return result;
		}
		if (!isDynamic && this.getChildren().size() != another.getChildren().size()) {
			return result;
		}
		if (isDependencyTerm() && (! islinearRightNormalizedMatchedBy(another))) {
			return result;
		}
		RDLTerm dependingChild = (RDLTerm) this.getChild(0);
		RDLTerm anotherDependingChild = (RDLTerm) another.getChild(0);
		Set<MatchConstraint> res = new HashSet<>();
		if (dependingChild instanceof MetaRDLTerm) {
			MetaRDLTerm metaChild = (MetaRDLTerm) dependingChild;
			res = metaChild.isMatchedBy(anotherDependingChild, constraint);
		} else {
			if (!(dependingChild.equals(anotherDependingChild))) {
				return result;
			}
		}
		if (isDependencyTerm() && !isVariable()) {
			for (List<Integer> perm : Permutation.permutation((another.getChildren().size() - 1) / 2)) {
				Set<MatchConstraint> res2 = new HashSet<>(res);
				boolean flg = true;
				for (int i = 0; i < perm.size(); i++) {
					int j = perm.get(i);
					TermPair termPair = this.metaDependencyTermGenerator.generate(j, perm.size());
					RDLTerm dependedChild = termPair.dependedTerm();
					RDLTerm argChild = termPair.argTerm();
					RDLTerm anotherDependedChild = (RDLTerm) another.getChild(i * 2 + 1);
					RDLTerm anotherArgChild = (RDLTerm) another.getChild(i * 2 + 2);
					if (dependedChild instanceof MetaRDLTerm) {
						MetaRDLTerm metaChild = (MetaRDLTerm) dependedChild;
						res2 = metaChild.isMatchedBy(anotherDependedChild, res2);
						if (res2.isEmpty()) {
							flg = false;
							break;
						}
					} else {
						if (!(dependedChild.equals(anotherDependedChild))) {
							flg = false;
							break;
						}
					}
					if (argChild instanceof MetaRDLTerm) {
						MetaRDLTerm metaChild = (MetaRDLTerm) argChild;
						res2 = metaChild.isMatchedBy(anotherArgChild, res2);
						if (res2.isEmpty()) {
							flg = false;
							break;
						}
					} else {
						if (!(argChild.equals(anotherArgChild))) {
							flg = false;
							break;
						}
					}
				}
				if (flg) {
					result.addAll(res2);
				}
			}
			return result;
		} else if (isDependency() && !isVariable()) {
			for (List<Integer> perm : Permutation.permutation(another.getChildren().size() - 1)) {
				Set<MatchConstraint> res2 = new HashSet<>(res);
				boolean flg = true;
				for (int i = 0; i < perm.size(); i++) {
					int j = perm.get(i);
					RDLTerm dependedChild = this.metaDependencyGenerator.generate(j, perm.size());
					RDLTerm anotherDependedChild = (RDLTerm) another.getChild(i + 1);
					if (dependedChild instanceof MetaRDLTerm) {
						MetaRDLTerm metaChild = (MetaRDLTerm) dependedChild;
						res2 = metaChild.isMatchedBy(anotherDependedChild, res2);
						if (res2.isEmpty()) {
							flg = false;
							break;
						}
					} else {
						if (!(dependedChild.equals(anotherDependedChild))) {
							flg = false;
							break;
						}
					}
				}
				if (flg) {
					result.addAll(res2);
				}
			}
			return result;
		}
		return result;
	}
	
	public boolean checkTermType(Class<? extends RDLTerm> clazz) {
		return clazz.isAssignableFrom(termType.getBaseTermClass());
	}
	
	public boolean isDependency() {
		return checkTermType(Dependency.class);
	}
	
	public boolean isEvaluatableTerm() {
		return checkTermType(EvaluatableTerm.class);
	}
	
	public boolean isDependencyTerm() {
		return checkTermType(DependencyTerm.class);
	}
	
	public boolean isResourceVariable() {
		return checkTermType(Resource.class);
	}
	
	public void setLinearRightNormalizedType(LinearRightNormalizedType next) {
		if (isDependency()) {
			return;
		}
		this.linearRightNormalizedType = next;
	}
	
	public Collection<MetaVariable> getAllVariables() {
		return getSubTerms(MetaVariable.class).values();
	}
	
	protected boolean islinearRightNormalizedMatchedBy(RDLTerm another) {
		if (linearRightNormalizedType != LinearRightNormalizedType.UNDEFINED) {
			if (another instanceof DependencyTerm) {
				if (((DependencyTerm) another).getLinearRightNormalizedType() != linearRightNormalizedType) {
					return false;
				}
			}
			else if (another instanceof MetaRDLTerm) {
				if (((MetaRDLTerm) another).getLinearRightNormalizedType() != linearRightNormalizedType) {
					return false;
				}
			}
		}
		return true;
	}
	
	@Override
	public String toString() {
		switch(termType) {
		case META_DEPENDENCY:
			return "[" + getChild(0).toString() + " : " + getChildren().stream().skip(1).map(Expression::toString).collect(Collectors.joining(",")) + "]";
		case META_DEPENDENCY_TERM:
			return "[" + getChild(0).toString() + " : " + IntStream.range(0, (getChildren().size() - 1) / 2)
					.mapToObj(i -> getChild(i * 2 + 1).toString() + " -> " + getChild(i * 2 + 2)).collect(Collectors.joining(",")) + "]";
		default:
			return "";
		}
	}

	@Override
	public String toStringWithOrder() {
		switch(termType) {
		case META_DEPENDENCY:
			return "[" + ((RDLTerm) getChild(0)).toStringWithOrder() + " : " + ((RDLTerm) getChild(1)).toStringWithOrder() + "]";
		case META_DEPENDENCY_LIST:
			return "[" + ((RDLTerm) getChild(0)).toStringWithOrder() + "]";
		case META_DEPENDENCY_TERM:
			return "[" + ((RDLTerm) getChild(0)).toStringWithOrder() + " : "
					+ ((RDLTerm) getChild(1)).toStringWithOrder() + " -> " + ((RDLTerm) getChild(2)).toStringWithOrder() + "]";
		default:
			return "";
		}
	}

	@Override
	public boolean equals(Object another) {
		if(! (another instanceof MetaRDLTerm)) {
			return false;
		}
		MetaRDLTerm anotherTerm = (MetaRDLTerm) another;
		return super.equals(another) && termType == anotherTerm.getTermType();
	}

	@Override
	public int hashCode() {
		return (termType.toString() + toStringWithOrder()).hashCode();
	}

	@Override
	public Object clone() {

		return null;
	}

	protected static enum TermType {
		META_RDL_TERM(RDLTerm.class),
		META_DEPENDENCY(Dependency.class),
		META_DEPENDENCY_LIST(Dependency.class),
		META_DEPENDENCY_VARIABLE(Dependency.class),
		META_DEPENDENCY_TERM(DependencyTerm.class),
		META_DEPENDENCY_TERM_VARIABLE(DependencyTerm.class),
		META_EVALUATABLE_TERM_VARIABLE(EvaluatableTerm.class),
		META_RESOURCE_VARIABLE(Resource.class);
		
		@Getter
		private Class<?> baseTermClass;
		
		private TermType(Class<?> clazz) {
			this.baseTermClass = clazz;
		}
		
	}
	
}
