/**
 * 
 */
package xhail.core.entities;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections4.iterators.ArrayIterator;

import xhail.core.Buildable;
import xhail.core.Config;
import xhail.core.parser.Parser;
import xhail.core.statements.Display;
import xhail.core.statements.Example;
import xhail.core.statements.ModeB;
import xhail.core.statements.ModeH;
import xhail.core.terms.Atom;
import xhail.core.terms.Clause;
import xhail.core.terms.Literal;
import xhail.core.terms.Number;
import xhail.core.terms.Variable;

/**
 * @author stefano
 *
 */
public class Hypothesis implements Iterable<Atom> {

	public static class Builder implements Buildable<Hypothesis> {

		private Set<Literal> covered = new HashSet<>();
		private Set<Atom> facts = new HashSet<>();
		private Grounding grounding;
		private Set<Atom> literals = new HashSet<>();
		private Set<Atom> model = new HashSet<>();

		private Set<Literal> uncovered = new HashSet<>();

		public Builder(Grounding grounding) {
			if (null == grounding)
				throw new IllegalArgumentException("Illegal 'grounding' argument in Hypothesis.Builder(Grounding): " + grounding);
			this.grounding = grounding;
		}

		public Builder addAtom(Atom atom) {
			if (null == atom)
				throw new IllegalArgumentException("Illegal 'atom' argument in Hypothesis.Builder.addAtom(Atom): " + atom);
			if (atom.getIdentifier().equals("use_clause_literal") && 2 == atom.getArity()) {
				literals.add(atom);
			} else {
				if (grounding.getConfig().isFull() && grounding.hasDisplays() && grounding.lookup(atom))
					model.add(atom);
				facts.add(atom);
			}
			return this;
		}

		public Builder addAtoms(Collection<Atom> atoms) {
			if (null == atoms)
				throw new IllegalArgumentException("Illegal 'indAtoms' argument in Hypothesis.Builder.addAtoms(Collection<Atom>): " + atoms);
			for (Atom atom : atoms)
				addAtom(atom);
			return this;
		}

		@Override
		public Hypothesis build() {
			covered.clear();
			uncovered.clear();
			for (Example example : grounding.getExamples()) {
				Atom atom = example.getAtom();
				if (example.isNegated() != facts.contains(atom))
					covered.add(new Literal.Builder(atom).setNegated(example.isNegated()).build());
				else
					uncovered.add(new Literal.Builder(atom).setNegated(example.isNegated()).build());
			}
			return new Hypothesis(this);
		}

		public Builder clear() {
			this.model.clear();
			this.covered.clear();
			this.literals.clear();
			return this;
		}

		public Builder parse(Collection<String> answer) {
			if (null == answer)
				throw new IllegalArgumentException("Illegal 'answer' argument in Hypothesis.Builder.parse(Collection<String>): " + answer);
			for (String atom : answer)
				addAtom(Parser.parseToken(atom));
			return this;
		}

		public Builder removeAtom(Atom atom) {
			if (null == atom)
				throw new IllegalArgumentException("Illegal 'atom' argument in Hypothesis.Builder.removeAtom(Atom): " + atom);
			if (atom.getIdentifier().equals("use_clause_literal") && 2 == atom.getArity()) {
				literals.remove(atom);
			} else {
				if (grounding.getConfig().isFull() && grounding.hasDisplays() && grounding.lookup(atom))
					model.remove(atom);
				facts.remove(atom);
			}
			return this;
		}

		public Builder removeAtoms(Collection<Atom> atoms) {
			if (null == atoms)
				throw new IllegalArgumentException("Illegal 'indAtoms' argument in Hypothesis.Builder.removeAtoms(Collection<Atom>): " + atoms);
			for (Atom atom : atoms)
				removeAtom(atom);
			return this;
		}

	}

	private final Literal[] covered;

	private final Grounding grounding;

	private Clause[] hypotheses;

	private final Atom[] literals;

	private final Atom[] model;

	private final Literal[] uncovered;

	private Hypothesis(Builder builder) {
		if (null == builder)
			throw new IllegalArgumentException("Illegal 'builder' argument in Hypothesis(Hypothesis.Builder): " + builder);
		this.covered = builder.covered.toArray(new Literal[builder.covered.size()]);
		this.grounding = builder.grounding;
		this.literals = builder.literals.toArray(new Atom[builder.literals.size()]);
		this.model = builder.model.toArray(new Atom[builder.model.size()]);
		this.uncovered = builder.uncovered.toArray(new Literal[builder.uncovered.size()]);
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Hypothesis other = (Hypothesis) obj;
		if (!Arrays.equals(hypotheses, other.hypotheses))
			return false;
		if (!Arrays.equals(covered, other.covered))
			return false;
		if (grounding == null) {
			if (other.grounding != null)
				return false;
		} else if (!grounding.equals(other.grounding))
			return false;
		if (!Arrays.equals(literals, other.literals))
			return false;
		if (!Arrays.equals(model, other.model))
			return false;
		if (!Arrays.equals(uncovered, other.uncovered))
			return false;
		return true;
	}

