AbstractTerm.java

/*
 * #%L
 * prolobjectlink-jpi
 * %%
 * Copyright (C) 2019 Prolobjectlink Project
 * %%
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * #L%
 */
package io.github.prolobjectlink.prolog;

import static io.github.prolobjectlink.prolog.PrologTermType.ATOM_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.CLASS_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.CUT_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.DOUBLE_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.FAIL_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.FALSE_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.FIELD_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.FLOAT_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.INTEGER_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.LIST_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.LONG_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.MAP_ENTRY_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.MAP_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.MIXIN_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.NIL_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.OBJECT_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.PARAMETER_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.RESULT_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.STRUCTURE_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.TRUE_TYPE;
import static io.github.prolobjectlink.prolog.PrologTermType.VARIABLE_TYPE;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;

/**
 * Partial implementation of {@link PrologTerm} interface.
 * 
 * @author Jose Zalacain
 * @since 1.0
 */
public abstract class AbstractTerm implements PrologTerm {

	protected int type;
	protected final PrologProvider provider;

	protected AbstractTerm(int type, PrologProvider provider) {
		this.type = type;
		this.provider = provider;
	}

	protected final void checkIndex(int index) {
		if (index < 0) {
			throw new ArrayIndexOutOfBoundsException(index);
		}
	}

	protected final void checkIndex(int index, int max) {
		if (index < 0 || index > max) {
			throw new ArrayIndexOutOfBoundsException(index);
		}
	}

	protected final String removeQuoted(String functor) {
		if (functor != null && functor.startsWith("\'") && functor.endsWith("\'")) {
			return functor.substring(1, functor.length() - 1);
		}
		return functor;
	}

	protected final <K extends PrologTerm> K toTerm(Object o, Class<K> from) {
		return provider.toTerm(o, from);
	}

	protected final <K extends PrologTerm> K[] toTermArray(Object[] os, Class<K[]> from) {
		return provider.toTermArray(os, from);
	}

	protected final <K> K fromTerm(PrologTerm term, Class<K> to) {
		return provider.fromTerm(term, to);
	}

	protected final <K> K[] fromTermArray(PrologTerm[] terms, Class<K[]> to) {
		return provider.fromTermArray(terms, to);
	}

	protected final PrologLogger getLogger() {
		return provider.getLogger();
	}

	public PrologTerm getArgument(int index) {
		PrologTerm[] array = getArguments();
		checkIndex(index, array.length);
		return array[index];
	}

	public final String getIndicator() {
		return getFunctor() + "/" + getArity();
	}

	public final boolean hasIndicator(String functor, int arity) {
		return getFunctor().equals(functor) && getArity() == arity;
	}

	public PrologTerm getTerm() {
		return this;
	}

	public final int getType() {
		return type;
	}

	public final PrologProvider getProvider() {
		return provider;
	}

	/**
	 * Casts a PrologTerm to the class or interface represented by this
	 * {@code Class} object.
	 *
	 * @param term the object to be cast
	 * @param type the class or interface to be casted
	 * @return the PrologTerm after casting, or null if term is null
	 *
	 * @throws ClassCastException if the object is not null and is not assignable to
	 *                            the type T.
	 * @since 1.1
	 */
	protected final <T extends PrologTerm> T cast(PrologTerm term, Class<T> type) {
		return type.cast(term);
	}

	/**
	 * Casts a PrologTerm to the class or interface represented by this
	 * {@code Class} object.
	 *
	 * @param term the object to be cast
	 * @return the PrologTerm after casting, or null if term is null
	 *
	 * @throws ClassCastException if the object is not null and is not assignable to
	 *                            the type T.
	 * @since 1.1
	 */
	protected final <T extends PrologTerm> T cast(PrologTerm term) {
		return (T) term;
	}

	public final <T extends PrologTerm> T cast() {
		return cast(this);
	}

	public Object getObject() {
		PrologTerm term = this;
		switch (term.getType()) {
		case NIL_TYPE:
			return null;
		case CUT_TYPE:
			return "!";
		case FAIL_TYPE:
			return "fail";
		case TRUE_TYPE:
			return true;
		case FALSE_TYPE:
			return false;
		case ATOM_TYPE:
			return term.getFunctor();
		case FLOAT_TYPE:
			return ((PrologNumber) term).getFloatValue();
		case INTEGER_TYPE:
			return ((PrologNumber) term).getIntegerValue();
		case DOUBLE_TYPE:
			return ((PrologNumber) term).getDoubleValue();
		case LONG_TYPE:
			return ((PrologNumber) term).getLongValue();
		case VARIABLE_TYPE:
			return ((PrologVariable) term).getName();
		case LIST_TYPE:
			return fromTermArray(term.getArguments(), Object[].class);
		case STRUCTURE_TYPE:
			return term;
		case OBJECT_TYPE:
			return term.getObject();
		case FIELD_TYPE:
			PrologVariable field = term.cast();
			return "field " + field.getName();
		case RESULT_TYPE:
			PrologVariable result = term.cast();
			return "result " + result.getName();
		case PARAMETER_TYPE:
			PrologVariable parameter = term.cast();
			return "parameter " + parameter.getName();
		case CLASS_TYPE:
		case MIXIN_TYPE:
			return "class " + term.getFunctor();
		default:
			return null;
		}
	}

