package inference;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import exceptions.SubstituteFailedException;
import models.Position;
import models.formulas.DependencyFormula;
import models.formulas.Formula;
import models.formulas.meta.MetaEquationFormula;
import models.formulas.meta.MetaFormula;
import models.terms.DependencyTerm;
import models.terms.EvaluatableTerm;
import models.terms.RDLTerm;
import models.terms.Resource;
import models.terms.meta.MatchConstraint;
import models.terms.meta.MetaRDLTerm;

public class EquationAxiom extends InferenceRule {

	public EquationAxiom(String name, List<MetaFormula> assumptions, MetaEquationFormula conclusion, InferenceOrderConstraint constraint) {
		super(name, assumptions, conclusion, constraint);
	}
	
	public EquationAxiom(String name, List<MetaFormula> assumptions, MetaEquationFormula conclusion) {
		super(name, assumptions, conclusion);
	}
	
	protected EquationAxiom(String name) {
		super(name);
	}
	
	public Set<EvaluatableTerm> apply(List<Formula> assumptions, EvaluatableTerm term) {
		return apply(assumptions, term, MatchConstraint.createDefault());
	}
	
	public Set<EvaluatableTerm> apply(List<Formula>assumptions,  EvaluatableTerm term, MatchConstraint constraint) {
		Set<EvaluatableTerm> result = new HashSet<>();
		if (assumptions.size() < this.assumptions.size()) {
			return new HashSet<>();
		}
		
		Set<MatchConstraint> matchResult = assumptionMatch(assumptions, constraint);
		
		Set<MatchConstraint> conclusionLeftMatchResult = conclusionLeftSideHandMatch(term, matchResult);
		for (MatchConstraint leftConst: conclusionLeftMatchResult) {
			leftConst.getContext().put("isLeft", true);
		}
		Set<MatchConstraint> conclusionRightMatchResult = conclusionRightSideHandMatch(term, matchResult);
		for (MatchConstraint rightConst: conclusionRightMatchResult) {
			rightConst.getContext().put("isLeft", false);
		}
		Set<MatchConstraint> conclusionMatchResult = new HashSet<>();
		conclusionMatchResult.addAll(conclusionLeftMatchResult);
		conclusionMatchResult.addAll(conclusionRightMatchResult);
		
		MetaEquationFormula metaConclusion = (MetaEquationFormula) this.conclusion;
		for (MatchConstraint matchRes: conclusionMatchResult) {
			boolean isLeft =(Boolean) matchRes.getContext().get("isLeft");
			if (defaultOrderConstraint != null && ! defaultOrderConstraint.check(matchRes.getOrderConstraint())) {
				continue;
			}
			try {
				if (isLeft) {
					result.add((EvaluatableTerm)((MetaRDLTerm) metaConclusion.getRightSideHand()).substitute(matchRes.getBinding(), matchRes.getContext()));
				} else {
					result.add((EvaluatableTerm)((MetaRDLTerm) metaConclusion.getLeftSideHand()).substitute(matchRes.getBinding(), matchRes.getContext()));
				}
			} catch (SubstituteFailedException e) {
				continue;
			}
		}
		return result;
	}
	
	protected Set<MatchConstraint> assumptionMatch(List<Formula>assumptions, MatchConstraint constraint) {
		Set<MatchConstraint> matchResult = new HashSet<>();
		matchResult.add(constraint);
		for (int i = 0; i < assumptions.size(); i++) {
			Formula assumption = assumptions.get(i);
			MetaFormula metaAssumption = this.assumptions.get(i);
			matchResult = metaAssumption.isMatchedBy(assumption, matchResult);
			if (matchResult.isEmpty()) {
				return new HashSet<>();
			}
		}
		return matchResult;
	}
	
	protected Set<MatchConstraint> conclusionLeftSideHandMatch(EvaluatableTerm term, Set<MatchConstraint> matchResult) {
		MetaEquationFormula metaConclusion = (MetaEquationFormula) this.conclusion;
		return ((MetaRDLTerm) metaConclusion.getLeftSideHand()).isMatchedBy(term, matchResult);
	}
	
	protected Set<MatchConstraint> conclusionRightSideHandMatch(EvaluatableTerm term, Set<MatchConstraint> matchResult) {
		MetaEquationFormula metaConclusion = (MetaEquationFormula) this.conclusion;
		return ((MetaRDLTerm) metaConclusion.getRightSideHand()).isMatchedBy(term, matchResult);
	}
	
	
	protected static Set<DependencyFormula> requiredAssumptions(EvaluatableTerm term) {
		if (term instanceof Resource) {
			return new HashSet<>();
		}
		DependencyTerm depTerm = (DependencyTerm) term;
		Set<DependencyFormula> result = new HashSet<>();
		EvaluatableTerm dependingTerm = depTerm.getDependingTerm();
		List<EvaluatableTerm> dependedTerms = depTerm.getDependedTerms();
		List<EvaluatableTerm> argumentTerms = depTerm.getArgumentTerms();
		if (dependingTerm instanceof Resource) {
			result.add(new DependencyFormula(dependingTerm, dependedTerms));
		} else if (dependingTerm instanceof DependencyTerm depending) {
			for (Formula formula : requiredAssumptions(depending)) {
				if (formula instanceof DependencyFormula dependency) {
					DependencyTerm newDependingTerm = new DependencyTerm((EvaluatableTerm) dependency.getDependency().getDependingTerm(), dependedTerms, argumentTerms);
					List<EvaluatableTerm> newDependedTerms = new ArrayList<>();
					for (RDLTerm dependedTerm : dependency.getDependency().getDependedTerms()) {
						newDependedTerms.add(new DependencyTerm((EvaluatableTerm) dependedTerm, dependedTerms, argumentTerms));
					}
					result.add(new DependencyFormula(newDependingTerm, newDependedTerms));
				} 
			}
		}
		return result;
	}
	
	protected static boolean conclusionCheck(EvaluatableTerm leftSideHand, Set<Formula> formulas) {
		for (Formula formula: requiredAssumptions(leftSideHand)) {
			if (! formulas.contains(formula)) { 
				return false;
			}
		}
		return true;
	}
	
	protected static Map<MatchConstraint, Map<Position, Integer>> savePositions(Set<MatchConstraint> matchConstraint) {
		Map<MatchConstraint, Map<Position, Integer>> result = new HashMap<>();
		for (MatchConstraint constraint : matchConstraint) {
			Deque<Position> posDeque = new ArrayDeque<>();
			posDeque.add(new Position());
			Map<Position, Integer> posMap = new HashMap<>();
			while (posDeque.size() != 0) {
				Position pos = posDeque.pollFirst();
				if (! constraint.getContext().containsKey(pos)) {
					continue;
				}
				int i = 0;
				while (true) {
					Position nextPos = pos.addPath(i);
					if (! constraint.getContext().containsKey(nextPos)) {
						break;
					}
					posMap.put(pos, (Integer) constraint.getContext().get(pos));
					posDeque.add(nextPos);
				}
			}
			result.put(constraint, posMap);
		}
		return result;
	}
	
}
