/**
 * 
 */
package xhail.core.terms;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

import xhail.core.Buildable;

/**
 * @author stefano
 *
 */
public class Scheme implements SchemeTerm, Iterable<Scheme> {

	public static class Builder implements Buildable<Scheme> {

		private String identifier;

		private boolean negated = false;

		private List<SchemeTerm> terms = new ArrayList<>();

		public Builder(String identifier) {
			if (null == identifier || (identifier = identifier.trim()).isEmpty() || identifier.charAt(0) < 'a' || identifier.charAt(0) > 'z')
				throw new IllegalArgumentException("Illegal 'identifier' argument in Scheme.Builder(String): " + identifier);
			this.identifier = identifier;
		}

		public Builder addTerm(SchemeTerm term) {
			if (null == term)
				throw new IllegalArgumentException("Illegal 'term' argument in Scheme.Builder.addTerm(SchemeTerm): " + term);
			this.terms.add(term);
			return this;
		}

		public Builder addTerms(Collection<SchemeTerm> terms) {
			if (null == terms)
				throw new IllegalArgumentException("Illegal 'terms' argument in Scheme.Builder.addTerms(Collection<SchemeTerm>): " + terms);
			this.terms.addAll(terms);
			return this;
		}

		@Override
		public Scheme build() {
			return new Scheme(this);
		}

		public Builder clearTerms() {
			this.terms.clear();
			return this;
		}

		public Builder removeTerm(SchemeTerm term) {
			if (null == term)
				throw new IllegalArgumentException("Illegal 'term' argument in Scheme.Builder.removeTerm(SchemeTerm): " + term);
			this.terms.remove(term);
			return this;
		}

		public Builder removeTerms(Collection<SchemeTerm> terms) {
			if (null == terms)
				throw new IllegalArgumentException("Illegal 'terms' argument in Scheme.Builder.removeTerms(Collection<SchemeTerm>): " + terms);
			this.terms.removeAll(terms);
			return this;
		}

		public Builder setIdentifier(String identifier) {
			if (null == identifier || (identifier = identifier.trim()).isEmpty() || identifier.charAt(0) < 'a' || identifier.charAt(0) > 'z')
				throw new IllegalArgumentException("Illegal 'identifier' argument in Scheme.Builder.setIdentifier(String): " + identifier);
			this.identifier = identifier;
			return this;
		}

		public Builder setNegated(boolean negated) {
			this.negated = negated;
			return this;
		}

	}

	private final String identifier;

	private final boolean negated;

	private final SchemeTerm[] terms;

	private Scheme(Builder builder) {
		if (null == builder)
			throw new IllegalArgumentException("Illegal 'builder' argument in Scheme(Scheme.Builder): " + builder);
		this.identifier = builder.identifier;
		this.negated = builder.negated;
		this.terms = builder.terms.toArray(new SchemeTerm[builder.terms.size()]);
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Scheme other = (Scheme) obj;
		if (identifier == null) {
			if (other.identifier != null)
				return false;
		} else if (!identifier.equals(other.identifier))
			return false;
		if (negated != other.negated)
			return false;
		if (!Arrays.equals(terms, other.terms))
			return false;
		return true;
	}

	@Override
	public Term generalises(Set<Variable> set) {
		if (null == set)
			throw new IllegalArgumentException("Illegal 'set' argument in Scheme.generalises(Set<Variable>): " + set);
		Atom.Builder builder = new Atom.Builder(identifier).setScheme(this);
		for (int i = 0; i < terms.length; i++) {
			Term nested = terms[i].generalises(set);
			if (null == nested)
				return null;
			builder.addTerm(nested);
		}
		return builder.build();
	}

	@Override
	public Term generalises(Term term, Map<Term, Variable> map) {
		if (null == term)
			throw new IllegalArgumentException("Illegal 'term' argument in Scheme.generalises(Term, Map<Term, Variable>): " + term);
		if (null == map)
			throw new IllegalArgumentException("Illegal 'map' argument in Scheme.generalises(Term, Map<Term, Variable>): " + map);
		if (term instanceof Atom) {
			Atom other = (Atom) term;
			if (other.getIdentifier().equals(identifier) && other.getArity() == terms.length) {
				Atom.Builder builder = new Atom.Builder(other).clearTerms();
				for (int i = 0; i < terms.length; i++) {
					Term nested = terms[i].generalises(other.getTerm(i), map);
					if (null == nested)
						return null;
					builder.addTerm(nested);
				}
				return builder.build();
			} else
				return null;
		} else
			return null;
	}

	public final int getArity() {
		return terms.length;
	}

	public final String getIdentifier() {
		return identifier;
	}

	public final SchemeTerm getTerm(int index) {
		if (index < 0 || index >= terms.length)
			throw new IndexOutOfBoundsException("Illegal 'index' argument in Scheme.getTerm(int): " + index);
		return terms[index];
	}

