public static final Term LIST_FUNCTOR = new Term("."); public static final TermList NULL_LIST = new TermList(); public static Term newAtom(final String text) {
  if (".".equals(text)) {
    return LIST_FUNCTOR;
  } else {
    return new Term(text);
  }
} public static TermDouble newDouble(final String text) {
  return new TermDouble(text);
} public static TermList newList(final Term term) {
  return new TermList(term);
} public static TermLong newLong(final String text) {
  return new TermLong(text);
} public static TermStruct newStruct(final Term functor) {
  return new TermStruct(functor);
} public static TermVar newVar(final String name) {
  return new TermVar(name);
} public static TermList createOrAppendToList(final TermList nullableList, final Term term) {
  final TermList newList = Terms.newList(term);

  if (nullableList != null && !nullableList.isNullList()) {
    newList.setTail(nullableList.getTail());
    nullableList.setTail(newList);
  }

  return newList;
} public abstract class NumericTerm extends Term {

  NumericTerm(final String text) {
    super(text);
  }

  public abstract int compare(NumericTerm term);

  public abstract NumericTerm add(NumericTerm term);

  public abstract NumericTerm sub(NumericTerm term);

  public abstract NumericTerm div(NumericTerm term);

  public abstract NumericTerm mul(NumericTerm term);

  public abstract NumericTerm neg();

  public abstract NumericTerm abs();

  public abstract NumericTerm sign();

  public abstract boolean isDouble();

  @Override
  public final String forWrite() {
    return this.getText();
  }

  @Override
  public final boolean dryUnifyTo(Term atom) {
    if (this == atom) {
      return true;
    }

    if (atom.getTermType() == VAR) {
      atom = ((TermVar) atom).getValue();
    }

    if (atom == null) {
      return true;
    }

    if (atom.getClass() == this.getClass()) {
      return this.compare((NumericTerm) atom) == 0;
    }
    return false;
  }

  @Override
  public final boolean unifyTo(Term atom) {
    if (this == atom) {
      return true;
    }

    switch (atom.getTermType()) {
      case ATOM: {
        if (atom.getClass() == this.getClass()) {
          return this.compare((NumericTerm) atom) == 0;
        } else {
          return false;
        }
      }
      case VAR: {
        final TermVar var = (TermVar) atom;
        final Term value = var.getValue();
        if (value == null) {
          return ((TermVar) atom).setValue(this);
        } else {
          return unifyTo(value);
        }
      }
    }
    return false;
  }

} public class Term {

  private final String text;

  Term(final String text) {
    this.text = requireNonNull(text);
  }

  public int getPriority() {
    return 0;
  }

  public String getText() {
    return this.text;
  }

  public TermType getTermType() {
    return ATOM;
  }

  public boolean dryUnifyTo(Term term) {
    if (this == term) {
      return true;
    }

    switch (term.getTermType()) {
      case VAR: {
        term = ((TermVar) term).getValue();
        return term == null || this.dryUnifyTo(term);
      }
      case ATOM: {
        return term.getClass() == Term.class && getText().equals(term.getText());
      }
      case STRUCT: {
        final TermStruct thatStruct = (TermStruct) term;
        return thatStruct.getArity() == 0 && getText().equals(thatStruct.getFunctor().getText());
      }
      default:
        return false;
    }
  }

  public Map<String, TermVar> allNamedVarsAsMap() {
    return this.variables()
        .collect(toMap(TermVar::getText, e -> e, (v1, v2) -> v2));
  }

  public Map<String, Term> allNamedVarValuesAsMap() {
    return this.variables()
        .filter(v -> !v.isFree())
        .collect(toMap(TermVar::getText, e -> e.<Term>findNonVarOrDefault(e), (v1, v2) -> v2));
  }

  public boolean isGround() {
    return true;
  }

  @SuppressWarnings("unchecked")
  public <T extends Term> T findNonVarOrSame() {
    return (T) this;
  }

  @SuppressWarnings("unchecked")
  public <T extends Term> T findNonVarOrDefault(final T term) {
    return (T) this;
  }

  public Stream<Term> stream() {
    return Stream.of(this);
  }

  public Stream<TermVar> variables() {
    return Stream.empty();
  }

  public String toSrcString() {
    return String.format("'%s'", escapeSrc(getText()));
  }

  @Override
  public String toString() {
    return toSrcString();
  }

  @Override
  public int hashCode() {
    if (text == null) {
      return super.hashCode();
    } else {
      return text.hashCode();
    }
  }

  public Number toNumber() {
    throw new ProlTypeErrorException("numeric", "NonNumeric term", this);
  }

  public String getSignature() {
    return getText() + "/0";
  }

  public String forWrite() {
    return getText();
  }

  public boolean unifyTo(final Term other) {
    if (this == other) {
      return true;
    }

    boolean result = false;

    switch (other.getTermType()) {
      case VAR: {
        final TermVar var = (TermVar) other;
        final Term value = var.getValue();
        if (value == null) {
          result = ((TermVar) other).setValue(this);
        } else {
          result = unifyTo(value);
        }
      }
      break;
      case ATOM: {
        result = other.getClass() == Term.class && getText().equals(other.getText());
      }
      break;
      case STRUCT: {
        final TermStruct thatStruct = (TermStruct) other;
        result = thatStruct.getArity() == 0 && getText().equals(thatStruct.getFunctor().getText());
      }
      break;
    }

    return result;
  }

  @Override
  public boolean equals(final Object obj) {
    if (obj == null) {
      return false;
    }

    if (this == obj) {
      return true;
    }

    final Class<?> objclass = obj.getClass();

    if (objclass == TermLong.class || obj.getClass() == Term.class) {

      final Term other = (Term) obj;

      if (hashCode() != other.hashCode()) {
        return false;
      }
      return this.text.equals(other.text);
    } else {
      return false;
    }
  }

  protected void doArrangeVars(final Map<String, TermVar> variables) {
  }

  public final void arrangeVariablesInsideTerms(final Term termTwo) {
    final Map<String, TermVar> varMap = new HashMap<>();
    this.doArrangeVars(varMap);
    termTwo.doArrangeVars(varMap);
  }

  public int getTextLength() {
    return this.text == null ? 0 : this.text.length();
  }

  public boolean hasVariableWithName(final String name) {
    return false;
  }

  public Term makeClone() {
    return this;
  }

  protected Term doMakeClone(final Map<Integer, TermVar> vars) {
    return this;
  }

  public final Term makeCloneAndVarBound() {
    return this.makeCloneAndVarBound(new HashMap<>());
  }

  protected Term makeCloneAndVarBound(final Map<Integer, TermVar> vars) {
    return this;
  }

} public final class TermDouble extends NumericTerm {

  private final Double value;

  TermDouble(final String name) {
    super(name);

    int len = name.length() - 1;
    while (len >= 0) {
      if (Character.isWhitespace(name.charAt(len--))) {
        throw new NumberFormatException();
      }
    }

    value = Double.parseDouble(name);
  }

  TermDouble(final double value) {
    super("");
    this.value = value;
  }

  @Override
  public String toString() {
    return getText();
  }

  @Override
  public int hashCode() {
    return this.value.hashCode();
  }

  @Override
  public boolean equals(final Object obj) {
    if (obj == null) {
      return false;
    }

    if (obj.getClass() == TermDouble.class) {
      return ((TermDouble) obj).value == value;
    }

    return super.equals(obj);
  }

  @Override
  public Number toNumber() {
    return this.value;
  }

  @Override
  public String toSrcString() {
    String text = getText();
    if (text.indexOf('.') < 0) {
      text += ".0";
    }
    return text;
  }

  @Override
  public String getText() {
    return Double.toString(this.value);
  }

  @Override
  public int compare(final NumericTerm atom) {
    final double value = atom.toNumber().doubleValue();
    return Double.compare(this.value, value);
  }

  @Override
  public NumericTerm add(final NumericTerm atom) {
    final double value = atom.toNumber().doubleValue();
    return Terms.newDouble(this.value + value);
  }

  @Override
  public NumericTerm sub(final NumericTerm atom) {
    final double value = atom.toNumber().doubleValue();
    return Terms.newDouble(this.value - value);
  }

  @Override
  public NumericTerm div(final NumericTerm atom) {
    final double value = atom.toNumber().doubleValue();
    return Terms.newDouble(this.value / value);
  }

  @Override
  public NumericTerm mul(final NumericTerm atom) {
    final double value = atom.toNumber().doubleValue();
    return Terms.newDouble(this.value * value);
  }

  @Override
  public NumericTerm neg() {
    return Terms.newDouble(-this.value);
  }

  @Override
  public boolean isDouble() {
    return true;
  }

  @Override
  public NumericTerm abs() {
    if (Double.compare(this.value, 0.0d) >= 0) {
      return this;
    }
    return Terms.newDouble(Math.abs(value));
  }

  @Override
  public NumericTerm sign() {
    return Terms.newLong(Double.compare(this.value, 0.0d));
  }
} public final class TermList extends TermStruct {

  private static final int INDEX_HEAD = 0;
  private static final int INDEX_TAIL = 1;

  TermList() {
    super(Terms.LIST_FUNCTOR, null);
  }

  TermList(final Term term) {
    super(Terms.LIST_FUNCTOR, new Term[] {term, Terms.NULL_LIST});
  }

  TermList(final Term head, final Term tail) {
    super(Terms.LIST_FUNCTOR, new Term[] {head, tail});
  }

  public static TermList asTermList(final Term... elements) {
    if (elements.length == 0) {
      return Terms.NULL_LIST;
    } else if (elements.length == 1) {
      final Term element = elements[0];
      switch (element.getTermType()) {
        case LIST:
          return (TermList) element;
        case STRUCT: {
          final TermStruct struct = (TermStruct) element;
          final TermList result = newList(struct.getFunctor());
          TermList curResult = result;
          final int arity = struct.getArity();
          for (int li = 0; li < arity; li++) {
            curResult = createOrAppendToList(curResult, struct.getElement(li));
          }
          return result;
        }
        default:
          return newList(element);
      }
    } else {
      final TermList result = newList(elements[0]);
      TermList next = result;
      final int length = elements.length;
      for (int li = 1; li < length; li++) {
        next = createOrAppendToList(next, elements[li]);
      }
      return result;
    }
  }

  @SuppressWarnings("unchecked")
  public <T extends Term> T getHead() {
    return (T) this.terms[INDEX_HEAD];
  }

  @SuppressWarnings("unchecked")
  public <T extends Term> T getTail() {
    final Term tail = this.terms[INDEX_TAIL];
    switch (tail.getTermType()) {
      case VAR: {
        final TermVar var = (TermVar) tail;
        final Term val = var.getValue();
        return (T) (val == null ? var : val);
      }
      case LIST:
      default:
        return (T) tail;
    }
  }

  public void setTail(final Term newTail) {
    this.terms[INDEX_TAIL] = Objects.requireNonNull(newTail, "Null is not allowed as list tail");
  }

  public int calculateLength() {
    if (this == Terms.NULL_LIST) {
      return 0;
    }
    final Term tail = this.terms[INDEX_TAIL];
    if (tail.getTermType() == LIST) {
      return ((TermList) tail).calculateLength() + 1;
    }
    return 2;
  }

  @Override
  public Stream<Term> stream() {
    return this.isNullList() ? Stream.of(this.functor) : super.stream();
  }

  @Override
  public Stream<TermVar> variables() {
    return this.isNullList() ? Stream.empty() : super.variables();
  }

  @Override
  public Term makeClone() {
    return this.isNullList() ? this : this.doMakeClone(new HashMap<>());
  }

  public boolean isNullList() {
    return this.terms == EMPTY_ARRAY;
  }

  @Override
  protected Term makeCloneAndVarBound(final Map<Integer, TermVar> vars) {
    return this.isNullList() ? Terms.NULL_LIST : newList(this.getHead().makeCloneAndVarBound(vars),
        this.getTail().makeCloneAndVarBound(vars));
  }

  @Override
  public boolean isGround() {
    return this.isNullList() || this.stream().allMatch(Term::isGround);
  }

  @Override
  public TermType getTermType() {
    return TermType.LIST;
  }

  @Override
  protected Term doMakeClone(Map<Integer, TermVar> vars) {
    return this.isNullList() ? Terms.NULL_LIST :
        newList(this.getHead().doMakeClone(vars), this.getTail().doMakeClone(vars));
  }

  @Override
  public String toString() {
    if (isNullList()) {
      return "[]";
    }
    final StringBuilder builder = new StringBuilder("[");

    boolean notfirst = false;
    Term list = this;

    while (list != Terms.NULL_LIST) {
      if (list.getTermType() == LIST) {
        if (notfirst) {
          builder.append(',');
        }
        final TermList asList = (TermList) list;
        builder.append(asList.getHead().toString());
        list = asList.getTail();
      } else {
        if (notfirst) {
          builder.append('|');
        }
        builder.append(list.toString());
        break;
      }
      notfirst = true;
    }

    builder.append(']');
    return builder.toString();
  }

  @Override
  public String toSrcString() {
    if (isNullList()) {
      return "[]";
    }
    StringBuilder builder = new StringBuilder("[");

    boolean notfirst = false;
    Term list = this;

    while (list != Terms.NULL_LIST) {
      if (list.getTermType() == LIST) {
        if (notfirst) {
          builder.append(',');
        }
        final TermList asList = (TermList) list;
        builder.append(asList.getHead().toSrcString());
        list = asList.getTail();
      } else {
        if (notfirst) {
          builder.append('|');
        }
        builder.append(list.toSrcString());
        break;
      }
      notfirst = true;
    }

    builder.append(']');
    return builder.toString();
  }

  @Override
  public String getSignature() {
    return "./2";
  }

  public final void replaceLastElement(final Term newLastElement) {
    if (isNullList()) {
      throw new ProlCriticalError("Attemption to change Null list");
    }
    TermList curList = this;
    while (!Thread.currentThread().isInterrupted()) {
      Term tail = curList.getTail();
      if (tail == Terms.NULL_LIST || tail.getTermType() != LIST) {
        curList.setTail(newLastElement);
        break;
      }
      curList = (TermList) tail;
    }
  }

  @Override
  public boolean unifyTo(final Term atom) {
    if (this == atom) {
      return true;
    }

    switch (atom.getTermType()) {
      case LIST: {
        TermList thatList = (TermList) atom;
        if (this.isNullList()) {
          return thatList.isNullList();
        } else if (thatList.isNullList()) {
          return false;
        }

        return this.getHead().unifyTo(thatList.getHead()) &&
            this.getTail().unifyTo(thatList.getTail());
      }
      case VAR: {
        final TermVar var = (TermVar) atom;
        final Term value = var.getValue();
        if (value == null) {
          return ((TermVar) atom).setValue(this);
        } else {
          return unifyTo(value);
        }
      }
    }
    return false;
  }

  @Override
  public boolean dryUnifyTo(Term atom) {
    if (this == atom) {
      return true;
    }

    if (atom.getTermType() == VAR) {
      atom = ((TermVar) atom).getValue();
    }

    if (atom == null) {
      return true;
    }

    if (atom.getTermType() == LIST) {
      if (this == atom) {
        return true;
      }
      final TermList thisList = this;
      final TermList thatList = (TermList) atom;

      if (thisList.isNullList()) {
        return thatList.isNullList();
      } else {
        if (thatList.isNullList()) {
          return false;
        }
      }

      return thisList
          .getHead().dryUnifyTo(thatList.getHead())
          && thisList.getTail().dryUnifyTo(thatList.getTail());
    }
    return false;
  }

  @Override
  public String forWrite() {
    if (isNullList()) {
      return "[]";
    }
    StringBuilder builder = new StringBuilder("[");

    boolean notfirst = false;
    Term list = this;

    while (list != Terms.NULL_LIST) {
      if (list.getTermType() == LIST) {
        if (notfirst) {
          builder.append(',');
        }
        final TermList asList = (TermList) list;
        builder.append(asList.getHead().forWrite());
        list = asList.getTail();
      } else {
        if (notfirst) {
          builder.append('|');
        }
        builder.append(list.forWrite());
        break;
      }
      notfirst = true;
    }

    builder.append(']');
    return builder.toString();
  }

  public Term[] toArray() {
    if (this.isNullList()) {
      return new Term[0];
    }

    final ArrayList<Term> arraylist = new ArrayList<>();
    TermList curlist = this;
    while (!Thread.currentThread().isInterrupted()) {
      if (curlist.isNullList()) {
        break;
      }
      arraylist.add(curlist.getHead());
      final Term nextList = curlist.getTail();
      if (nextList.getTermType() == LIST) {
        curlist = (TermList) nextList;
      } else {
        arraylist.add(nextList);
        break;
      }
    }
    return arraylist.toArray(new Term[0]);
  }

  public Term toAtom() {
    if (this.isNullList()) {
      return new Term("<empty>");
    }
    if (this.getTail() == Terms.NULL_LIST) {
      return this.getHead();
    } else {
      final int length = this.calculateLength();
      if (length == 3) {
        if (this.getHead() == Terms.LIST_FUNCTOR) {
          final TermList secondElement = this.getTail();
          final TermList thirdElement = secondElement.getTail();

          if (thirdElement.getHead().getTermType() == LIST) {
            return newList(secondElement.getHead(), thirdElement.getHead());
          }
        }
      }

      final TermStruct result;
      if (length == 1) {
        result = newStruct(this.getHead());
      } else {
        Term[] elements = new Term[length - 1];
        TermList lst = this.getTail();
        int index = 0;
        while (lst != Terms.NULL_LIST) {
          elements[index++] = lst.getHead();
          lst = lst.getTail();
        }
        result = newStruct(this.getHead(), elements);
      }
      return result;
    }
  }

  protected void doArrangeVars(final Map<String, TermVar> variables) {
    TermList list = this;
    while (!list.isNullList()) {
      list.getHead().doArrangeVars(variables);
      final Term tail = list.getTail();
      if (tail.getTermType() == LIST) {
        list = (TermList) tail;
      } else {
        tail.doArrangeVars(variables);
        break;
      }
    }
  }

  @Override
  public boolean hasVariableWithName(final String name) {
    if (this == Terms.NULL_LIST) {
      return false;
    }
    return getHead().hasVariableWithName(name) || getTail().hasVariableWithName(name);
  }
} public final class TermLong extends NumericTerm {

  private final Long value;

  TermLong(final String name) {
    super(name);
    value = Long.parseLong(name);
  }

  TermLong(final long value) {
    super("");
    this.value = value;
  }

  @Override
  public String toString() {
    final String val = getText();
    if (val.isEmpty()) {
      return Long.toString(value);
    } else {
      return val;
    }
  }

  @Override
  public int hashCode() {
    return this.value.hashCode();
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == null) {
      return false;
    }

    if (obj.getClass() == TermLong.class) {
      return ((TermLong) obj).value == this.value;
    }

    return super.equals(obj);
  }

  @Override
  public Number toNumber() {
    return this.value;
  }

  @Override
  public String toSrcString() {
    return Long.toString(value);
  }

  @Override
  public String getText() {
    return Long.toString(this.value);
  }

  @Override
  public int compare(final NumericTerm atom) {
    if (atom.isDouble()) {
      final double value = atom.toNumber().doubleValue();
      return Double.compare((double) this.value, value);
    }
    return Long.compare(this.value, atom.toNumber().longValue());
  }

  @Override
  public NumericTerm add(final NumericTerm atom) {
    if (atom.isDouble()) {
      final double value = atom.toNumber().doubleValue();
      return Terms.newDouble((double) this.value + value);
    } else {
      return newLong(this.value + atom.toNumber().longValue());
    }
  }

  @Override
  public NumericTerm sub(final NumericTerm atom) {
    if (atom.isDouble()) {
      final double value = atom.toNumber().doubleValue();
      return Terms.newDouble((double) this.value - value);
    } else {
      return newLong(this.value - atom.toNumber().longValue());
    }
  }

  @Override
  public NumericTerm div(final NumericTerm atom) {
    if (atom.isDouble()) {
      final double value = atom.toNumber().doubleValue();
      return Terms.newDouble((double) this.value / value);
    } else {
      return newLong(this.value / atom.toNumber().longValue());
    }
  }

  @Override
  public NumericTerm mul(final NumericTerm atom) {
    if (atom.isDouble()) {
      final double value = atom.toNumber().doubleValue();
      return Terms.newDouble((double) this.value * value);
    } else {
      return newLong(this.value * atom.toNumber().longValue());
    }
  }

  @Override
  public NumericTerm neg() {
    return newLong(-this.value);
  }

  @Override
  public boolean isDouble() {
    return false;
  }

  @Override
  public NumericTerm abs() {
    if (this.value >= 0L) {
      return this;
    }
    return newLong(Math.abs(this.value));
  }

  @Override
  public NumericTerm sign() {
    int sign = 0;
    if (this.value < 0L) {
      sign = -1;
    } else if (this.value > 0L) {
      sign = 1;
    }
    return newLong(sign);
  }
} public final class TermOperator extends Term {

  public static final int PRIORITY_MAX = 0;
  public static final int PRIORITY_MIN = 1200;

  private final Op op;
  private final String signature;

  public TermOperator(final int priority, final OpAssoc type, final String name) {
    super(name);

    if (priority < PRIORITY_MAX || priority > PRIORITY_MIN) {
      throw new IllegalArgumentException("Wrong priority value");
    }

    switch (type) {
      case FX:
      case FY:
      case XF:
      case YF: {
        signature = name + "/1";
      }
      break;
      case YFX:
      case XFY:
      case XFX: {
        signature = name + "/2";
      }
      break;
      default:
        throw new IllegalArgumentException("Wrong operator type");
    }

    this.op = Op.make(priority, type, name);
  }

  public Op asOp() {
    return this.op;
  }

  @Override
  public TermType getTermType() {
    return OPERATOR;
  }

  public OpAssoc getOperatorType() {
    return this.op.getAssoc();
  }

  @Override
  public int getPriority() {
    return op.getPrecedence();
  }

  @Override
  public int hashCode() {
    return this.op.hashCode();
  }

  @Override
  public boolean equals(final Object obj) {
    if (obj == null) {
      return false;
    }
    if (getClass() != obj.getClass()) {
      return false;
    }

    final TermOperator other = (TermOperator) obj;

    if (!this.op.equals(other.op)) {
      return false;
    }
    return this.getText().equals(other.getText());
  }

  @Override
  public String toSrcString() {
    return getText();
  }

  @Override
  public String toString() {
    return getText();
  }

  public String getTypeAsString() {
    return this.op.getAssoc().getText();
  }

  public void write(final PrintWriter writer) {
    writer.print(":- op(");
    writer.print(this.op.getPrecedence());
    writer.print(',');
    writer.print(getTypeAsString());
    writer.print(",'");
    writer.print(getText());
    writer.println("').");
  }

  @Override
  public String getSignature() {
    return signature;
  }

  @Override
  public String forWrite() {
    return getText();
  }

  @Override
  public boolean unifyTo(final Term atom) {
    if (this == atom) {
      return true;
    }

    final boolean result;

    switch (atom.getTermType()) {
      case OPERATOR:
      case ATOM: {
        result = getText().equals(atom.getText());
      }
      break;
      case VAR: {
        final TermVar var = (TermVar) atom;
        final Term value = var.getValue();
        if (value == null) {
          result = ((TermVar) atom).setValue(this);
        } else {
          result = unifyTo(value);
        }
      }
      break;
      default:
        result = false;
        break;
    }
    return result;
  }

  @Override
  public boolean dryUnifyTo(final Term atom) {
    if (this == atom) {
      return true;
    }

    final boolean result;

    switch (atom.getTermType()) {
      case OPERATOR:
      case ATOM: {
        result = getText().equals(atom.getText());
      }
      break;
      case VAR: {
        final TermVar var = (TermVar) atom;
        final Term value = var.getValue();
        result = value == null || this.dryUnifyTo(value);
      }
      break;
      default:
        result = false;
        break;
    }
    return result;
  }
} public class TermStruct extends CompoundTerm {

  static final Term[] EMPTY_ARRAY = new Term[0];
  final Term[] terms;
  final Term functor;
  private final String structureSignature;
  private volatile PredicateInvoker predicateProcessor;

  TermStruct(final Term functor) {
    this(functor, EMPTY_ARRAY);
  }

  TermStruct(final String functor, final Term[] elements) {
    this(new Term(functor), elements);
  }

  TermStruct(final Term functor, final Term[] elements) {
    super(functor.getText());
    this.functor = functor;
    this.terms = elements == null ? EMPTY_ARRAY : elements;
    this.structureSignature = functor.getText() + '/' + getArity();
    this.predicateProcessor = PredicateInvoker.NULL_PROCESSOR;
  }

  TermStruct(final Term functor, final Term[] elements, final PredicateInvoker processor) {
    this(functor, elements);
    this.predicateProcessor = requireNonNull(processor);
  }

  public final Term getFunctor() {
    return functor;
  }

  public final Term[] getElementArray() {
    return this.terms;
  }

  public void setElement(final int index, final Term element) {
    this.terms[index] = element;
  }

  @Override
  public Stream<TermVar> variables() {
    return this.stream().flatMap(Term::variables);
  }

  @Override
  public Stream<Term> stream() {
    return Stream.concat(Stream.of(this.functor), Arrays.stream(this.terms).flatMap(Term::stream));
  }

  @SuppressWarnings("unchecked")
  public <T extends Term> T getElement(final int index) {
    return (T) this.terms[index];
  }

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

  public boolean isClause() {
    final int arity = this.getArity();
    return (arity > 0 && arity < 3) && ":-".equals(this.functor.getText());
  }

  @Override
  public int getPriority() {
    if (functor.getTermType() == OPERATOR) {
      return functor.getPriority();
    } else {
      return 0;
    }
  }

  @Override
  public String toString() {
    return getStringRepresentation(false);
  }

  @Override
  public String toSrcString() {
    return getStringRepresentation(true);
  }

  private String getStringRepresentation(final boolean sourceLike) {
    if (functor.getTermType() != OPERATOR) {
      // just struct
      final StringBuilder buffer = new StringBuilder(Utils.escapeSrc(getText()));

      if (getArity() != 0) {
        buffer.append('(');
        for (int li = 0; li < getArity(); li++) {
          if (li > 0) {
            buffer.append(',');
          }
          buffer.append(sourceLike ? getElement(li).toSrcString() : getElement(li).toString());
        }
        buffer.append(')');
      }

      return buffer.toString();
    } else {
      // it's an operator
      final String opName = sourceLike ? functor.toSrcString() : functor.toString();
      final StringBuilder builder = new StringBuilder();

      final TermOperator OperatorFunctor = (TermOperator) functor;

      final int priority = OperatorFunctor.getPriority();

      final Term arg1 = this.getElement(0);
      final Term arg2 = getArity() > 1 ? this.getElement(1) : null;

      switch (OperatorFunctor.getOperatorType()) {
        case FX: {
          builder.append(opName);
          builder.append(' ');

          final String text = sourceLike ? arg1.toSrcString() : arg1.toString();

          if (arg1.getPriority() >= priority) {
            builder.append('(').append(text).append(')');
          } else {
            builder.append(text);
          }
        }
        break;
        case FY: {
          builder.append(opName);
          builder.append(' ');

          final String text = sourceLike ? arg1.toSrcString() : arg1.toString();

          if (arg1.getPriority() > priority) {
            builder.append('(').append(text).append(')');
          } else {
            builder.append(text);
          }
        }
        break;
        case XF: {
          final String text = sourceLike ? arg1.toSrcString() : arg1.toString();

          if (arg1.getPriority() >= priority) {
            builder.append('(').append(text).append(')');
          } else {
            builder.append(text);
          }

          builder.append(' ');
          builder.append(opName);
        }
        break;
        case YF: {
          final String text = sourceLike ? arg1.toSrcString() : arg1.toString();

          if (arg1.getPriority() > priority) {
            builder.append('(').append(text).append(')');
          } else {
            builder.append(text);
          }

          builder.append(' ');
          builder.append(opName);
        }
        break;
        case XFX: {
          final String text = sourceLike ? arg1.toSrcString() : arg1.toString();
          final String text2 = sourceLike ? arg2.toSrcString() : arg2.toString();

          if (arg1.getPriority() >= priority) {
            builder.append('(').append(text).append(')');
          } else {
            builder.append(text);
          }

          builder.append(' ');
          builder.append(opName);
          builder.append(' ');

          if (arg2.getPriority() >= priority) {
            builder.append('(').append(text2).append(')');
          } else {
            builder.append(text2);
          }
        }
        break;
        case YFX: {
          final String text = sourceLike ? arg1.toSrcString() : arg1.toString();
          final String text2 = sourceLike ? arg2.toSrcString() : arg2.toString();

          if (arg1.getPriority() > priority) {
            builder.append('(').append(text).append(')');
          } else {
            builder.append(text);
          }

          builder.append(' ');
          builder.append(opName);
          builder.append(' ');

          if (arg2.getPriority() >= priority) {
            builder.append('(').append(text2).append(')');
          } else {
            builder.append(text2);
          }
        }
        break;
        case XFY: {
          final String text = sourceLike ? arg1.toSrcString() : arg1.toString();
          final String text2 = sourceLike ? arg2.toSrcString() : arg2.toString();

          if (arg1.getPriority() >= priority) {
            builder.append('(').append(text).append(')');
          } else {
            builder.append(text);
          }

          builder.append(' ');
          builder.append(opName);
          builder.append(' ');

          if (arg2.getPriority() > priority) {
            builder.append('(').append(text2).append(')');
          } else {
            builder.append(text2);
          }
        }
        break;
        default:
          throw new ProlCriticalError("Unsupported type");
      }

      return builder.toString();
    }
  }

  @Override
  public String forWrite() {
    if (functor.getTermType() != OPERATOR) {
      // just struct
      final StringBuilder buffer = new StringBuilder(functor.forWrite());

      if (getArity() > 0) {
        buffer.append('(');
        for (int li = 0; li < getArity(); li++) {
          if (li > 0) {
            buffer.append(',');
          }
          buffer.append(getElement(li).forWrite());
        }
        buffer.append(')');
      }

      return buffer.toString();
    } else {
      // it's an operator
      final String opName = functor.forWrite();
      final StringBuilder builder = new StringBuilder();

      final TermOperator OperatorFunctor = (TermOperator) functor;

      switch (OperatorFunctor.getOperatorType()) {
        case FX:
        case FY: {
          builder.append(opName);
          builder.append(' ');
          builder.append(getElement(0).forWrite());
        }
        break;
        case XF:
        case YF: {
          builder.append(getElement(0).forWrite());
          builder.append(' ');
          builder.append(opName);
        }
        break;
        case XFX:
        case YFX:
        case XFY: {
          builder.append(getElement(0).forWrite());
          builder.append(' ');
          builder.append(opName);
          builder.append(' ');
          builder.append(getElement(1).forWrite());
        }
        break;
        default:
          throw new ProlCriticalError("Unsupported type");
      }

      return builder.toString();
    }
  }

  public final PredicateInvoker getPredicateProcessor() {
    return this.predicateProcessor;
  }

  public void setPredicateProcessor(final PredicateInvoker processor) {
    this.predicateProcessor = requireNonNull(processor);
  }

  @Override
  public boolean isGround() {
    return this.getArity() == 0 || this.stream().allMatch(Term::isGround);
  }

  @Override
  public TermType getTermType() {
    return STRUCT;
  }

  @Override
  public String getSignature() {
    return this.structureSignature;
  }

  @Override
  public boolean unifyTo(final Term atom) {
    if (this == atom) {
      return true;
    }

    switch (atom.getTermType()) {
      case STRUCT: {
        final TermStruct thisStruct = this;
        final TermStruct thatStruct = (TermStruct) atom;

        final int arity = thisStruct.getArity();

        if (arity == thatStruct.getArity() &&
            thisStruct.getFunctor().unifyTo(thatStruct.getFunctor())) {
          for (int li = 0; li < arity; li++) {
            final Term thiselement = thisStruct.getElement(li);
            final Term thatelement = thatStruct.getElement(li);
            if (thiselement != thatelement && !thiselement.unifyTo(thatelement)) {
              return false;
            }
          }
          return true;
        }
      }
      break;
      case VAR: {
        final TermVar var = (TermVar) atom;
        final Term value = var.getValue();

        if (value == null) {
          return var.setValue(this);
        } else {
          if (value == this) {
            return true;
          }
          return value.unifyTo(this);
        }
      }
      case ATOM: {
        return this.getArity() == 0 && this.getFunctor().getText().equals(atom.getText());
      }
    }
    return false;
  }

  @Override
  public boolean dryUnifyTo(Term atom) {
    if (this == atom) {
      return true;
    }

    if (atom.getTermType() == VAR) {
      atom = ((TermVar) atom).getValue();
    }

    if (atom == null) {
      return true;
    } else {
      if (atom == this) {
        return true;
      }
    }

    if (atom.getTermType() == STRUCT) {
      final TermStruct thisStruct = this;
      final TermStruct thatStruct = (TermStruct) atom;

      final int arity = thisStruct.getArity();

      if (arity == thatStruct.getArity() &&
          thisStruct.getFunctor().dryUnifyTo(thatStruct.getFunctor())) {
        for (int li = 0; li < arity; li++) {
          if (!thisStruct.getElement(li).dryUnifyTo(thatStruct.getElement(li))) {
            return false;
          }
        }
        return true;
      }
    } else if (this.getArity() == 0 && atom.getTermType() == ATOM) {
      return this.getFunctor().getText().equals(atom.getText());
    }
    return false;
  }

  public Term makeClone() {
    return this.getArity() == 0 ? this : this.doMakeClone(new HashMap<>());
  }

  @Override
  protected Term makeCloneAndVarBound(final Map<Integer, TermVar> vars) {
    final Term result;
    if (this.getArity() == 0) {
      result = this;
    } else {
      final Term[] elements = this.getElementArray();
      final int arity = elements.length;
      final Term[] destElements = new Term[arity];

      for (int li = 0; li < arity; li++) {
        final Term element = elements[li];
        destElements[li] = element.makeCloneAndVarBound(vars);
      }
      result = Terms.newStruct(this.getFunctor(), destElements, this.getPredicateProcessor());
    }
    return result;
  }

  @Override
  protected void doArrangeVars(final Map<String, TermVar> variables) {
    final TermStruct struct = this;
    final int arity = struct.getArity();

    for (int li = 0; li < arity; li++) {
      final Term element = struct.getElement(li);
      if (element.getTermType() == VAR) {
        final TermVar thatVar = (TermVar) element;
        final String varname = thatVar.getText();
        if (!thatVar.isAnonymous() && thatVar.isFree()) {
          final TermVar var = variables.get(varname);
          if (var == null) {
            variables.put(varname, (TermVar) element);
          } else {
            struct.setElement(li, var);
          }
        }
      } else {
        element.doArrangeVars(variables);
      }
    }
  }


  @Override
  protected Term doMakeClone(Map<Integer, TermVar> vars) {
    final Term result;
    if (this.getArity() == 0) {
      result = this;
    } else {
      final Term[] elements = this.getElementArray();
      final int arity = elements.length;
      final Term[] destElements = new Term[arity];

      for (int li = 0; li < arity; li++) {
        final Term element = elements[li];
        destElements[li] = element.doMakeClone(vars);
      }
      result = Terms.newStruct(this.getFunctor(), destElements, this.getPredicateProcessor());
    }
    return result;
  }


  @Override
  public boolean hasVariableWithName(final String name) {
    final int arity = getArity();
    for (int li = 0; li < arity; li++) {
      if (getElement(li).hasVariableWithName(name)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public boolean equals(final Object obj) {
    return this == obj;
  }

  @Override
  public int hashCode() {
    return System.identityHashCode(this);
  }
} public final class TermVar extends Term {

  private static final AtomicInteger ANONYM_GENERATOR = new AtomicInteger(0);
  private static final AtomicInteger UID_GENERATOR = new AtomicInteger(0);
  private final int uid;
  private final boolean anonymous;
  private volatile Term value;

  private TermVar(final String name, final boolean anonymous) {
    super(name);
    this.uid = UID_GENERATOR.incrementAndGet();
    this.anonymous = anonymous;
  }

  TermVar(final String name) {
    this(name, false);
  }

  TermVar() {
    this("_$" + Long.toHexString(ANONYM_GENERATOR.incrementAndGet()), true);
  }

  public final int getVarUid() {
    return this.uid;
  }

  @Override
  public TermType getTermType() {
    return VAR;
  }

  @Override
  public Term makeClone() {
    final Term value = this.getThisValue();
    TermVar result = this.isAnonymous() ? newVar() : newVar(this.getText());
    if (value != null) {
      final Map<Integer, TermVar> vars = new HashMap<>();
      vars.put(this.getVarUid(), result);
      result.setThisValue(doMakeClone(vars));
    }
    return result;
  }

  @Override
  public Number toNumber() {
    final Term data = this.getValue();
    if (data instanceof NumericTerm) {
      return data.toNumber();
    }
    throw new ProlInstantiationErrorException("NonInstantiated variable", this);
  }

  @Override
  protected void doArrangeVars(final Map<String, TermVar> variables) {
    final String name = this.getText();
    if (variables.containsKey(name)) {
      final TermVar var = variables.get(name);
      this.unifyTo(var);
    } else {
      variables.put(name, this);
    }
  }


  @Override
  protected Term makeCloneAndVarBound(final Map<Integer, TermVar> vars) {
    Term value = this.getValue();
    if (value == null) {
      final Term result;
      final Term val = this.getThisValue();
      if (val == null) {
        final String varName = this.getText();
        final int varId = this.getVarUid();
        TermVar newVar = vars.get(varId);
        if (newVar == null) {
          newVar = this.isAnonymous() ? newVar() : newVar(varName);
          vars.put(varId, newVar);

          final Term thisVal = this.getThisValue();

          if (thisVal != null) {
            newVar.setThisValue(thisVal.makeCloneAndVarBound(vars));
          }
        }
        result = newVar;
      } else {
        result = val.doMakeClone(vars);
      }
      return result;
    } else {
      return value;
    }
  }

  @Override
  protected Term doMakeClone(Map<Integer, TermVar> vars) {
    final Term result;

    final Term val = this.getThisValue();
    if (val == null) {
      final String varName = this.getText();
      final int varId = this.getVarUid();
      TermVar newVar = vars.get(varId);
      if (newVar == null) {
        newVar = this.isAnonymous() ? newVar() : newVar(varName);
        vars.put(varId, newVar);

        final Term thisVal = this.getThisValue();

        if (thisVal != null) {
          newVar.setThisValue(thisVal.doMakeClone(vars));
        }
      }
      result = newVar;
    } else {
      result = val.doMakeClone(vars);
    }

    return result;
  }

  public final Term getValue() {
    Term result = this.value;
    if (result != null && result.getTermType() == VAR) {
      final TermVar nextVar = (TermVar) result;
      result = nextVar.getValue();
      if (result == null) {
        result = nextVar;
      }
    }
    return result;
  }

  public final boolean setValue(final Term value) {
    boolean result = true;

    if (value != this) {

      if (value.getTermType() == VAR) {
        // check for loop
        TermVar curVar = ((TermVar) value);
        while (!Thread.currentThread().isInterrupted()) {
          if (curVar == this) {
            // loop detected, just return
            return true;
          } else {
            final Term nextval = curVar.getThisValue();
            if (nextval != null && nextval.getTermType() == VAR) {
              curVar = (TermVar) nextval;
            } else {
              break;
            }
          }
        }
      }

      if (this.value == null) {
        this.value = value;
      } else {
        final Term curValue = getValue();
        if (curValue == null) {
          ((TermVar) this.value).setValue(value);
        } else {
          result = curValue.unifyTo(value);
        }
      }
    }
    return result;
  }

  @Override
  public Stream<TermVar> variables() {
    return this.isAnonymous() ? Stream.empty() : Stream.of(this);
  }

  public boolean isAnonymous() {
    return anonymous;
  }

  @Override
  public String toSrcString() {
    return this.getValue() == null ? (this.isAnonymous() ? "_" : this.getText()) :
        this.getValue().toSrcString();
  }

  public boolean isGround() {
    return this.value != null && this.value.isGround();
  }

  public boolean isFree() {
    return this.value == null ||
        (this.value.getTermType() == VAR && ((TermVar) this.value).isFree());
  }

  @Override
  @SuppressWarnings("unchecked")
  public <T extends Term> T findNonVarOrSame() {
    return this.value == null ? (T) this : (T) this.value.findNonVarOrDefault(this);
  }

  @Override
  public <T extends Term> T findNonVarOrDefault(final T term) {
    return this.value == null ? term : this.value.findNonVarOrDefault(term);
  }

  @Override
  public String toString() {
    final StringBuilder builder = new StringBuilder();
    final Term val = getValue();
    if (val == null) {
      builder.append(isAnonymous() ? '_' : getText());
    } else {
      builder.append(val.toString());
    }
    return builder.toString();
  }

  public Term getThisValue() {
    return value;
  }

  public void setThisValue(final Term value) {
    this.value = value;
  }

  private TermVar getDeepestVar() {
    if (this.value == null) {
      return this;
    } else {
      return this.value.getTermType() == VAR ? ((TermVar) this.value).getDeepestVar() : this;
    }
  }

  public void changeVarChainValue(final Term value) {
    this.getDeepestVar().setThisValue(value);
  }

  @Override
  public int hashCode() {
    return uid;
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == null) {
      return false;
    }
    if (this == obj) {
      return true;
    }
    if (obj.getClass() == TermVar.class) {
      final TermVar that = (TermVar) obj;
      return (uid == that.uid && that.getText().hashCode() == getText().hashCode());
    }
    return false;
  }

  @Override
  public String getSignature() {
    return ".TermVar." + getText();
  }

  @Override
  public String forWrite() {
    final Term val = this.getValue();
    if (val == null) {
      if (isAnonymous()) {
        return "_";
      } else {
        return this.getText();
      }
    } else {
      return val.forWrite();
    }
  }

  @Override
  public boolean unifyTo(final Term atom) {
    boolean result = true;
    if (this != atom) {
      final Term val = getValue();
      if (val == null) {
        result = setValue(atom);
      } else {
        result = val.unifyTo(atom);
      }
    }
    return result;
  }

  @Override
  public boolean dryUnifyTo(final Term atom) {
    if (this == atom) {
      return true;
    } else {
      final Term varValue = this.getValue();
      return varValue == null || varValue.dryUnifyTo(atom);
    }
  }

  @Override
  public boolean hasVariableWithName(final String name) {
    return getText().equals(name);
  }
} public final class Terms {
  public static final Term TRUE = new Term("true");
  public static final Term FALSE = new Term("false");
  public static final TermLong INT_ONE = new TermLong(1L);
  public static final TermLong INT_ZERO = new TermLong(0L);
  public static final TermLong INT_MINUS_ONE = new TermLong(-1L);
  public static final Term LIST_FUNCTOR = new Term(".");
  public static final TermList NULL_LIST = new TermList();

  public static Term newAtom(final String text) {
    if (".".equals(text)) {
      return LIST_FUNCTOR;
    } else {
      return new Term(text);
    }
  }

  public static TermDouble newDouble(final String text) {
    return new TermDouble(text);
  }

  public static TermDouble newDouble(final double value) {
    return new TermDouble(value);
  }

  public static TermLong newLong(final String text) {
    return new TermLong(text);
  }

  public static TermLong newLong(final long value) {
    if (value == 0L) {
      return INT_ZERO;
    } else if (value == 1L) {
      return INT_ONE;
    } else if (value == -1L) {
      return INT_MINUS_ONE;
    } else {
      return new TermLong(value);
    }
  }

  public static TermList newList(final Term term) {
    return new TermList(term);
  }

  public static TermList newList(final Term head, final Term tail) {
    return new TermList(head, tail);
  }

  public static TermVar newVar(final String name) {
    return new TermVar(name);
  }

  public static TermVar newVar() {
    return new TermVar();
  }

  public static TermStruct newStruct(final Term functor) {
    return new TermStruct(functor);
  }

  public static TermStruct newStruct(final String functor, final Term[] elements) {
    return new TermStruct(functor, elements);
  }

  public static TermStruct newStruct(final Term functor, final Term[] elements) {
    return new TermStruct(functor, elements);
  }

  public static TermStruct newStruct(final Term functor, final Term[] elements,
                                     final PredicateInvoker processor) {
    return new TermStruct(functor, elements, processor);
  }

  private static Term convert(final JProlContext context, final PrologTerm term,
                              final Map<String, TermVar> vars) {
    switch (term.getType()) {
      case ATOM: {
        if (term instanceof PrologNumeric) {
          if (term instanceof PrologFloat) {
            return newDouble(((PrologFloat) term).getFloatValue().doubleValue());
          } else {
            return newLong(((PrologInt) term).getIntValue().longValue());
          }
        } else {
          final String text = term.getText();
          if (context.hasZeroArityPredicateForName(text)) {
            final TermStruct result = newStruct(text, new Term[0]);
            result.setPredicateProcessor(context.findProcessor(result));
            return result;
          } else {
            return newAtom(text);
          }
        }
      }
      case OPERATOR: {
        return context.getKnowledgeBase().findOperatorForName(context, term.getText())
            .getForTypePrecisely(((Op) term).getAssoc());
      }
      case LIST: {
        final PrologList list = (PrologList) term;
        return list.isEmpty() ? NULL_LIST :
            newList(convert(context, list.getHead(), vars), convert(context, list.getTail(), vars));
      }
      case STRUCT: {
        final PrologStruct struct = (PrologStruct) term;

        if (struct.isBlock()) {
          return convert(context, struct.getTermAt(0), vars);
        } else {
          final TermStruct result;
          final int arity = struct.getArity();
          if (arity == 0) {
            result = newStruct(convert(context, struct.getFunctor(), vars));
          } else {
            final Term[] terms = new Term[arity];
            for (int i = 0; i < arity; i++) {
              terms[i] = convert(context, struct.getTermAt(i), vars);
            }
            result = newStruct(convert(context, struct.getFunctor(), vars), terms);
          }
          result.setPredicateProcessor(context.findProcessor(result));
          return result;
        }
      }
      case VAR: {
        if (((PrologVar) term).isAnonymous()) {
          return newVar();
        } else {
          return vars.computeIfAbsent(term.getText(), Terms::newVar);
        }
      }
      default:
        throw new IllegalArgumentException("Unexpected parsed prolog term: " + term);
    }
  }

  public static Term fromParsed(final JProlContext context, final PrologTerm term) {
    if (term.getType() == com.igormaznitsa.prologparser.terms.TermType.ATOM
        || term.getType() == com.igormaznitsa.prologparser.terms.TermType.OPERATOR) {
      return convert(context, term, null);
    } else {
      return convert(context, term, new HashMap<>());
    }
  }

} public abstract class ProlAbstractCatcheableException extends ProlException {

  static final Term UNDEFINED = newAtom("<undefined>");
  private static final String ERROR_FUNCTOR = "error";
  private static final long serialVersionUID = 6911111912695145529L;
  private final Term culprit;

  public ProlAbstractCatcheableException(final Term culprit) {
    this.culprit = Objects.requireNonNull(culprit);
  }

  public ProlAbstractCatcheableException(final String message, final Term culprit) {
    super(message);
    this.culprit = culprit == null ? UNDEFINED : culprit.makeCloneAndVarBound();
  }

  public ProlAbstractCatcheableException(final String message, final Term culprit,
                                         final Throwable cause) {
    super(message, cause);
    this.culprit = culprit == null ? UNDEFINED : culprit.makeCloneAndVarBound();
  }

  public ProlAbstractCatcheableException(final Term culprit, final Throwable cause) {
    super(cause);
    this.culprit = culprit == null ? UNDEFINED : culprit.makeCloneAndVarBound();
  }

  public abstract Term getErrorTerm();

  public Term getCulprit() {
    return this.culprit.makeClone();
  }

  protected TermStruct makeErrorStruct(final Term formal, final Term context) {
    return newStruct(ERROR_FUNCTOR, new Term[] {formal, context});
  }

  public abstract TermStruct getAsStruct();
} public class ProlCriticalError extends Error {

  private static final long serialVersionUID = 1340153646373377400L;

  public ProlCriticalError(final Throwable cause) {
    super(cause);
  }

  public ProlCriticalError(final String message, final Throwable cause) {
    super(message, cause);
  }

  public ProlCriticalError(final String message) {
    super(message);
  }

  public ProlCriticalError() {
    super();
  }

} public class ProlCustomErrorException extends ProlAbstractCatcheableException {

  private static final long serialVersionUID = -4720280738591345468L;

  private final Term error;

  public ProlCustomErrorException(final Term error, final Term culprit) {
    super(culprit);
    if (error == null) {
      throw new IllegalArgumentException("Error must not be null");
    }
    this.error = error;
  }

  @Override
  public Term getErrorTerm() {
    return this.error;
  }

  @Override
  public TermStruct getAsStruct() {
    return this.makeErrorStruct(this.error, this.getCulprit());
  }

} public class ProlDomainErrorException extends ProlAbstractCatcheableException {

  private static final long serialVersionUID = -7481773982226534683L;

  private static final Term ERROR_TERM = newAtom("domain_error");

  private final String validDomain;

  public ProlDomainErrorException(final String validDomain, final Term culprit,
                                  final Throwable cause) {
    super(culprit, cause);
    this.validDomain = validDomain == null ? UNDEFINED.getText() : validDomain;
  }

  public ProlDomainErrorException(final String validDomain, final String message,
                                  final Term culprit, final Throwable cause) {
    super(message, culprit, cause);
    this.validDomain = validDomain == null ? UNDEFINED.getText() : validDomain;
  }

  public ProlDomainErrorException(final String validDomain, final String message,
                                  final Term culprit) {
    super(message, culprit);
    this.validDomain = validDomain == null ? UNDEFINED.getText() : validDomain;
  }

  public ProlDomainErrorException(final String validDomain, final Term culprit) {
    super(culprit);
    this.validDomain = validDomain == null ? UNDEFINED.getText() : validDomain;
  }

  @Override
  public String getMessage() {
    return String.format("%s: detected = %s, expected = %s",
        (super.getMessage() == null ? "domain_error" : super.getMessage()),
        this.getCulprit().getText(), (this.validDomain == null ? "not defined" : this.validDomain));
  }

  public String getValidDomain() {
    return this.validDomain;
  }

  @Override
  public Term getErrorTerm() {
    return ERROR_TERM;
  }

  @Override
  public TermStruct getAsStruct() {
    return this.makeErrorStruct(ERROR_TERM,
        TermList.asTermList(ERROR_TERM, newAtom(this.validDomain), this.getCulprit()));
  }
} public class ProlEvaluationErrorException extends ProlAbstractCatcheableException {

  private static final long serialVersionUID = -8773616695972049425L;

  private static final Term TERM_ERROR = newAtom("evaluation_error");

  private final String error;

  public ProlEvaluationErrorException(final String error, final Term culprit,
                                      final Throwable cause) {
    super(culprit, cause);
    this.error = error == null ? UNDEFINED.getText() : error;
  }

  public ProlEvaluationErrorException(final String error, final String message, final Term culprit,
                                      final Throwable cause) {
    super(message, culprit, cause);
    this.error = error == null ? UNDEFINED.getText() : error;
  }

  public ProlEvaluationErrorException(final String error, final String message,
                                      final Term culprit) {
    super(message, culprit);
    this.error = error == null ? UNDEFINED.getText() : error;
  }

  public ProlEvaluationErrorException(final String error, final Term culprit) {
    super(culprit);
    this.error = error;
  }

  public String getError() {
    return this.error;
  }


  @Override
  public Term getErrorTerm() {
    return TERM_ERROR;
  }

  @Override
  public TermStruct getAsStruct() {
    return this
        .makeErrorStruct(TERM_ERROR, TermList.asTermList(newAtom(this.error), this.getCulprit()));
  }
} public class ProlHaltExecutionException extends ProlException {

  private static final long serialVersionUID = -8265930292043474634L;

  private final long status;

  public ProlHaltExecutionException() {
    super("The program is halted.");
    this.status = 0L;
  }

  public ProlHaltExecutionException(final long status) {
    super("The program is halted.");
    this.status = status;
  }

  public ProlHaltExecutionException(final String cause, final long status) {
    super(cause);
    this.status = status;
  }

  public long getStatus() {
    return this.status;
  }
} public class ProlInstantiationErrorException extends ProlAbstractCatcheableException {

  private static final long serialVersionUID = -5157739502740121566L;

  private static final Term TERM_ERROR = newAtom("instantiation_error");

  public ProlInstantiationErrorException(final Term culprit, final Throwable cause) {
    super(culprit, cause);
  }

  public ProlInstantiationErrorException(final String message, final Term culprit,
                                         final Throwable cause) {
    super(message, culprit, cause);
  }

  public ProlInstantiationErrorException(final String message, final Term culprit) {
    super(message, culprit);
  }

  public ProlInstantiationErrorException(final Term culprit) {
    super(culprit);
  }


  @Override
  public Term getErrorTerm() {
    return TERM_ERROR;
  }

  @Override
  public TermStruct getAsStruct() {
    return this.makeErrorStruct(TERM_ERROR, this.getCulprit());
  }
} public class ProlPermissionErrorException extends ProlAbstractCatcheableException {

  private static final long serialVersionUID = 7752699517532895102L;

  private static final Term TERM_ERROR = newAtom("permission_error");
  private final String operation;

  private final String permissionType;

  public ProlPermissionErrorException(final String operation, final String permissionType,
                                      final Term culprit, final Throwable cause) {
    super(culprit, cause);
    this.operation = operation == null ? UNDEFINED.getText() : operation;
    this.permissionType = permissionType == null ? UNDEFINED.getText() : permissionType;
  }

  public ProlPermissionErrorException(final String operation, final String permissionType,
                                      final String message, final Term culprit,
                                      final Throwable cause) {
    super(message, culprit, cause);
    this.operation = operation == null ? UNDEFINED.getText() : operation;
    this.permissionType = permissionType == null ? UNDEFINED.getText() : permissionType;
  }

  public ProlPermissionErrorException(final String operation, final String permissionType,
                                      final String message, final Term culprit) {
    super(message, culprit);
    this.operation = operation == null ? UNDEFINED.getText() : operation;
    this.permissionType = permissionType == null ? UNDEFINED.getText() : permissionType;
  }

  public ProlPermissionErrorException(final String operation, final String permissionType,
                                      final Term culprit) {
    super(culprit);
    this.operation = operation == null ? UNDEFINED.getText() : operation;
    this.permissionType = permissionType == null ? UNDEFINED.getText() : permissionType;
  }

  public String getOperation() {
    return operation;
  }

  public String getPermissionType() {
    return permissionType;
  }

  @Override
  public Term getErrorTerm() {
    return TERM_ERROR;
  }

  @Override
  public TermStruct getAsStruct() {
    return this.makeErrorStruct(TERM_ERROR,
        TermList.asTermList(newAtom(this.operation), newAtom(permissionType), getCulprit()));
  }

} public class ProlRepresentationErrorException extends ProlAbstractCatcheableException {

  private static final long serialVersionUID = -4337637438675029939L;

  private static final Term TERM_ERROR = newAtom("representation_error");
  private final String errorCode;

  public ProlRepresentationErrorException(final String errorCode, final Term culprit,
                                          final Throwable cause) {
    super(culprit, cause);
    this.errorCode = errorCode == null ? UNDEFINED.getText() : errorCode;
  }

  public ProlRepresentationErrorException(final String errorCode, final String message,
                                          final Term culprit, final Throwable cause) {
    super(message, culprit, cause);
    this.errorCode = errorCode == null ? UNDEFINED.getText() : errorCode;
  }

  public ProlRepresentationErrorException(final String errorCode, final String message,
                                          final Term culprit) {
    super(message, culprit);
    this.errorCode = errorCode == null ? UNDEFINED.getText() : errorCode;
  }

  public ProlRepresentationErrorException(final String flag, final Term culprit) {
    super(culprit);
    this.errorCode = flag == null ? UNDEFINED.getText() : flag;
  }

  public String getErrorCode() {
    return errorCode;
  }

  @Override
  public Term getErrorTerm() {
    return TERM_ERROR;
  }

  @Override
  public TermStruct getAsStruct() {
    return this.makeErrorStruct(TERM_ERROR,
        TermList.asTermList(newAtom(this.errorCode), this.getCulprit()));
  }
} public class ProlTypeErrorException extends ProlAbstractCatcheableException {

  private static final long serialVersionUID = -3258214534404942716L;

  private static final Term TERM_ERROR = newAtom("type_error");

  private final String validType;

  public ProlTypeErrorException(final String validType, final Term culprit, final Throwable cause) {
    super(culprit, cause);
    this.validType = validType == null ? UNDEFINED.getText() : validType;
  }

  public ProlTypeErrorException(final String validType, final String message, final Term culprit,
                                final Throwable cause) {
    super(message, culprit, cause);
    this.validType = validType == null ? UNDEFINED.getText() : validType;
  }

  public ProlTypeErrorException(final String validType, final String message, final Term culprit) {
    super(message, culprit);
    this.validType = validType == null ? UNDEFINED.getText() : validType;
  }

  public ProlTypeErrorException(final String validType, final Term culprit) {
    super(culprit);
    this.validType = validType == null ? UNDEFINED.getText() : validType;
  }

  public String getValidType() {
    return validType;
  }


  @Override
  public Term getErrorTerm() {
    return TERM_ERROR;
  }

  @Override
  public TermStruct getAsStruct() {
    return this.makeErrorStruct(TERM_ERROR, asTermList(newAtom(this.validType), getCulprit()));
  }

} public enum IteratorType {
  FACTS,
  RULES,
  ANY
} public interface KnowledgeBase {

  String getId();

  boolean removeOperator(String name, OpAssoc type);

  void addOperator(JProlContext context, TermOperator operator);

  TermOperatorContainer findOperatorForName(JProlContext context, String name);

  boolean hasOperatorStartsWith(JProlContext context, String str);

  CloseableIterator<TermStruct> iterate(IteratorType type, TermStruct template,
                                        Consumer<String> unknownPredicateConsumer);

  CloseableIterator<TermStruct> iterate(String signature,
                                        Consumer<String> unknownPredicateConsumer);

  CloseableIterator<TermStruct> iterateSignatures(TermStruct indicator);

  CloseableIterator<TermOperator> makeOperatorIterator();

  boolean assertZ(JProlContext context, TermStruct clause);

  boolean assertA(JProlContext context, TermStruct clause);

  boolean retractAll(JProlContext context, TermStruct clause);

  boolean retractA(JProlContext context, TermStruct clause);

  boolean retractZ(JProlContext context, TermStruct clause);

  void abolish(JProlContext context, String signature);

  KnowledgeBase makeCopy();
} public final class JProlChoicePoint implements Comparator<Term> {

  private static final Consumer<String> NULL_UNDEFINED_PREDICATE_CONSUMER = x -> {
  };
  private final Map<String, TermVar> variables;
  private final VariableStateSnapshot varSnapshot;
  private final JProlContext context;
  private final JProlChoicePoint rootChoicePoint;
  private final Term goalTerm;
  private final boolean validate;
  private final boolean debug;
  private boolean thereAreVariants;
  private Object payload;
  private JProlChoicePoint prevCp;
  private JProlChoicePoint rootLastGoalAtChain;
  private JProlChoicePoint subCp;
  private Term subChoicePointConnector;
  private Term thisConnector;
  private Term nextAndTerm;
  private Term nextAndTermForNextGoal;
  private Iterator<TermStruct> clauseIterator;
  private boolean cutMeet;
  private boolean notFirstProve;

  private JProlChoicePoint(
      final JProlChoicePoint rootChoicePoint,
      final Term goalToSolve,
      final JProlContext context,
      final boolean debug,
      final boolean validate,
      final Map<String, Term> presetVarValues
  ) {
    this.thereAreVariants = true;
    this.validate = validate;
    this.debug = debug;

    this.rootChoicePoint = rootChoicePoint == null ? this : rootChoicePoint;
    this.goalTerm = goalToSolve.getTermType() == ATOM ? newStruct(goalToSolve) : goalToSolve;
    this.context = context;

    final Term goal = goalToSolve.findNonVarOrSame();

    if (this.isArgsValidate()) {
      ProlAssertions.assertCallable(goal);
    }

    if (rootChoicePoint == null) {
      if (goal.getTermType() == ATOM) {
        this.varSnapshot = null;
        this.variables = null;
      } else {
        this.variables = goal.allNamedVarsAsMap();
        this.varSnapshot = new VariableStateSnapshot(goal, presetVarValues);
      }
      this.rootLastGoalAtChain = this;
      this.prevCp = null;
    } else {
      this.variables = null;
      if (goal.getTermType() == ATOM) {
        this.varSnapshot = null;
      } else {
        this.varSnapshot = new VariableStateSnapshot(rootChoicePoint.varSnapshot);
      }
      this.prevCp = rootChoicePoint.rootLastGoalAtChain;
      rootChoicePoint.rootLastGoalAtChain = this;
    }
  }

  public JProlChoicePoint(final String goal, final JProlContext context) {
    this(new JProlTreeBuilder(context).readPhraseAndMakeTree(new StringReader(goal)).term, context,
        null);
  }

  public JProlChoicePoint(final Term goal, final JProlContext context) {
    this(null, goal, context, context.isDebug(), context.isTemplateValidate(), null);
  }

  public JProlChoicePoint(final Term goal, final JProlContext context,
                          final Map<String, Term> predefinedVarValues) {
    this(null, goal, context, context.isDebug(), context.isTemplateValidate(), predefinedVarValues);
  }

  public JProlChoicePoint makeForGoal(final Term goal) {
    return new JProlChoicePoint(null, goal, this.context, this.debug, this.validate, null);
  }

  public boolean isDebug() {
    return this.debug;
  }

  public boolean isArgsValidate() {
    return this.validate;
  }

  public Map<String, Term> findAllGroundedVars() {
    return this.variables.entrySet()
        .stream()
        .filter(v -> v.getValue().isGround())
        .collect(toMap(Map.Entry::getKey, e -> e.getValue().makeClone()));
  }

  @Override
  public String toString() {
    return (this.isCompleted() ? "Completed " : "Active ") + "Goal(" + this.goalTerm.toString() +
        ')';
  }

  public Optional<TermVar> findVar(final String name) {
    return this.variables == null ? Optional.empty() :
        Optional.ofNullable(this.variables.get(requireNonNull(name)));
  }

  public JProlChoicePoint replaceLastGoalAtChain(final Term goal) {
    if (this.debug) {
      this.context.fireTraceEvent(EXIT, this.rootChoicePoint.rootLastGoalAtChain);
    }

    final JProlChoicePoint newGoal =
        new JProlChoicePoint(this.rootChoicePoint, goal, this.context, this.debug, this.validate,
            null);
    final JProlChoicePoint prevGoal = newGoal.prevCp;
    if (prevGoal != null) {
      newGoal.prevCp = prevGoal.prevCp;
      newGoal.nextAndTerm = prevGoal.nextAndTerm;
      newGoal.nextAndTermForNextGoal = prevGoal.nextAndTermForNextGoal;
    }
    return newGoal;
  }

  @SuppressWarnings("unchecked")
  public <T> T getPayload() {
    return (T) this.payload;
  }

  public void setPayload(final Object obj) {
    this.payload = obj;
  }

  public Term getGoalTerm() {
    return this.goalTerm;
  }

  public JProlContext getContext() {
    return this.context;
  }

  public Term prove() {
    return this.proveNext(x -> this.context.notifyAboutUndefinedPredicate(this, x));
  }

  public Term proveWithFailForUnknown() {
    return this.proveNext(NULL_UNDEFINED_PREDICATE_CONSUMER);
  }

  private Term proveNext(final Consumer<String> unknownPredicateConsumer) {
    Term result = null;

    boolean loop = true;

    while (loop && !Thread.currentThread().isInterrupted()) {
      if (this.context.isDisposed()) {
        throw new ProlHaltExecutionException();
      }

      JProlChoicePoint goalToProcess = this.rootChoicePoint.rootLastGoalAtChain;
      if (goalToProcess == null) {
        break;
      } else {
        if (goalToProcess.thereAreVariants) {
          switch (goalToProcess.resolve(unknownPredicateConsumer)) {
            case FAIL: {
              if (this.debug) {
                this.context.fireTraceEvent(TraceEvent.FAIL, goalToProcess);
                this.context.fireTraceEvent(EXIT, goalToProcess);
              }
              this.rootChoicePoint.rootLastGoalAtChain = goalToProcess.prevCp;
            }
            break;
            case SUCCESS: {
              // we have to renew data about last chain goal because it can be changed during the operation
              goalToProcess = this.rootChoicePoint.rootLastGoalAtChain;

              if (goalToProcess.nextAndTerm == null) {
                result = this.rootChoicePoint.goalTerm;
                loop = false;
              } else {
                final JProlChoicePoint nextGoal =
                    new JProlChoicePoint(this.rootChoicePoint, goalToProcess.nextAndTerm,
                        this.context, this.debug, this.validate, null);
                nextGoal.nextAndTerm = goalToProcess.nextAndTermForNextGoal;
              }
            }
            break;
            case STACK_CHANGED: {
            }
            break;
            default:
              throw new Error("Unexpected status");
          }
        } else {
          if (this.debug) {
            this.context.fireTraceEvent(EXIT, goalToProcess);
          }
          this.rootChoicePoint.rootLastGoalAtChain = goalToProcess.prevCp;
        }
      }
    }

    return result;
  }

  private JProlChoicePointResult resolve(final Consumer<String> unknownPredicateConsumer) {
    if (Thread.currentThread().isInterrupted()) {
      return JProlChoicePointResult.FAIL;
    }
    final TraceEvent traceEvent;
    if (this.notFirstProve) {
      traceEvent = TraceEvent.REDO;
    } else {
      traceEvent = TraceEvent.CALL;
      this.notFirstProve = true;
    }
    if (this.debug) {
      this.context.fireTraceEvent(traceEvent, this);
    }

    JProlChoicePointResult result = JProlChoicePointResult.FAIL;

    boolean doLoop = true;

    while (doLoop) {
      // reset variables to their initial state
      if (this.varSnapshot != null) {
        this.varSnapshot.resetToState();
      }

      if (this.subCp != null) {
        // solve subgoal
        final Term solvedTerm = this.subCp.proveNext(unknownPredicateConsumer);

        if (this.subCp.cutMeet) {
          this.clauseIterator = null;
        }

        if (solvedTerm == null) {
          this.subCp = null;
          if (this.clauseIterator == null) {
            break;
          }
        } else {
          if (!this.thisConnector.unifyTo(this.subChoicePointConnector)) {
            throw new ProlCriticalError("Critical error #980234");
          }
          result = JProlChoicePointResult.SUCCESS;
          break;
        }
      }

      if (this.clauseIterator != null) {
        // next clause
        if (this.clauseIterator.hasNext()) {
          final TermStruct nextClause = this.clauseIterator.next();

          final Term goalTermForEqu;
          if (((TermStruct) this.goalTerm).isClause()) {
            goalTermForEqu = ((TermStruct) this.goalTerm).getElement(0).makeClone();
          } else {
            goalTermForEqu = this.goalTerm.makeClone();
          }

          if (!goalTermForEqu
              .unifyTo(nextClause.isClause() ? nextClause.getElement(0) : nextClause)) {
            throw new ProlCriticalError(
                "Unexpectedly can't unify term with prvided by knowledge base!");
          }

          if (nextClause.isClause()) {
            this.thisConnector = goalTerm;
            this.subChoicePointConnector = nextClause.getElement(0);
            this.subCp = new JProlChoicePoint(nextClause.getElement(1), this.context);
            continue;
          } else {
            if (!this.goalTerm.unifyTo(nextClause)) {
              throw new ProlCriticalError("Impossible situation #0009824");
            }
            result = JProlChoicePointResult.SUCCESS;
            break;
          }
        } else {
          this.clauseIterator = null;
          cutVariants();
          break;
        }
      }

      switch (this.goalTerm.getTermType()) {
        case ATOM: {
          final String text = this.goalTerm.getText();
          if (this.context.hasZeroArityPredicateForName(text)) {
            result = JProlChoicePointResult.SUCCESS;
          } else {
            this.context.notifyAboutUndefinedPredicate(this, this.goalTerm.getSignature());
            result = JProlChoicePointResult.FAIL;
          }
          cutVariants();
          doLoop = false;
        }
        break;
        case STRUCT: {
          final TermStruct struct = (TermStruct) goalTerm;
          final int arity = struct.getArity();

          if (struct.isClause()) {
            final TermStruct structClone = (TermStruct) struct.makeClone();

            this.thisConnector = struct.getElement(0);
            this.subChoicePointConnector = structClone.getElement(0);

            if (arity == 1) {
              this.subCp = new JProlChoicePoint(structClone.getElement(0), this.context);
            } else {
              this.subCp = new JProlChoicePoint(structClone.getElement(1), this.context);
            }
          } else {

            final Term functor = struct.getFunctor();
            final String functorText = functor.getText();

            boolean nonConsumed = true;

            if (arity == 0) {
              final int len = functorText.length();
              if (len == 1 && functorText.charAt(0) == '!') {
                // cut
                cut();
                nonConsumed = false;
                doLoop = false;
                result = JProlChoicePointResult.SUCCESS;
                this.thereAreVariants = false;
              }
            } else if (arity == 2) {
              final int textLen = functorText.length();
              if (textLen == 1) {
                if (functorText.charAt(0) == ',') {// and
                  final JProlChoicePoint leftSubgoal = replaceLastGoalAtChain(struct.getElement(0));
                  leftSubgoal.nextAndTerm = struct.getElement(1);
                  leftSubgoal.nextAndTermForNextGoal = this.nextAndTerm;

                  result = JProlChoicePointResult.STACK_CHANGED;

                  doLoop = false;
                  nonConsumed = false;
                } else if (functorText.charAt(0) == ';') {// or
                  if (getPayload() == null) {
                    final JProlChoicePoint leftSubbranch =
                        new JProlChoicePoint(this.rootChoicePoint, struct.getElement(0),
                            this.context, this.debug, this.validate, null);
                    leftSubbranch.nextAndTerm = this.nextAndTerm;
                    setPayload(leftSubbranch);
                  } else {
                    replaceLastGoalAtChain(struct.getElement(1));
                  }
                  result = JProlChoicePointResult.STACK_CHANGED;
                  nonConsumed = false;
                  doLoop = false;
                }
              }
            }

            if (nonConsumed) {
              final PredicateInvoker foundProcessor = findProcessorInLibraries(struct);
              if (foundProcessor == PredicateInvoker.NULL_PROCESSOR) {
                this.clauseIterator = this.context.getKnowledgeBase().iterate(
                    IteratorType.ANY,
                    struct,
                    unknownPredicateConsumer
                );
                if (!this.clauseIterator.hasNext()) {
                  doLoop = false;
                  this.cutVariants();
                  result = JProlChoicePointResult.FAIL;
                }
              } else {
                if (foundProcessor.isEvaluable() || foundProcessor.isDetermined()) {
                  this.cutVariants();
                }

                if (foundProcessor.execute(this, struct)) {
                  result = JProlChoicePointResult.SUCCESS;
                } else {
                  result = JProlChoicePointResult.FAIL;
                }

                if (result == JProlChoicePointResult.SUCCESS &&
                    foundProcessor.doesChangeGoalChain()) {
                  result = JProlChoicePointResult.STACK_CHANGED;
                }

                doLoop = false;
              }
            }
          }
        }
        break;
        default: {
          result = JProlChoicePointResult.FAIL;
          doLoop = false;
          this.cutVariants();
        }
        break;
      }
    }

    return result;
  }

  public void cutVariants() {
    this.thereAreVariants = false;
  }

  public void cut() {
    this.rootChoicePoint.cutMeet = true;
    this.rootChoicePoint.clauseIterator = null;
    this.prevCp = null;
  }

  public void cutLocally() {
    this.prevCp = null;
  }

  private PredicateInvoker findProcessorInLibraries(final TermStruct structure) {
    PredicateInvoker processor = structure.getPredicateProcessor();
    if (processor == null) {
      processor = this.context.findProcessor(structure);
      structure.setPredicateProcessor(processor);
    }
    return processor;
  }

  public boolean isCompleted() {
    return this.rootChoicePoint.rootLastGoalAtChain == null || !this.thereAreVariants;
  }

  @Override
  public int compare(Term term1, Term term2) {
    if (term1 == term2) {
      return 0;
    }

    term1 = term1.findNonVarOrSame();
    term2 = term2.findNonVarOrSame();

    final int result;
    switch (term1.findNonVarOrSame().getTermType()) {
      case ATOM: {
        if (term2 instanceof CompoundTerm) {
          result = -1;
        } else if (term2.getTermType() == ATOM) {
          if (term1 instanceof NumericTerm) {
            if (term2 instanceof NumericTerm) {
              if (term1 instanceof TermDouble || term2 instanceof TermDouble) {
                result =
                    Double.compare(term1.toNumber().doubleValue(), term2.toNumber().doubleValue());
              } else {
                result = Long.compare(term1.toNumber().longValue(), term2.toNumber().longValue());
              }
            } else {
              result = -1;
            }
          } else {
            result = term2 instanceof NumericTerm ? 1 : term1.getText().compareTo(term2.getText());
          }
        } else {
          result = 1;
        }
      }
      break;
      case LIST:
      case STRUCT: {
        if (term2 instanceof CompoundTerm) {
          final TermStruct struct1 = (TermStruct) term1;
          final TermStruct struct2 = (TermStruct) term2;
          int res = Integer.compare(struct1.getArity(), struct2.getArity());
          if (res == 0) {
            res = struct1.getFunctor().getText().compareTo(struct2.getFunctor().getText());
            if (res == 0) {
              for (int i = 0; i < struct1.getArity() && res == 0; i++) {
                res = this.compare(struct1.getElement(i), struct2.getElement(i));
              }
            }
          }
          result = res;
        } else {
          result = 1;
        }
      }
      break;
      case VAR: {
        if (term2.getTermType() == VAR) {
          result = term1.getText().compareTo(term2.getText());
        } else {
          result = -1;
        }
      }
      break;
      default:
        result = 1;
    }
    return result;
  }
} public final class JProlContext {
  private final String contextId;

  private final Map<String, List<JProlTrigger>> triggersOnAssert = new ConcurrentHashMap<>();
  private final Map<String, List<JProlTrigger>> triggersOnRetract = new ConcurrentHashMap<>();
  private final Map<String, ReentrantLock> namedLockers = new ConcurrentHashMap<>();
  private final List<AbstractJProlLibrary> libraries = new CopyOnWriteArrayList<>();
  private final AtomicBoolean disposed = new AtomicBoolean(false);
  private final KnowledgeBase knowledgeBase;
  private final ExecutorService executorService;
  private final List<JProlContextListener> contextListeners = new CopyOnWriteArrayList<>();
  private final Map<JProlSystemFlag, Term> systemFlags = new ConcurrentHashMap<>();
  private final AtomicInteger asyncTaskCounter = new AtomicInteger();

  private final ParserContext parserContext = new ParserContext() {
    @Override
    public boolean hasOpStartsWith(final PrologParser prologParser, final String s) {
      return JProlContext.this.knowledgeBase.hasOperatorStartsWith(JProlContext.this, s);
    }

    @Override
    public OpContainer findOpForName(final PrologParser prologParser, final String s) {
      final TermOperatorContainer container =
          JProlContext.this.knowledgeBase.findOperatorForName(JProlContext.this, s);
      return container == null ? null : container.asOpContainer();
    }

    @Override
    public int getFlags() {
      return ParserContext.FLAG_ZERO_STRUCT
          | ParserContext.FLAG_ZERO_QUOTATION_CHARCODE
          | ParserContext.FLAG_ZERO_QUOTATION_ALLOWS_WHITESPACE_CHAR
          | ParserContext.FLAG_BLOCK_COMMENTS;
    }
  };

  private final List<IoResourceProvider> ioProviders = new CopyOnWriteArrayList<>();
  private boolean templateValidate;
  private boolean debug;
  private UndefinedPredicateBehavior undefinedPredicateBehaviour;

  public JProlContext(final String name, final AbstractJProlLibrary... libs) {
    this(
        name,
        new InMemoryKnowledgeBase(name + "_kbase"),
        ForkJoinPool.commonPool(),
        emptyMap(),
        emptyList(),
        emptyList(),
        libs
    );
  }

  private JProlContext(
      final String contextId,
      final KnowledgeBase base,
      final ExecutorService executorService,
      final Map<JProlSystemFlag, Term> systemFlags,
      final List<JProlContextListener> contextListeners,
      final List<IoResourceProvider> ioProviders,
      final AbstractJProlLibrary... additionalLibraries
  ) {
    this.contextId = requireNonNull(contextId, "Context Id is null");
    this.knowledgeBase = requireNonNull(base, "Knowledge base is null");
    this.executorService = requireNonNull(executorService);
    this.contextListeners.addAll(contextListeners);

    Arrays.stream(JProlSystemFlag.values())
        .filter(x -> !x.isReadOnly())
        .forEach(x -> this.systemFlags.put(x, x.getDefaultValue()));

    this.systemFlags.putAll(systemFlags);
    this.onSystemFlagsUpdated();

    this.ioProviders.addAll(ioProviders);

    this.libraries.add(new JProlBootstrapLibrary());

    asList(additionalLibraries).forEach(this::addLibrary);
  }

  public boolean isTemplateValidate() {
    return this.templateValidate;
  }

  public UndefinedPredicateBehavior getUndefinedPredicateBehavior() {
    return this.undefinedPredicateBehaviour;
  }

  public boolean isDebug() {
    return this.debug;
  }

  public Term getSystemFlag(final JProlSystemFlag flag) {
    return this.systemFlags.getOrDefault(flag, flag.getDefaultValue());
  }

  public void setSystemFlag(final JProlSystemFlag flag, final Term term) {
    if (flag.isReadOnly()) {
      throw new IllegalStateException("Flag is marked as read-only: " + flag);
    } else {
      final Term value = Objects.requireNonNull(term.findNonVarOrDefault(null));
      this.systemFlags.put(flag, value);
      this.onSystemFlagsUpdated();
    }
  }

  private void onSystemFlagsUpdated() {
    this.templateValidate =
        Boolean.parseBoolean(this.systemFlags.get(JProlSystemFlag.VERIFY).getText());
    this.debug = Boolean.parseBoolean(this.systemFlags.get(JProlSystemFlag.DEBUG).getText());
    this.undefinedPredicateBehaviour = UndefinedPredicateBehavior
        .find(this.systemFlags.get(JProlSystemFlag.UNKNOWN).getText())
        .orElseThrow(() -> new ProlDomainErrorException(
            Arrays.toString(UndefinedPredicateBehavior.values()),
            this.systemFlags.get(JProlSystemFlag.UNKNOWN))
        );
  }

  public JProlContext addContextListener(final JProlContextListener listener) {
    this.contextListeners.add(listener);
    return this;
  }

  public JProlContext removeContextListener(final JProlContextListener listener) {
    this.contextListeners.remove(listener);
    return this;
  }

  public JProlContext addIoResourceProvider(final IoResourceProvider provider) {
    this.ioProviders.add(provider);
    return this;
  }

  public JProlContext removeIoResourceProvider(final IoResourceProvider provider) {
    this.ioProviders.remove(provider);
    return this;
  }

  public Optional<Reader> findResourceReader(final String readerId) {
    return this.ioProviders.stream().map(x -> x.findReader(this, readerId)).filter(Objects::nonNull)
        .findFirst();
  }

  public Optional<Writer> findResourceWriter(final String writerId, final boolean append) {
    return this.ioProviders.stream().map(x -> x.findWriter(this, writerId, append))
        .filter(Objects::nonNull).findFirst();
  }

  public final String getName() {
    return this.contextId;
  }

  void fireTraceEvent(final TraceEvent event, final JProlChoicePoint choicePoint) {
    if (!this.contextListeners.isEmpty()) {
      this.contextListeners.forEach(l -> l.onChoicePointTraceEvent(this, choicePoint, event));
    }
  }

  public int getCurrentAsyncTaskNumber() {
    return this.asyncTaskCounter.get();
  }

  public void waitAllAsyncTasks() {
    while (this.asyncTaskCounter.get() > 0 && !Thread.currentThread().isInterrupted()) {
      synchronized (this.asyncTaskCounter) {
        try {
          this.asyncTaskCounter.wait();
        } catch (InterruptedException ex) {
          Thread.currentThread().interrupt();
          return;
        }
      }
    }
  }

  private void onAsyncTaskCompleted(final Term goal) {
    this.asyncTaskCounter.decrementAndGet();
    synchronized (this.asyncTaskCounter) {
      this.asyncTaskCounter.notifyAll();
    }
  }

  public CompletableFuture<Void> proveAllAsync(final Term goal) {
    this.assertNotDisposed();
    this.asyncTaskCounter.incrementAndGet();
    return CompletableFuture.runAsync(() -> {
      final JProlChoicePoint asyncGoal =
          new JProlChoicePoint(requireNonNull(goal), this.makeCopy());
      while (asyncGoal.prove() != null && !Thread.currentThread().isInterrupted()) {
        ;
      }
    }, this.executorService).handle((x, e) -> {
      onAsyncTaskCompleted(goal);
      if (e != null) {
        throw new ProlForkExecutionException("Error during async/1", goal, new Throwable[] {e});
      }
      return x;
    });
  }

  public CompletableFuture<Term> proveOnceAsync(final Term goal) {
    this.assertNotDisposed();
    this.asyncTaskCounter.incrementAndGet();
    return CompletableFuture.supplyAsync(() -> {
      final JProlChoicePoint asyncGoal =
          new JProlChoicePoint(requireNonNull(goal), this.makeCopy());
      final Term result = asyncGoal.prove();
      asyncGoal.cutVariants();
      return result;
    }, this.executorService).handle((x, e) -> {
      onAsyncTaskCompleted(goal);
      if (e != null) {
        throw new ProlForkExecutionException("Error during once async/1", goal,
            new Throwable[] {e});
      }
      return x;
    });
  }

  public ExecutorService getContextExecutorService() {
    return this.executorService;
  }

  private Optional<ReentrantLock> findLockerForId(final String lockerId,
                                                  final boolean createIfAbsent) {
    if (createIfAbsent) {
      return Optional.of(this.namedLockers.computeIfAbsent(lockerId, s -> new ReentrantLock()));
    } else {
      return Optional.ofNullable(this.namedLockers.get(lockerId));
    }
  }

  public void lockLockerForName(final String lockerId) {
    try {
      this.findLockerForId(lockerId, true)
          .orElseThrow(
              () -> new IllegalArgumentException("Named locker is not presented: " + lockerId))
          .lockInterruptibly();
    } catch (InterruptedException ex) {
      Thread.currentThread().interrupt();
      throw new RuntimeException("Locker wait has been interrupted: " + lockerId, ex);
    }
  }

  public boolean trylockLockerForName(final String lockerId) {
    return this.findLockerForId(lockerId, true).orElseThrow(
        () -> new IllegalArgumentException("Named locker is not presented: " + lockerId)).tryLock();
  }

  public void unlockLockerForName(final String lockerId) {
    this.findLockerForId(lockerId, false).ifPresent(ReentrantLock::unlock);
  }

  private void assertNotDisposed() {
    if (this.disposed.get()) {
      throw new ProlException("Context is disposed: " + this.contextId);
    }
  }

  public boolean addLibrary(final AbstractJProlLibrary library) {
    assertNotDisposed();
    if (library == null) {
      throw new IllegalArgumentException("Library must not be null");
    }
    if (this.libraries.contains(library)) {
      return false;
    }

    this.libraries.add(0, library);

    final JProlConsultText consultText = library.getClass().getAnnotation(JProlConsultText.class);
    if (consultText != null) {
      final String text = String.join("\n", consultText.value());
      if (text.length() > 0) {
        this.consult(new StringReader(text), null);
      }
    }

    final JProlConsultFile consultFile = library.getClass().getAnnotation(JProlConsultFile.class);
    if (consultFile != null) {
      final String resourceText = Arrays.stream(consultFile.value())
          .filter(x -> !(x == null || x.trim().isEmpty()))
          .map(File::new)
          .map(x -> {
            try {
              return Utils.readAsUtf8(x);
            } catch (IOException ex) {
              throw new Error("Can't read file: " + x);
            }
          })
          .collect(Collectors.joining("\n"));
      this.consult(new StringReader(resourceText), null);
    }

    final JProlConsultClasspath consultClasspath =
        library.getClass().getAnnotation(JProlConsultClasspath.class);
    if (consultClasspath != null) {
      final String resourceText = Arrays.stream(consultClasspath.value())
          .filter(x -> !(x == null || x.trim().isEmpty()))
          .map(x -> {
            final InputStream inStream = ClassLoader.getSystemClassLoader().getResourceAsStream(x);
            if (inStream == null) {
              throw new Error("Can't find resource: " + x);
            }
            return inStream;
          })
          .map(x -> {
            final StringBuilder buffer = new StringBuilder();
            try (final Reader reader = new InputStreamReader(new BufferedInputStream(x),
                StandardCharsets.UTF_8)) {
              while (!Thread.currentThread().isInterrupted()) {
                final int value = reader.read();
                if (value < 0) {
                  break;
                }
                buffer.append((char) value);
              }
            } catch (IOException ex) {
              throw new Error("Can't read resource", ex);
            }
            return buffer.toString();
          })
          .collect(Collectors.joining("\n"));
      this.consult(new StringReader(resourceText), null);
    }

    return true;
  }

  public KnowledgeBase getKnowledgeBase() {
    return this.knowledgeBase;
  }

  public boolean removeLibrary(final AbstractJProlLibrary library) {
    this.assertNotDisposed();
    if (library == null) {
      throw new IllegalArgumentException("Library must not be null");
    }
    return libraries.remove(library);
  }

  public PredicateInvoker findProcessor(final TermStruct predicate) {
    return this.libraries
        .stream()
        .map(lib -> lib.findProcessorForPredicate(predicate))
        .filter(Objects::nonNull)
        .findFirst()
        .orElse(NULL_PROCESSOR);
  }

  public boolean hasZeroArityPredicateForName(final String name) {
    return this.libraries.stream()
        .anyMatch(lib -> lib.hasZeroArityPredicate(name));
  }

  public List<TermStruct> findAllForPredicateIndicatorInLibs(final Term predicateIndicator) {
    return this.libraries.stream()
        .flatMap(lib -> lib.findAllForPredicateIndicator(predicateIndicator).stream())
        .collect(toList());
  }

  public boolean hasPredicateAtLibraryForSignature(final String signature) {
    return this.libraries.stream()
        .anyMatch(lib -> lib.hasPredicateForSignature(signature));
  }

  public boolean isSystemOperator(final String name) {
    return this.libraries.stream()
        .anyMatch(lib -> lib.isSystemOperator(name));
  }

  public TermOperatorContainer getSystemOperatorForName(final String name) {
    return this.libraries.stream()
        .map(lib -> lib.findSystemOperatorForName(name))
        .filter(Objects::nonNull)
        .findFirst().orElse(null);
  }

  public Iterator<AbstractJProlLibrary> makeLibraryIterator() {
    return this.libraries.iterator();
  }

  public boolean hasSystemOperatorStartsWith(final String str) {
    return this.libraries.stream()
        .anyMatch(lib -> lib.hasSyatemOperatorStartsWith(str));
  }

  public void dispose() {
    if (this.disposed.compareAndSet(false, true)) {
      this.executorService.shutdownNow();

      concat(this.triggersOnAssert.entrySet().stream(), this.triggersOnRetract.entrySet().stream())
          .flatMap(x -> x.getValue().stream())
          .distinct()
          .forEach(x -> x.onContextHalting(this));

      this.triggersOnAssert.clear();
      this.triggersOnRetract.clear();

      this.libraries.forEach((library) -> library.onContextDispose(this));
    }
  }

  public boolean isDisposed() {
    return disposed.get();
  }

  public void registerTrigger(final JProlTrigger trigger) {
    assertNotDisposed();
    final Map<String, JProlTriggerType> signatures = trigger.getSignatures();

    signatures.forEach((key, triggerType) -> {
      String signature = Utils.validateSignature(key);
      if (signature == null) {
        throw new IllegalArgumentException("Unsupported signature: " + key);
      }
      signature = Utils.normalizeSignature(signature);

      if (triggerType == JProlTriggerType.TRIGGER_ASSERT ||
          triggerType == JProlTriggerType.TRIGGER_ASSERT_RETRACT) {
        this.triggersOnAssert.computeIfAbsent(signature, k -> new CopyOnWriteArrayList<>())
            .add(trigger);
      }

      if (triggerType == JProlTriggerType.TRIGGER_RETRACT ||
          triggerType == JProlTriggerType.TRIGGER_ASSERT_RETRACT) {
        this.triggersOnRetract.computeIfAbsent(signature, k -> new CopyOnWriteArrayList<>())
            .add(trigger);
      }
    });
  }

  public void unregisterTrigger(final JProlTrigger trigger) {
    Stream.of(this.triggersOnAssert.entrySet().iterator(),
        this.triggersOnRetract.entrySet().iterator()).forEach(iterator -> {
      while (iterator.hasNext()) {
        final Entry<String, List<JProlTrigger>> entry = iterator.next();
        final List<JProlTrigger> lst = entry.getValue();
        if (lst.remove(trigger) && lst.isEmpty()) {
          iterator.remove();
        }
      }
    });
  }

  public boolean hasRegisteredTriggersForSignature(final String normalizedSignature,
                                                   final JProlTriggerType observedEvent) {
    boolean result;
    switch (observedEvent) {
      case TRIGGER_ASSERT: {
        result = this.triggersOnAssert.containsKey(normalizedSignature);
      }
      break;
      case TRIGGER_RETRACT: {
        result = this.triggersOnRetract.containsKey(normalizedSignature);
      }
      break;
      case TRIGGER_ASSERT_RETRACT: {
        result = this.triggersOnAssert.containsKey(normalizedSignature);
        if (!result) {
          result = this.triggersOnRetract.containsKey(normalizedSignature);
        }
      }
      break;
      default: {
        throw new IllegalArgumentException(
            "Unsupported observed event [" + observedEvent.name() + ']');
      }
    }

    return result;
  }

  public void notifyAboutUndefinedPredicate(final JProlChoicePoint choicePoint,
                                            final String signature) {
    switch (this.getUndefinedPredicateBehavior()) {
      case ERROR: {
        throw new ProlExistenceErrorException("predicate", "Undefined predicate: " + signature,
            choicePoint.getGoalTerm());
      }
      case WARNING: {
        this.contextListeners
            .forEach(x -> x.onUndefinedPredicateWarning(this, choicePoint, signature));
      }
      break;
      case FAIL: {
        //nothing
      }
      break;
      default:
        throw new Error("Unexpected behavior: " + this.getUndefinedPredicateBehavior());
    }
  }

  public void notifyTriggersForSignature(final String normalizedSignature,
                                         final JProlTriggerType observedEvent) {
    final List<JProlTrigger> listOfTriggers;

    switch (observedEvent) {
      case TRIGGER_ASSERT: {
        listOfTriggers = this.triggersOnAssert.getOrDefault(normalizedSignature, emptyList());
      }
      break;
      case TRIGGER_RETRACT: {
        listOfTriggers = this.triggersOnRetract.getOrDefault(normalizedSignature, emptyList());
      }
      break;
      case TRIGGER_ASSERT_RETRACT: {
        final List<JProlTrigger> triggersAssert =
            this.triggersOnAssert.getOrDefault(normalizedSignature, emptyList());
        final List<JProlTrigger> triggersRetract =
            this.triggersOnRetract.getOrDefault(normalizedSignature, emptyList());
        listOfTriggers = triggersRetract.isEmpty() && triggersAssert.isEmpty() ? emptyList() :
            concat(triggersAssert.stream(), triggersRetract.stream()).collect(toList());
      }
      break;
      default: {
        throw new IllegalArgumentException("Unsupported trigger event [" + observedEvent.name());
      }
    }

    if (!listOfTriggers.isEmpty()) {
      final TriggerEvent event = new TriggerEvent(this, normalizedSignature, observedEvent);
      listOfTriggers.forEach(x -> x.onTriggerEvent(event));
    }
  }

  public void consult(final Reader source) {
    this.consult(source, null);
  }

  public void consult(final Reader source, final ConsultInteractor interactor) {
    final JProlTreeBuilder treeBuilder = new JProlTreeBuilder(this);
    do {
      final JProlTreeBuilder.Result parseResult = treeBuilder.readPhraseAndMakeTree(source);
      if (parseResult == null) {
        break;
      }

      final int line = parseResult.line;
      final int strpos = parseResult.pos;

      final Term nextItem = parseResult.term;

      try {
        switch (nextItem.getTermType()) {
          case ATOM: {
            this.knowledgeBase.assertZ(this, newStruct(nextItem));
          }
          break;
          case STRUCT: {
            final TermStruct struct = (TermStruct) nextItem;
            final Term functor = struct.getFunctor();

            if (functor.getTermType() == TermType.OPERATOR) {
              final TermOperator op = (TermOperator) functor;
              final String text = op.getText();
              final OpAssoc type = op.getOperatorType();

              if (struct.isClause()) {
                switch (type) {
                  case XFX: {
                    // new rule
                    this.knowledgeBase.assertZ(this, struct);
                  }
                  break;
                  case FX: {
                    // directive
                    if (!processDirective(struct.getElement(0))) {
                      throw new ProlHaltExecutionException(2);
                    }
                  }
                  break;
                }

              } else if ("?-".equals(text)) {
                final Term termGoal = struct.getElement(0);

                if (interactor != null && interactor.onFoundInteractiveGoal(this, termGoal)) {

                  final Map<String, TermVar> varmap = new HashMap<>();
                  final AtomicInteger solutioncounter = new AtomicInteger();

                  final JProlChoicePoint thisGoal = new JProlChoicePoint(termGoal, this, null);

                  boolean doFindNextSolution;
                  do {
                    varmap.clear();
                    if (solveGoal(thisGoal, varmap)) {
                      doFindNextSolution = interactor
                          .onSolution(this, termGoal, varmap, solutioncounter.incrementAndGet());
                      if (!doFindNextSolution) {
                        throw new ProlHaltExecutionException(
                            String.format("Solution search halted: %s", termGoal), 1);
                      }
                    } else {
                      interactor.onFail(this, termGoal, solutioncounter.get());
                      doFindNextSolution = false;
                    }
                  } while (doFindNextSolution && !Thread.currentThread().isInterrupted());
                }
              } else {
                this.knowledgeBase.assertZ(this, struct);
              }
            } else {
              this.knowledgeBase.assertZ(this, struct);
            }
          }
          break;
          default: {
            throw new ProlKnowledgeBaseException(
                "Such element can't be saved at knowledge base [" + nextItem + ']');
          }
        }
      } catch (Exception ex) {
        throw new PrologParserException(
            ex.getCause() == null ? ex.getMessage() : ex.getCause().getMessage(), line, strpos, ex);
      }
    } while (!Thread.currentThread().isInterrupted());
  }

  private boolean solveGoal(final JProlChoicePoint goal, final Map<String, TermVar> varTable) {
    final Term result = goal.prove();

    if (result != null && varTable != null) {
      result.variables().forEach(e -> varTable.put(e.getText(), e));
    }

    return result != null;
  }

  private boolean processDirective(final Term directive) {
    final JProlChoicePoint goal = new JProlChoicePoint(directive, this, null);
    return goal.prove() != null;
  }


  @Override
  public String toString() {
    return "ProlContext(" + contextId + ')' + '[' + super.toString() + ']';
  }

  public JProlContext makeCopy() {
    return new JProlContext(
        this.contextId + "_copy",
        this.knowledgeBase.makeCopy(),
        this.executorService,
        this.systemFlags,
        this.contextListeners,
        this.ioProviders
    );
  }

  public ParserContext getParserContext() {
    return this.parserContext;
  }

} public class JProlTreeBuilder {

  private final JProlContext context;
  private final ThreadLocal<PrologParser> parsers = new ThreadLocal<>();

  public JProlTreeBuilder(final JProlContext context) {
    this.context = context;
  }

  public Result readPhraseAndMakeTree(final Reader reader) {
    PrologParser parser = parsers.get();
    if (parser == null) {
      parser = new GenericPrologParser(reader, this.context.getParserContext());
      parsers.set(parser);
    }

    try {
      final PrologTerm result = parser.next();
      return new Result(Terms.fromParsed(this.context, result), result.getLine(), result.getPos());
    } catch (NoSuchElementException ex) {
      parsers.remove();
      return null;
    }
  }

  public static final class Result {
    public final Term term;
    public final int line;
    public final int pos;

    Result(final Term term, final int line, final int pos) {
      this.term = term;
      this.line = line;
      this.pos = pos;
    }
  }
} public final class PredicateInvoker {

  public static final PredicateInvoker NULL_PROCESSOR =
      new PredicateInvoker(null, true, false, false, null, null);
  private static final MethodHandles.Lookup METHOD_LOOKUP = MethodHandles.lookup();
  private static final Class<?> CLASS_RESULT_VOID = void.class;
  private final String predicateSignature;
  private final MethodHandle methodHandle;
  private final AbstractJProlLibrary ownerLibrary;
  private final boolean voidResult;
  private final boolean determined;
  private final boolean evaluable;
  private final boolean changesGoalChain;

  public PredicateInvoker(
      final AbstractJProlLibrary owner,
      final boolean determined,
      final boolean evaluable,
      final boolean affectsChain,
      final String signature,
      final Method method
  ) {
    super();
    this.predicateSignature = signature;
    this.ownerLibrary = owner;

    if (method == null) {
      this.methodHandle = null;
      this.voidResult = true;
      this.determined = true;
      this.evaluable = false;
      this.changesGoalChain = false;
    } else {
      try {
        MethodHandle mhandle = METHOD_LOOKUP.unreflect(method);
        if (!Modifier.isStatic(method.getModifiers())) {
          mhandle = mhandle.bindTo(this.ownerLibrary);
        }
        this.methodHandle = mhandle;
      } catch (IllegalAccessException ex) {
        throw new Error(String
            .format("Can't process library '%s' method '%s'", owner.getLibraryUid(),
                method.getName()), ex);
      }

      this.voidResult = method.getReturnType() == CLASS_RESULT_VOID;
      this.determined = determined;
      this.evaluable = evaluable;
      this.changesGoalChain = affectsChain;
    }
  }

  public final boolean doesChangeGoalChain() {
    return this.changesGoalChain;
  }

  public final boolean isDetermined() {
    return this.determined;
  }

  public final boolean isEvaluable() {
    return this.evaluable;
  }

  public final AbstractJProlLibrary getLibrary() {
    return this.ownerLibrary;
  }

  public final String getSignature() {
    return this.predicateSignature;
  }

  public final MethodHandle getMethod() {
    return this.methodHandle;
  }

  public final Term executeEvaluable(final JProlChoicePoint goal, final TermStruct predicate) {
    try {
      final Object result = this.methodHandle.invoke(goal, predicate);
      return (Term) result;
    } catch (IllegalAccessException ex) {
      throw new ProlCriticalError("Illegal access exception at " + predicate, ex);
    } catch (Throwable thr) {
      final Throwable cause = thr.getCause();

      if (cause instanceof ArithmeticException) {
        throw new ProlEvaluationErrorException(cause.getMessage(), predicate);
      }

      if (cause instanceof ProlAbstractCatcheableException) {
        throw (ProlAbstractCatcheableException) cause;
      }
      if (cause instanceof ProlException) {
        throw (ProlException) cause;
      }
      throw new ProlCriticalError("Exception at [" + goal + ']', cause == null ? thr : cause);
    }
  }

  public final boolean execute(final JProlChoicePoint goal, final TermStruct predicate) {
    try {
      final Object result;
      result = this.methodHandle.invoke(goal, predicate);

      if (this.voidResult) {
        return true;
      } else if (result instanceof Boolean) {
        return (Boolean) result;
      } else {
        return result != null;
      }
    } catch (ProlException ex) {
      throw ex;
    } catch (IllegalAccessException ex) {
      throw new ProlException("Illegal access exception at " + predicate, ex);
    } catch (Throwable thr) {
      final Throwable cause = thr.getCause();

      if (cause instanceof ThreadDeath) {
        throw (ThreadDeath) cause;
      } else if (cause instanceof InterruptedException) {
        Thread.currentThread().interrupt();
      } else if (cause instanceof ProlAbstractCatcheableException) {
        throw (ProlAbstractCatcheableException) cause;
      } else if (cause instanceof ProlException) {
        throw (ProlException) cause;
      }
      throw new ProlCriticalError("Exception at [" + goal + ']', cause == null ? thr : cause);
    }
  }

  @Override
  public String toString() {
    return "Predicate processor " + predicateSignature + ' ' + methodHandle;
  }
} public enum JProlTriggerType {

  TRIGGER_ASSERT,
  TRIGGER_RETRACT,
  TRIGGER_ASSERT_RETRACT
} public class JProlTriggeringEventObserver extends AbstractJProlTrigger {

  private final PreparedGoal goal;

  public JProlTriggeringEventObserver(final Term goal) {
    super();
    this.goal = new PreparedGoal(goal);
  }

  @Override
  public void onTriggerEvent(final TriggerEvent event) {
    if (this.goal != null) {
      final JProlChoicePoint choicePoint = this.goal.makeChoicePoint(event.getContext());

      while (!Thread.currentThread().isInterrupted()) {
        final Term result = choicePoint.proveWithFailForUnknown();
        if (result == null) {
          break;
        }
      }
    }
  }

  @Override
  public void onContextHalting(final JProlContext context) {

  }
} public final class ProlAssertions {
  private ProlAssertions() {
  }

  public static void assertStruct(final Term term) {
    assertNonVar(term);
    if (term.getTermType() != STRUCT) {
      throw new ProlTypeErrorException("struct", "Expected struct: " + term, term);
    }
  }

  public static void assertArity(final Term term) {
    assertNonVar(term);
    if (term instanceof TermLong) {
      if (term.toNumber().longValue() < 0) {
        throw new ProlDomainErrorException("integer", "Expected zero or positive: " + term, term);
      }
    } else {
      throw new ProlTypeErrorException("integer", "Expected arity value: " + term, term);
    }
  }

  public static boolean isAtom(final Term t) {
    final boolean result;
    switch (t.getTermType()) {
      case LIST: {
        result = ((TermList) t).isNullList();
      }
      break;
      case ATOM: {
        result = !(t instanceof NumericTerm);
      }
      break;
      default:
        result = false;
        break;
    }
    return result;
  }

  public static void assertAtom(final Term t) {
    assertNonVar(t);
    if (!isAtom(t)) {
      throw new ProlTypeErrorException("atom", "Atom expected: " + t, t);
    }
  }

  public static void assertAtomOrAtomList(final Term t) {
    assertNonVar(t);
    boolean error = true;
    switch (t.getTermType()) {
      case ATOM: {
        if (!(t instanceof NumericTerm)) {
          error = false;
        }
      }
      break;
      case LIST: {
        TermList lst = (TermList) t;
        error = false;
        if (lst.isNullList()) {
          break;
        }

        while (!error) {
          Term head = lst.getHead();
          if (head.getTermType() == VAR) {
            head = ((TermVar) head).getValue();
            if (head == null) {
              error = true;
              break;
            }
          }
          if (head.getTermType() != TermType.ATOM) {
            error = true;
            break;
          }

          final Term tail = lst.getTail();
          if (tail == Terms.NULL_LIST) {
            break;
          }
          if (tail.getTermType() == TermType.LIST) {
            lst = (TermList) tail;
          } else {
            error = true;
            break;
          }
        }
      }
      break;
    }
    if (error) {
      throw new ProlInstantiationErrorException("Should be atom or atom list '" + t + '\'', t);
    }
  }

  public static void assertAtomic(final Term t) {
    assertNonVar(t);
    boolean errorType = false;
    switch (t.getTermType()) {
      case LIST: {
        errorType = !((TermList) t).isNullList();
      }
      break;
      case ATOM: {
      }
      break;
      default: {
        errorType = true;
      }
      break;
    }

    if (errorType) {
      throw new ProlTypeErrorException("atomic", "Atomic type expected: " + t, t);
    }
  }

  public static void assertByte(final Term t) {
    assertNonVar(t);
    boolean error = true;
    if (t instanceof TermLong) {
      final int value = t.toNumber().intValue();
      if ((value & 0xFF) == 0) {
        error = false;
      }
    }
    if (error) {
      throw new ProlInstantiationErrorException("Should be byte '" + t + '\'', t);
    }
  }

  public static void assertCallable(final Term t) {
    final boolean errorType;
    switch (t.getTermType()) {
      case STRUCT: {
        errorType = false;
      }
      break;
      case ATOM: {
        errorType = t instanceof NumericTerm;
      }
      break;
      case VAR: {
        throw new ProlInstantiationErrorException("Expected instantiated callable: " + t, t);
      }
      default:
        errorType = true;
        break;
    }
    if (errorType) {
      throw new ProlTypeErrorException("callable", "Callable term expected: " + t, t);
    }
  }

  public static void assertCharacter(final Term t) {
    assertNonVar(t);
    boolean error = true;
    if (t.getTermType() == TermType.ATOM && t.getText().length() == 1) {
      error = false;
    }
    if (error) {
      throw new ProlTypeErrorException("'character' expected, found an atom: " + t, t);
    }
  }

  public static void assertCharacterCode(final Term t) {
    assertNonVar(t);
    if (t instanceof TermLong) {
      final int value = t.toNumber().intValue();
      if ((value & 0xFFFF0000) != 0) {
        throw new ProlRepresentationErrorException("character_code",
            "Incompatible character code: " + t, t);
      }
    } else {
      throw new ProlTypeErrorException("character", "Char code expected: " + t, t);
    }
  }

  public static void assertCharacterCodeList(final Term t) {
    int errorCode = 0;
    if (t.getTermType() == TermType.LIST) {
      TermList lst = (TermList) t;
      while (!lst.isNullList() && errorCode == 0) {
        final Term value = lst.getHead().findNonVarOrSame();
        if (value.getTermType() == TermType.ATOM) {
          if (value instanceof TermLong) {
            if ((value.toNumber().intValue() & 0xFFFF0000) != 0) {
              errorCode = 2;
            }
          } else {
            errorCode = 2;
          }
        } else {
          errorCode = value.getTermType() == VAR ? 3 : 1;
        }
        final Term tail = lst.getTail().findNonVarOrSame();
        if (tail.getTermType() == TermType.LIST) {
          lst = (TermList) tail;
        } else {
          errorCode = tail.getTermType() == VAR ? 0 : 1;
          break;
        }
      }
    } else if (t.getTermType() == VAR) {
      errorCode = 3;
    } else {
      errorCode = 1;
    }
    switch (errorCode) {
      case 1:
        throw new ProlTypeErrorException("character_code_list",
            "Character code list expected: " + t, t);
      case 2:
        throw new ProlRepresentationErrorException("character_code",
            "Incompatible character code in list: " + t, t);
      case 3:
        throw new ProlInstantiationErrorException("Must be instantiated: " + t, t);
    }
  }

  public static void assertCharacterList(final Term t) {
    int errorCode = 0;
    if (t.getTermType() == VAR) {
      errorCode = 3;
    } else if (t.getTermType() == TermType.LIST) {
      TermList lst = (TermList) t;
      while (!lst.isNullList() && errorCode == 0) {
        final Term value = lst.getHead().findNonVarOrSame();
        if (value.getTermType() == TermType.ATOM) {
          if (value instanceof NumericTerm) {
            errorCode = 1;
          } else {
            if (value.getText().length() != 1) {
              errorCode = 2;
            }
          }
        } else {
          errorCode = value.getTermType() == VAR ? 3 : 1;
        }
        final Term tail = lst.getTail().findNonVarOrSame();
        if (tail.getTermType() == TermType.LIST) {
          lst = (TermList) tail;
        } else {
          errorCode = tail.getTermType() == VAR ? 0 : 1;
          break;
        }
      }
    } else {
      errorCode = 1;
    }
    switch (errorCode) {
      case 1:
        throw new ProlTypeErrorException("character_list", "Character list expected: " + t, t);
      case 2:
        throw new ProlRepresentationErrorException("character", "Character expected: " + t, t);
      case 3:
        throw new ProlInstantiationErrorException("Must be instantiated: " + t, t);
    }
  }

  public static void assertCompoundTerm(final Term t) {
    switch (t.getTermType()) {
      case LIST:
      case STRUCT: {
      }
      break;
      case VAR: {
        throw new ProlInstantiationErrorException("Expected instantiated compound term: " + t, t);
      }
      default:
        throw new ProlTypeErrorException("compound", "Expected compound: " + t, t);
    }
  }

  public static void assertEvaluable(final Term t) {
    assertNonVar(t);
    boolean error = true;
    if (t instanceof NumericTerm) {
      error = false;
    } else {
      if (t.getTermType() == STRUCT) {
        final TermStruct struct = (TermStruct) t;
        final PredicateInvoker processor = struct.getPredicateProcessor();
        if (processor != null && processor.isEvaluable()) {
          error = false;
        }
      }
    }
    if (error) {
      throw new ProlTypeErrorException("evaluable", "Expected evaluable: " + t, t);
    }
  }

  public static void assertHead(final Term t) {
    assertNonVar(t);
    boolean error = true;
    switch (t.getTermType()) {
      case ATOM: {
        if (!(t instanceof NumericTerm)) {
          error = false;
        }
      }
      break;
      case STRUCT: {
        final Term functor = ((TermStruct) t).getFunctor();
        if (functor.getTermType() == TermType.ATOM) {
          error = false;
        }
      }
      break;
    }
    if (error) {
      throw new ProlTypeErrorException("Unexpected head: " + t, t);
    }
  }

  public static void assertInByte(final Term t) {
    assertNonVar(t);
    boolean error = false;
    if (t instanceof TermLong) {
      final int val = t.toNumber().intValue();
      if (val == -1) {
        error = true;
      }
    } else {
      error = true;
    }
    if (error) {
      throw new ProlInstantiationErrorException("Should be byte or -1 '" + t + '\'', t);
    }
  }

  public static void assertInCharacter(final Term t) {
    assertNonVar(t);
    boolean error = false;
    if (t.getTermType() == TermType.ATOM) {
      final String text = t.getText();
      if (text.length() != 1 && !"end_of_file".equals(text)) {
        error = true;
      }
    } else {
      error = true;
    }
    if (error) {
      throw new ProlInstantiationErrorException("Should be character code or -1 '" + t + '\'', t);
    }
  }

  public static void assertInCharacterCode(final Term t) {
    assertNonVar(t);
    boolean error = false;
    if (t instanceof TermLong) {
      final int val = t.toNumber().intValue();
      if ((val & 0xFFFF0000) != 0 && val != -1) {
        error = true;
      }
    } else {
      error = true;
    }
    if (error) {
      throw new ProlInstantiationErrorException("Should be character code or -1 '" + t + '\'', t);
    }
  }

  public static void assertInteger(final Term t) {
    assertNonVar(t);
    if (!(t instanceof TermLong)) {
      throw new ProlTypeErrorException("integer", "Integer expected: " + t, t);
    }
  }

  public static void assertIoMode(final Term t) {
    assertNonVar(t);
    boolean error = true;
    if (t.getTermType() == TermType.ATOM) {
      final String text = t.getText();
      if (text.equals("read") || text.equals("write") || text.equals("append")) {
        error = false;
      }
    }
    if (error) {
      throw new ProlDomainErrorException("[read,write,append]", "Wrong io mode: " + t, t);
    }
  }

  public static void assertList(final Term t) {
    assertNonVar(t);
    if (t.getTermType() != TermType.LIST) {
      throw new ProlTypeErrorException("list", "List expected: " + t, t);
    }
  }

  public static void assertVar(final Term t) {
    if (t.getTermType() != VAR) {
      throw new ProlTypeErrorException("var", "Unbound variable expected: " + t, t);
    }
  }

  public static void assertNonVar(final Term t) {
    if (t.getTermType() == VAR) {
      throw new ProlInstantiationErrorException("Grounded value expected: " + t, t);
    }
  }

  public static void assertNumber(final Term t) {
    assertNonVar(t);
    if (!(t instanceof NumericTerm)) {
      throw new ProlTypeErrorException("number", "Number expected: " + t, t);
    }
  }

  public static void assertOperatorSpecifier(final Term t) {
    assertNonVar(t);
    boolean error;
    if (t.getTermType() == TermType.ATOM && !(t instanceof NumericTerm)) {
      final String text = t.getText();
      error = !OpAssoc.findForName(text).isPresent();
    } else {
      error = true;
    }
    if (error) {
      throw new ProlDomainErrorException(
          "Should be only [xfx,yfx,xfy,xf,fx,yf,fy] but '" + t + '\'', t);
    }
  }

  public static void assertIndicator(final Term t) {
    assertNonVar(t);
    int errorCode = 1;
    if (t.getTermType() == STRUCT) {
      final TermStruct struct = (TermStruct) t;
      if (struct.getArity() == 2 && "/".equals(struct.getFunctor().getText())) {
        final Term left = struct.getElement(0).findNonVarOrSame();
        final Term right = struct.getElement(1).findNonVarOrSame();

        final boolean leftOk =
            (left.getTermType() == TermType.ATOM && left.getClass() == Term.class)
                || (left.getTermType() == STRUCT && ((TermStruct) left).getArity() == 0)
                || (left.getTermType() == VAR && !left.isGround());

        final boolean rightOk =
            (right.getTermType() == TermType.ATOM && right.getClass() == TermLong.class)
                || (right.getTermType() == VAR && !right.isGround());

        if (leftOk && rightOk) {
          if (right instanceof NumericTerm) {
            if (right instanceof TermDouble || right.toNumber().longValue() < 0L) {
              errorCode = 2;
            } else if (right.toNumber().longValue() > Integer.MAX_VALUE) {
              errorCode = 3;
            } else {
              errorCode = 0;
            }
          } else {
            errorCode = 0;
          }
        } else {
          errorCode = 1;
        }
      }
    }

    switch (errorCode) {
      case 1:
        throw new ProlTypeErrorException("predicate_indicator",
            "Predicate indicator expected: " + t, t);
      case 2:
        throw new ProlDomainErrorException("integer", "Predicate indicator expected: " + t, t);
      case 3:
        throw new ProlRepresentationErrorException("max_arity", "Wrong arity: " + t, t);
    }
  }

  public static void assertNonEmptyList(final Term t) {
    assertNonVar(t);
    if (t.getTermType() != TermType.LIST) {
      throw new ProlTypeErrorException("list", "Expected list: " + t, t);
    } else {
      if (t == Terms.NULL_LIST) {
        throw new ProlDomainErrorException("[]", "Expected non-empty list: " + t, t);
      }
    }
  }

  public void assertTriggerEvent(final Term t) {
    assertNonVar(t);
    if (t.getTermType() != TermType.ATOM) {
      throw new ProlInstantiationErrorException("Should be an atom '" + t + '\'', t);
    } else {
      final String value = t.getText();
      if (!"onassert".equals(value) && !"onretract".equals(value) &&
          !"onassertretract".equals(value)) {
        throw new ProlDomainErrorException(
            "Should be a value from the list [onassert, onretract, onassertretract] '" + t + '\'',
            t);
      }
    }
  }
} public final class Utils {

  public static final TermOperator SIGNATURE_OPERATOR = new TermOperator(400, OpAssoc.YFX, "/");

  private Utils() {
  }

  public static TermList toCharCodeList(final Term term) {
    final String text = term.getText();

    if (text == null || text.isEmpty()) {
      return Terms.NULL_LIST;
    } else {
      final TermList result = createOrAppendToList(null, newLong(text.charAt(0)));
      TermList current = result;
      for (int i = 1; i < text.length(); i++) {
        current = createOrAppendToList(current, newLong(text.charAt(i)));
      }
      return result;
    }
  }

  public static int getJvmBitness() {
    String data = System.getProperty("sun.arch.data.model","");
    if (data.contains("32")) return 32;
    if (data.contains("64")) return 64;
    return 64;
  }
  
  public static TermList toCharList(final Term term) {
    final String text = term.getText();
    final int len = text.length();
    if (len == 0) {
      return Terms.NULL_LIST;
    }

    final StringBuilder buff = new StringBuilder(1);

    TermList resultList = null;
    TermList curList = null;

    for (int li = 0; li < len; li++) {
      buff.append(text.charAt(li));
      final Term newAtom = Terms.newAtom(buff.toString());
      buff.setLength(0);
      if (li == 0) {
        resultList = newList(newAtom);
        curList = resultList;
      } else {
        curList = createOrAppendToList(curList, newAtom);
      }
    }

    return resultList;
  }


  public static Throwable[] extractErrors(final List<CompletableFuture<Term>> futures) {
    return futures.stream().filter(CompletableFuture::isCompletedExceptionally).map(x -> {
      try {
        x.join();
      } catch (Throwable e) {
        if (!(e instanceof CancellationException)) {
          return e;
        }
      }
      return null;
    }).filter(Objects::nonNull).toArray(Throwable[]::new);
  }

  public static <T> CloseableIterator<T> makeCloseableIterator(final Iterator<T> iterator,
                                                               final Runnable onClose) {
    return new CloseableIterator<T>() {
      private final Iterator<T> wrapped = iterator;

      @Override
      public void close() {
        if (onClose != null) {
          onClose.run();
        }
      }

      @Override
      public boolean hasNext() {
        return this.wrapped.hasNext();
      }

      @Override
      public T next() {
        return this.wrapped.next();
      }
    };
  }

  public static void writeAsUtf8(final File file, final CharSequence seq) throws IOException {
    try (Writer writer = new BufferedWriter(
        new OutputStreamWriter(new FileOutputStream(file, false), StandardCharsets.UTF_8))) {
      writer.write(seq.toString());
      writer.flush();
    }
  }

  public static String readAsUtf8(final File file) throws IOException {
    try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
      final StringBuilder buffer =
          new StringBuilder((int) file.length() < 0 ? 16384 : (int) file.length());
      while (!Thread.currentThread().isInterrupted()) {
        final int chr = reader.read();
        if (chr < 0) {
          break;
        }
        buffer.append((char) chr);
      }
      return buffer.toString();
    }
  }

  public static TermList createOrAppendToList(final TermList nullableList, final Term term) {
    final TermList newList = Terms.newList(term);

    if (nullableList != null && !nullableList.isNullList()) {
      newList.setTail(nullableList.getTail());
      nullableList.setTail(newList);
    }

    return newList;
  }

  public static String escapeSrc(final String string) {

    if (string.length() == 0) {
      return string;
    }

    final StringBuilder builder = new StringBuilder(string.length());

    for (int li = 0; li < string.length(); li++) {
      final char curChar = string.charAt(li);

      switch (curChar) {
        case '\\':
          builder.append("\\\\");
          break;
        case '\'':
          builder.append("\\'");
          break;
        case '\"':
          builder.append("\\\"");
          break;
        case '\n':
          builder.append("\\n");
          break;
        case '\f':
          builder.append("\\f");
          break;
        case '\r':
          builder.append("\\r");
          break;
        case '\t':
          builder.append("\\t");
          break;
        case '_': {
          builder.append('_');
        }
        break;
        case '%':
        case '.':
        default: {
          builder.append(curChar);
        }
        break;
      }
    }

    return builder.toString();
  }

  public static String extractPredicateSignatureFromStructure(final Term term) {
    final TermStruct struct = term.findNonVarOrSame();

    if (struct.getArity() != 2) {
      return null;
    }
    final Term left = struct.getElement(0).findNonVarOrDefault(null);
    final Term right = struct.getElement(1).findNonVarOrDefault(null);

    if (right instanceof TermLong && left.getTermType() == ATOM) {
      return left.getText() + '/' + right.getText();
    }
    return null;
  }

  public static String normalizeSignature(final String signature) {
    if (signature == null) {
      return null;
    }
    String sig = signature.trim();

    if (sig.length() > 0 && sig.charAt(0) == '\'') {
      sig = sig.substring(1);
      final int lastIndex = sig.lastIndexOf('/');
      if (lastIndex < 0) {
        sig = null;
      } else {
        final String arity = sig.substring(lastIndex + 1).trim();
        String name = sig.substring(0, lastIndex - 1).trim();
        if (name.length() > 0 && name.charAt(name.length() - 1) == '\'') {
          name = name.substring(0, name.length() - 1);
          sig = name + '/' + arity;
        } else {
          sig = null;
        }
      }
    }
    return sig;
  }

  public static String validateSignature(final String signature) {
    final String[] parsed =
        Objects.requireNonNull(signature, "Null signature not allowed").split("/");
    if (parsed.length == 2) {
      String str = parsed[0].trim();
      boolean quoted = false;
      if (str.length() != 0) {
        if (str.charAt(0) == '\'') {
          if (str.length() > 1 && str.charAt(str.length() - 1) == '\'') {
            str = str.substring(1, str.length() - 1);
            if (str.length() == 0) {
              return null;
            }
            quoted = true;
          } else {
            // wrong name, it must not contain '\'' as the only symbol
            return null;
          }
        }

        final char firstChar = str.charAt(0);
        if (!quoted && (Character.isDigit(firstChar) || Character.isUpperCase(firstChar) ||
            Character.isWhitespace(firstChar) || firstChar == '.')) {
          return null;
        }

        // ok. the first part is ok, check the second part
        final int arity;
        try {
          arity = Integer.parseInt(parsed[1].trim());
          if (arity < 0) {
            throw new NumberFormatException("Negate number is not supported as arity");
          }
        } catch (NumberFormatException ex) {
          return null;
        }

        final StringBuilder builder = new StringBuilder(signature.length());

        if (quoted) {
          builder.append('\'').append(str).append('\'').append('/').append(arity);
        } else {
          builder.append(str).append('/').append(arity);
        }

        return builder.toString();
      }
    }
    return null;
  }

}
import static com.igormaznitsa.jprol.data.TermType.ATOM;
import static com.igormaznitsa.jprol.data.TermType.LIST;
import static com.igormaznitsa.jprol.data.TermType.STRUCT;
import static com.igormaznitsa.jprol.data.TermType.VAR;
import static com.igormaznitsa.jprol.data.Terms.LIST_FUNCTOR;
import static com.igormaznitsa.jprol.data.Terms.NULL_LIST;
import static com.igormaznitsa.jprol.data.Terms.newAtom;
import static com.igormaznitsa.jprol.data.Terms.newDouble;
import static com.igormaznitsa.jprol.data.Terms.newList;
import static com.igormaznitsa.jprol.data.Terms.newLong;
import static com.igormaznitsa.jprol.data.Terms.newStruct;
import static com.igormaznitsa.jprol.data.Terms.newVar;
import static com.igormaznitsa.jprol.utils.Utils.createOrAppendToList;
import static com.igormaznitsa.prologparser.tokenizer.OpAssoc.FX;
import static com.igormaznitsa.prologparser.tokenizer.OpAssoc.FY;
import static com.igormaznitsa.prologparser.tokenizer.OpAssoc.XFX;
import static com.igormaznitsa.prologparser.tokenizer.OpAssoc.XFY;
import static com.igormaznitsa.prologparser.tokenizer.OpAssoc.YFX;
import com.igormaznitsa.jprol.annotations.JProlOperator;
import com.igormaznitsa.jprol.annotations.JProlOperators;
import com.igormaznitsa.jprol.annotations.JProlPredicate;
import com.igormaznitsa.jprol.data.NumericTerm;
import com.igormaznitsa.jprol.data.Term;
import com.igormaznitsa.jprol.data.TermDouble;
import com.igormaznitsa.jprol.data.TermList;
import com.igormaznitsa.jprol.data.TermLong;
import com.igormaznitsa.jprol.data.TermOperator;
import com.igormaznitsa.jprol.data.TermStruct;
import com.igormaznitsa.jprol.data.TermVar;
import com.igormaznitsa.jprol.data.Terms;
import com.igormaznitsa.jprol.exceptions.ProlAbstractCatcheableException;
import com.igormaznitsa.jprol.exceptions.ProlCriticalError;
import com.igormaznitsa.jprol.exceptions.ProlCustomErrorException;
import com.igormaznitsa.jprol.exceptions.ProlDomainErrorException;
import com.igormaznitsa.jprol.exceptions.ProlEvaluationErrorException;
import com.igormaznitsa.jprol.exceptions.ProlHaltExecutionException;
import com.igormaznitsa.jprol.exceptions.ProlInstantiationErrorException;
import com.igormaznitsa.jprol.exceptions.ProlPermissionErrorException;
import com.igormaznitsa.jprol.exceptions.ProlRepresentationErrorException;
import com.igormaznitsa.jprol.exceptions.ProlTypeErrorException;
import com.igormaznitsa.jprol.kbase.IteratorType;
import com.igormaznitsa.jprol.kbase.KnowledgeBase;
import com.igormaznitsa.jprol.logic.JProlChoicePoint;
import com.igormaznitsa.jprol.logic.JProlContext;
import com.igormaznitsa.jprol.logic.JProlTreeBuilder;
import com.igormaznitsa.jprol.logic.PredicateInvoker;
import com.igormaznitsa.jprol.logic.triggers.JProlTriggerType;
import com.igormaznitsa.jprol.logic.triggers.JProlTriggeringEventObserver;
import com.igormaznitsa.jprol.utils.ProlAssertions;
import com.igormaznitsa.jprol.utils.Utils;
import com.igormaznitsa.prologparser.exceptions.PrologParserException;
import com.igormaznitsa.prologparser.tokenizer.OpAssoc;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
        }
        return true;
      }
    } else {

      try {
        final Term result = catchGoal.prove();
        if (result == null) {
          goal.cutVariants();
          return false;
        } else {
          if (catchGoal.isCompleted()) {
            goal.cutVariants();
          }
          return true;
        }
      } catch (final ProlAbstractCatcheableException ex) {
        if (catcher.unifyTo(ex.getAsStruct())) {
          catchGoal = new JProlChoicePoint(solver, goal.getContext());
          goal.setPayload(catchGoal);
          final Term result = catchGoal.prove();
          if (result == null) {
            goal.cutVariants();
            return false;
          } else {
            if (catchGoal.isCompleted()) {
              goal.cutVariants();
            }
            goal.setPayload(catchGoal);
            return true;
          }
        } else {
          return false;
        }
      }
    }
  }

  @JProlPredicate(determined = true, signature = "throw/1", args = "+callable", reference = "Throw an exception which can be catched by catch/3")
  public static void predicateTHROW(final JProlChoicePoint goal, final TermStruct predicate) {
    final Term term = predicate.getElement(0).findNonVarOrSame();
    if (goal.isArgsValidate()) {
      ProlAssertions.assertCallable(term);
    }

    final String exceptionSignature = term.getSignature();

    if ("instantiation_error/0".equals(exceptionSignature)) {
      throw new ProlInstantiationErrorException(predicate);
    }

    if ("type_error/2".equals(exceptionSignature)) {
      throw new ProlTypeErrorException(predicate.getElement(0).forWrite(), predicate.getElement(1));
    }

    if ("domain_error/2".equals(exceptionSignature)) {
      throw new ProlDomainErrorException(predicate.getElement(0).forWrite(),
          predicate.getElement(1));
    }

    if ("permission_error/3".equals(exceptionSignature)) {
      throw new ProlPermissionErrorException(predicate.getElement(0).forWrite(),
          predicate.getElement(1).forWrite(), predicate.getElement(2));
    }

    if ("representation_error/1".equals(exceptionSignature)) {
      throw new ProlRepresentationErrorException(predicate.getElement(0).forWrite(), predicate);
    }

    if ("evaluation_error/1".equals(exceptionSignature)) {
      throw new ProlEvaluationErrorException(predicate.getElement(0).forWrite(), predicate);
    }

    // all other errors make as custom
    //-------------------------------------
    Term arg = predicate.getElement(0);
    if (arg.getTermType() != STRUCT) {
      arg = newStruct(arg);
    }
    throw new ProlCustomErrorException(arg, predicate);
  }

  @JProlPredicate(determined = true, signature = "pause/1", args = {
      "+number"}, reference = "Make pause for defined milliseconds.")
  public static void predicatePAUSE(final JProlChoicePoint goal, final TermStruct predicate)
      throws InterruptedException {
    final Term term = predicate.getElement(0).findNonVarOrSame();
    if (goal.isArgsValidate()) {
      ProlAssertions.assertNumber(term);
    }
    final long milliseconds = term.toNumber().longValue();
    if (milliseconds > 0) {
      Thread.sleep(milliseconds);
    }
  }

  @JProlPredicate(determined = true, signature = "regtrigger/3", args = {
      "+predicate_indicator,+atom,+callable"}, reference = "regtrigger(somepredicate/3,onassert,triggerhandler) is always true. The predicate allows to register a trigger handler for distinguished predicate signature. The handled trigger event can be selected from the list [onassert, onretract, onassertretract].")
  public static boolean predicateREGTRIGGER3(final JProlChoicePoint goal,
                                             final TermStruct predicate) {
    final Term arg1 = predicate.getElement(0).findNonVarOrSame();
    final Term arg2 = predicate.getElement(1).findNonVarOrSame();
    final Term callableTerm = predicate.getElement(2).findNonVarOrSame();

    if (goal.isArgsValidate()) {
      ProlAssertions.assertIndicator(arg1);
      ProlAssertions.assertAtom(arg2);
      ProlAssertions.assertCallable(callableTerm);
    }

    final String signature = Utils.extractPredicateSignatureFromStructure(arg1);
    final String triggeringEvent = arg2.getText();
    final JProlContext context = goal.getContext();

    final JProlTriggeringEventObserver deferredTriggeringGoal =
        new JProlTriggeringEventObserver(callableTerm);

    if (triggeringEvent != null) {
      switch (triggeringEvent) {
        case "onassert":
          deferredTriggeringGoal.addSignature(signature, JProlTriggerType.TRIGGER_ASSERT);