package at.ac.tuwien.kr.alpha.commons.programs.rules;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.collections4.SetUtils;

import at.ac.tuwien.kr.alpha.api.programs.literals.Literal;
import at.ac.tuwien.kr.alpha.api.programs.rules.Rule;
import at.ac.tuwien.kr.alpha.api.programs.rules.heads.Head;
import at.ac.tuwien.kr.alpha.commons.util.Util;

/**
 * An abstract representation of a rule with a specific type of @{link Head} (type parameter H)
 * 
 * @param <H> the type of head for this rule
 * 
 *            Copyright (c) 2017-2019, the Alpha Team.
 */
public abstract class AbstractRule<H extends Head> implements Rule<H> {

	private final H head;
	private final Set<Literal> bodyLiteralsPositive;
	private final Set<Literal> bodyLiteralsNegative;

	public AbstractRule(H head, List<Literal> body) {
		this.head = head;
		Set<Literal> positiveBody = new LinkedHashSet<>();
		Set<Literal> negativeBody = new LinkedHashSet<>();
		for (Literal bodyLiteral : body) {
			if (bodyLiteral.isNegated()) {
				negativeBody.add(bodyLiteral);
			} else {
				positiveBody.add(bodyLiteral);
			}
		}
		this.bodyLiteralsPositive = Collections.unmodifiableSet(positiveBody);
		this.bodyLiteralsNegative = Collections.unmodifiableSet(negativeBody);

		if (!this.isSafe()) {
			// TODO: safety check needs to be adapted to solver what the solver actually understands. Will change in the future,
			// adapt exception message accordingly.
			throw new RuntimeException("Encountered unsafe rule: " + toString() + System.lineSeparator()
					+ "Notice: A rule is considered safe if all variables occurring in negative literals, builtin atoms, and the head of the rule also occur in some positive literal.");
		}
	}

	/**
	 * Checks whether a rule is safe. The actual safety condition may vary over the next improvements. Currently, a rule is
	 * safe iff all negated variables and
	 * all variables occurring in the head also occur in the positive body).
	 * 
	 * @return true if this rule is safe.
	 */
	boolean isSafe() {
		// TODO: do the real check.
		// Note that - once a proper safety check is implemented - that check should probably be specific for each rule
		// implementation, therefore this method should be "protected abstract" here and implemented in each subclass.
		return true;
		/*
		 * Set<VariableTerm> positiveVariables = new HashSet<>(); Set<VariableTerm> builtinVariables = new HashSet<>();
		 * 
		 * // Check that all negative variables occur in the positive body. for (Literal literal : body) { // FIXME: The
		 * following five lines depend on concrete
		 * // implementations of the Atom interface. Not nice. if (literal instanceof BasicAtom) {
		 * positiveVariables.addAll(literal.getOccurringVariables()); }
		 * else if (literal instanceof BuiltinAtom) { builtinVariables.addAll(literal.getOccurringVariables()); } }
		 * 
		 * for (Atom negAtom : bodyAtomsNegative) { for (VariableTerm term : negAtom.getOccurringVariables()) { if
		 * (!positiveVariables.contains(term)) { return
		 * false; } } } for (VariableTerm builtinVariable : builtinVariables) { if
		 * (!positiveVariables.contains(builtinVariable)) { return false; } }
		 * 
		 * // Constraint are safe at this point if (isConstraint()) { return true; }
		 * 
		 * // Check that all variables of the head occur in the positive body. List<VariableTerm> headVariables =
		 * head.getOccurringVariables();
		 * headVariables.removeAll(positiveVariables); return headVariables.isEmpty();
		 */
	}

	@Override
	public boolean isConstraint() {
		return head == null;
	}

	@Override
	public String toString() {
		return Util.join((isConstraint() ? "" : head.toString() + " ") + ":- ", getBody(), ".");
	}

	@Override
	public H getHead() {
		return head;
	}

	@Override
	public Set<Literal> getBody() {
		return SetUtils.union(this.bodyLiteralsPositive, this.bodyLiteralsNegative);
	}

	@Override
	public Set<Literal> getPositiveBody() {
		return this.bodyLiteralsPositive;
	}

	@Override
	public Set<Literal> getNegativeBody() {
		return this.bodyLiteralsNegative;
	}

	@Override
	public int hashCode() {
		return Objects.hash(bodyLiteralsNegative, bodyLiteralsPositive, head);
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (!(obj instanceof AbstractRule)) {
			return false;
		}
		AbstractRule<?> other = (AbstractRule<?>) obj;
		return Objects.equals(this.bodyLiteralsNegative, other.bodyLiteralsNegative)
				&& Objects.equals(this.bodyLiteralsPositive, other.bodyLiteralsPositive)
				&& Objects.equals(this.head, other.head);
	}

}