	public final SchemeTerm[] getTerms() {
		return terms;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((identifier == null) ? 0 : identifier.hashCode());
		result = prime * result + (negated ? 1231 : 1237);
		result = prime * result + Arrays.hashCode(terms);
		return result;
	}

	public final boolean isNegated() {
		return negated;
	}

	public boolean isPlacemarker() {
		return (Placemarker.CONSTANT_STRING.equals(identifier) || Placemarker.INPUT_STRING.equals(identifier) || Placemarker.OUTPUT_STRING.equals(identifier))
				&& (1 == terms.length || 2 == terms.length);
	}

	@Override
	public Iterator<Scheme> iterator() {
		return new ArrayIterator<>(terms);
	}

//	@Override
//	public Map<Term, Collection<Atom>> matching(Set<Term> usables, Map<SchemeTerm, Set<Atom>> parts) {
//		Map<Atom.Builder, Set<Term>> builders = new HashMap<>();
//		builders.put(new Atom.Builder(identifier), new HashSet<>());
//		for (int i = 0; i < terms.length; i++) {
//			Map<Term, Collection<Atom>> nested = terms[i].matching(usables, parts);
//			if (null == nested)
//				return null;
//			Map<Atom.Builder, Set<Term>> step = new HashMap<>();
//			for (Term term : nested.keySet()) {
//				for (Atom.Builder builder : builders.keySet()) {
//					Atom.Builder key = builder.clone().addTerm(term);
//					Set<Term> value = new HashSet<>(builders.get(builder));
//					value.addAll(nested.get(term));
//					step.put(key, value);
//				}
//			}
//			builders = step;
//		}
//		Map<Term, Collection<Atom>> result = new HashMap<>();
//		for (Atom.Builder builder : builders.keySet())
//			result.put(builder.build(), builders.get(builder));
//		return result;
//	}

	@Override
	public String toString() {
		String result = "";
		if (negated)
			result += "not ";
		result += identifier;
		if (terms.length > 0)
			result += "(" + StringUtils.join(terms, ",") + ")";
		return result;
	}

	// getTypes

	// getTypesWithoutConstantPlaceMarkers

	// getPlacemarkers

	// getPlacemarkersWithoutConstantPlacemarkers

	// getVariables

	// getVariablesWithoutConstantPlacemarkers

	private final void getPlacemarkers(Set<Placemarker> result) {
		if (null == result)
			throw new IllegalArgumentException("Illegal 'result' argument in Scheme.getPlacemarkers(Set<Placemarker>): " + result);
		for (SchemeTerm term : terms)
			if (term instanceof Placemarker)
				result.add((Placemarker) term);
			else if (term instanceof Scheme)
				((Scheme) term).getPlacemarkers(result);
	}

	private Placemarker[] placemarkers;

	public final boolean hasPlacemarkers() {
		return getPlacemarkers().length > 0;
	}

	public final Placemarker[] getPlacemarkers() {
		if (null == placemarkers) {
			Set<Placemarker> result = new LinkedHashSet<>();
			getPlacemarkers(result);
			placemarkers = result.toArray(new Placemarker[result.size()]);
		}
		return placemarkers;
	}

	public final boolean hasTypes() {
		return getPlacemarkers().length > 0;
	}

	public final String[] getTypes() {
		int length = getPlacemarkers().length;
		String[] result = new String[length];
		for (int i = 0; i < length; i++)
			result[i] = String.format("%s(V%d)", placemarkers[i].getIdentifier(), 1 + i);
		return result;
	}

	public final String[] getVariables() {
		int length = getPlacemarkers().length;
		String[] result = new String[length];
		for (int i = 0; i < length; i++)
			result[i] = String.format("V%d", 1 + i);
		return result;
	}

	public final boolean matches(Term candidate) {
		if (null == candidate)
			throw new IllegalArgumentException("Illegal 'candidate' argument in Scheme.matches(Term): " + candidate);
		if (!(candidate instanceof Atom))
			return false;
		Atom atom = (Atom) candidate;
		if (!atom.getIdentifier().equals(identifier) || atom.getArity() != terms.length)
			return false;
		boolean result = true;
		for (int i = 0; result && i < terms.length; i++)
			if (terms[i] instanceof Number)
				result = atom.getTerm(i) instanceof Number && ((Number) terms[i]).equals((Number) atom.getTerm(i));
			else if (terms[i] instanceof Quotation)
				result = atom.getTerm(i) instanceof Quotation && ((Quotation) terms[i]).equals((Quotation) atom.getTerm(i));
			else if (terms[i] instanceof Placemarker)
				result = true;
			else if (terms[i] instanceof Scheme)
				result = ((Scheme) terms[i]).matches(atom.getTerm(i));
			else
				result = false;
		return result;
	}

}