	public final boolean isEntry() {
		return getType() == MAP_ENTRY_TYPE;
	}

	public final boolean isMap() {
		return getType() == MAP_TYPE;
	}

	public boolean isField() {
		return getType() == FIELD_TYPE;
	}

	public boolean isResult() {
		return getType() == RESULT_TYPE;
	}

	public boolean isParameter() {
		return getType() == PARAMETER_TYPE;
	}

	public final boolean isMixin() {
		return getType() == MIXIN_TYPE;
	}

	public final boolean isClass() {
		return getType() == CLASS_TYPE;
	}

	public boolean isVariableBound() {
		return isVariable() && getTerm() != this;
	}

	public boolean isVariableNotBound() {
		return isVariable() && getTerm() == this;
	}

	public final boolean isClause() {
		return false;
	}

	public final boolean isTerm() {
		return true;
	}

	/**
	 * Match to other term returning list of substitutions.
	 * 
	 * @param term - term to match check
	 * @return list of substitutions.
	 */
	public final Map<String, PrologTerm> match(PrologTerm term) {
		Deque<PrologTerm> stack = new ArrayDeque<PrologTerm>();
		if (unify(term, stack)) {
			int size = stack.size();
			HashMap<String, PrologTerm> substitution = new HashMap<String, PrologTerm>(size);
			while (size > 0) {
				PrologVariable variable = (PrologVariable) stack.pop();
				substitution.put(variable.getName(), variable.getTerm());
				// variable.unbind();
				size--;
			}
			return substitution;
		}
		return new HashMap<String, PrologTerm>();
	}

	/**
	 * Unification is the basic primitive operation in logic programming. Check that
	 * two terms (x and y) unify. Prolog unification algorithm is based on three
	 * principals rules:
	 * <ul>
	 * <li>If x and y are atomics constants then x and y unify only if they are same
	 * object.</li>
	 * <li>If x is a variable and y is anything then they unify and x is
	 * instantiated to y. Conversely, if y is a variable then this is instantiated
	 * to x.</li>
	 * <li>If x and y are structured terms then unify only if they match (equals
	 * funtor and arity) and all their correspondents arguments unify.</li>
	 * </ul>
	 * 
	 * 
	 * @param term  - the term to unify with the current term
	 * @param stack - the stack is used to store the addresses of variables which
	 *              are bound by the unification. This is needed when backtracking.
	 * @return true if the specified term unify whit the current term, false if not
	 */
	private boolean unify(PrologTerm term, Deque<PrologTerm> stack) throws PrologError {

		PrologTerm thisTerm = this;
		PrologTerm otherTerm = term;

		if (thisTerm.isVariableBound()) {
			return ((AbstractTerm) thisTerm.getTerm()).unify(otherTerm, stack);
		}

		else if (otherTerm.isVariableBound()) {
			return ((AbstractTerm) otherTerm.getTerm()).unify(thisTerm, stack);
		}

		// current term is a free variable
		else if (thisTerm.isVariableNotBound()) {
			// if (!thisTerm.occurs(otherTerm)) {
			// thisTerm.bind(otherTerm);
			stack.push(thisTerm);
			return true;
			// }
		}

		// the other term is a free variable
		else if (otherTerm.isVariableNotBound()) {
			// if (!otherTerm.occurs(thisTerm)) {
			// otherTerm.bind(thisTerm);
			stack.push(otherTerm);
			return true;
			// }
		}

		// if at least term is a number then check equivalence
		else if (thisTerm.isNumber() || otherTerm.isNumber()) {
			if ((thisTerm.isInteger() || thisTerm.isLong()) && (otherTerm.isInteger() || otherTerm.isLong())) {
				int thisInt = ((PrologNumber) thisTerm).getIntegerValue();
				int otherInt = ((PrologNumber) otherTerm).getIntegerValue();
				return thisInt == otherInt;
			}
			return thisTerm.equals(otherTerm);
		}

		else {

			int thisArity = thisTerm.getArity();
			int otherArity = otherTerm.getArity();
			String thisFunctor = thisTerm.getFunctor();
			String otherFunctor = otherTerm.getFunctor();

			if (thisFunctor.equals(otherFunctor) && thisArity == otherArity) {
				for (int i = 0; i < thisArity; i++) {
					if (thisTerm.getArgument(i) != null && otherTerm.getArgument(i) != null) {
						if (!((AbstractTerm) thisTerm.getArgument(i)).unify(otherTerm.getArgument(i), stack)) {
							return false;
						}
					}
				}
				return true;
			}
		}
		return false;
	}

}