	public final String[] getBackground() {
		return grounding.getBackground();
	}

	public final Config getConfig() {
		return grounding.getConfig();
	}

	public final Literal[] getCovered() {
		return covered;
	}

	public final Atom[] getDelta() {
		return grounding.getDelta();
	}

	public final Display[] getDisplays() {
		return grounding.getDisplays();
	}

	public final String[] getDomains() {
		return grounding.getDomains();
	}

	public final Example[] getExamples() {
		return grounding.getExamples();
	}

	public final Clause[] getGeneralisation() {
		return grounding.getGeneralisation();
	}

	public Grounding getGrounding() {
		return grounding;
	}

	public final Clause[] getHypotheses() {
		if (null == hypotheses) {
			Set<Clause> set = new HashSet<>();
			Clause[] generalisation = grounding.getGeneralisation();
			Map<Integer, Clause.Builder> builders = new HashMap<>();
			Map<Integer, Set<Literal>> types = new HashMap<>();
			for (Atom atom : literals) {
				int clauseId = ((Number) atom.getTerm(0)).getValue();
				int literalId = ((Number) atom.getTerm(1)).getValue();
				if (0 == literalId && 0 <= clauseId && clauseId < generalisation.length) {
					builders.put(clauseId, new Clause.Builder().setHead(generalisation[clauseId].getHead()));
					types.put(clauseId, new LinkedHashSet<>());
				}
			}
			for (Atom atom : literals) {
				int clauseId = ((Number) atom.getTerm(0)).getValue();
				int literalId = ((Number) atom.getTerm(1)).getValue();
				if (literalId > 0 && 0 <= clauseId && clauseId < generalisation.length) {

					Literal literal = generalisation[clauseId].getBody(literalId);
					builders.get(clauseId).addLiteral(literal);
					Set<Literal> literals = types.get(clauseId);
					for (Variable variable : literal.getVariables())
						literals.add(new Literal.Builder( //
								new Atom.Builder(variable.getType().getIdentifier()).addTerm(variable).build() //
						).build());
				}
			}
			for (int c : builders.keySet()) {
				Clause.Builder builder = builders.get(c);
				for (Literal literal : types.get(c))
					builder.addLiteral(literal);
				set.add(builder.build());
			}
			hypotheses = set.toArray(new Clause[set.size()]);
		}
		return hypotheses;
	}

	public final Clause[] getKernel() {
		return grounding.getKernel();
	}

	public final ModeB[] getModeBs() {
		return grounding.getModeBs();
	}

	public final ModeH[] getModeHs() {
		return grounding.getModeHs();
	}

	public final Atom[] getModel() {
		return model;
	}

	public final Problem getProblem() {
		return grounding.getProblem();
	}

	public final Literal[] getUncovered() {
		return uncovered;
	}

	public final boolean hasBackground() {
		return grounding.hasBackground();
	}

	public final boolean hasCovered() {
		return covered.length > 0;
	}

	public final boolean hasDelta() {
		return grounding.hasDelta();
	}

	public final boolean hasDisplays() {
		return grounding.hasDisplays();
	}

	public final boolean hasDomains() {
		return grounding.getDomains().length > 0;
	}

	public final boolean hasExamples() {
		return grounding.hasExamples();
	}

	public final boolean hasGeneralisation() {
		return grounding.hasGeneralisation();
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + Arrays.hashCode(hypotheses);
		result = prime * result + Arrays.hashCode(covered);
		result = prime * result + ((grounding == null) ? 0 : grounding.hashCode());
		result = prime * result + Arrays.hashCode(literals);
		result = prime * result + Arrays.hashCode(model);
		result = prime * result + Arrays.hashCode(uncovered);
		return result;
	}

	public final boolean hasHypotheses() {
		return getHypotheses().length > 0;
	}

	public final boolean hasKernel() {
		return grounding.hasKernel();
	}

	public final boolean hasModel() {
		return model.length > 0;
	}

	public final boolean hasModes() {
		return grounding.hasModes();
	}

	public final boolean hasUncovered() {
		return uncovered.length > 0;
	}

	@Override
	public Iterator<Atom> iterator() {
		return new ArrayIterator<>(literals);
	}

	@Override
	public String toString() {
		return "Hypothesis [\n  hypotheses=" + Arrays.toString(hypotheses) + ",\n  covered=" + Arrays.toString(covered) + ",\n  grounding=" + grounding
				+ ",\n  literals=" + Arrays.toString(literals) + ",\n  model=" + Arrays.toString(model) + ",\n  uncovered=" + Arrays.toString(uncovered)
				+ "\n]";
	}

}