public static TermStruct newStruct(final Term functor) {
  return new TermStruct(functor);
} 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 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 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 final class TermOperatorContainer extends SpecialTerm {

  private final OpContainer opContainer;
  private volatile TermOperator opFZ;
  private volatile TermOperator opZF;
  private volatile TermOperator opZFZ;

  private TermOperatorContainer(final TermOperatorContainer etalon) {
    super(etalon.getText());
    this.opContainer = OpContainer.make(etalon.getText(),
        etalon.opFZ == null ? null : etalon.opFZ.asOp(),
        etalon.opZF == null ? null : etalon.opZF.asOp(),
        etalon.opZFZ == null ? null : etalon.opZFZ.asOp());
    opFZ = etalon.opFZ;
    opZF = etalon.opZF;
    opZFZ = etalon.opZFZ;
  }

  public TermOperatorContainer(final TermOperator operator) {
    super(operator.getText());
    this.opContainer = OpContainer.make(operator.asOp());
    setOperator(operator);
  }

  public OpContainer asOpContainer() {
    return this.opContainer;
  }

  public boolean setOperator(final TermOperator operator) {
    switch (operator.getOperatorType()) {
      case FX:
      case FY: {
        if (opFZ != null) {
          return false;
        }
        opFZ = operator;
        this.opContainer.add(operator.asOp());
      }
      break;
      case XF:
      case YF: {
        if (opZF != null) {
          return false;
        }
        opZF = operator;
        this.opContainer.add(operator.asOp());
      }
      break;
      case XFX:
      case XFY:
      case YFX: {
        if (opZFZ != null) {
          return false;
        }
        opZFZ = operator;
        this.opContainer.add(operator.asOp());
      }
      break;
      default: {
        throw new Error("Unsupported operator type");
      }
    }
    return true;
  }

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

  public void write(final PrintWriter writer) {
    if (opFZ != null) {
      opFZ.write(writer);
    }
    if (opZF != null) {
      opZF.write(writer);
    }
    if (opZFZ != null) {
      opZFZ.write(writer);
    }
  }

  public TermOperator getForTypePrecisely(final OpAssoc type) {
    TermOperator result = null;
    switch (type) {
      case FY:
      case FX: {
        if (opFZ != null) {
          result = opFZ;
        }
      }
      break;
      case XF:
      case YF: {
        if (opZF != null) {
          result = opZF;
        }
      }
      break;
      case XFX:
      case YFX:
      case XFY: {
        if (opZFZ != null) {
          result = opZFZ;
        }
      }
      break;
      default: {
        throw new Error("Unsupported operator type");
      }
    }

    if (result != null && result.getOperatorType() == type) {
      return result;
    }
    return null;
  }

  public boolean removeOperatorForType(final OpAssoc type) {
    boolean result = false;
    switch (type) {
      case FX:
      case FY: {
        if (opFZ != null && opFZ.getOperatorType() == type) {
          opFZ = null;
          this.opContainer.removeForType(type);
          result = true;
        }
      }
      break;
      case XF:
      case YF: {
        if (opZF != null && opZF.getOperatorType() == type) {
          opZF = null;
          this.opContainer.removeForType(type);
          result = true;
        }
      }
      break;
      case XFX:
      case YFX:
      case XFY: {
        if (opZFZ != null && opZFZ.getOperatorType() == type) {
          opZFZ = null;
          this.opContainer.removeForType(type);
          result = true;
        }
      }
      break;
      default:
        return false;
    }
    return result;
  }

  public List<TermOperator> toList() {
    final List<TermOperator> result = new ArrayList<>(3);
    final TermOperator fz = this.opFZ;
    final TermOperator zfz = this.opZFZ;
    final TermOperator zf = this.opZF;
    if (fz != null) {
      result.add(fz);
    }
    if (zfz != null) {
      result.add(zfz);
    }
    if (zf != null) {
      result.add(zf);
    }
    return result;
  }

  public TermOperatorContainer makeCopy() {
    return new TermOperatorContainer(this);
  }
} 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 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 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 ProlKnowledgeBaseException extends ProlException {

  private static final long serialVersionUID = 7055306365499059734L;

  public ProlKnowledgeBaseException() {
    super();
  }

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

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

  public ProlKnowledgeBaseException(Throwable cause) {
    super(cause);
  }

} 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();
} @Data
public abstract class InMemoryItem {
  protected final TermStruct clause;
  protected final Term rightHandSide;
  protected final boolean leftHandSidePresented;

  InMemoryItem(final TermStruct clause) {
    this.clause = clause;

    if (clause.isClause()) {
      this.leftHandSidePresented = true;
      this.rightHandSide = clause.getElement(0);
    } else {
      this.leftHandSidePresented = false;
      this.rightHandSide = clause;
    }
  }

  public static InMemoryItem fromClause(final TermStruct clause) {
    final Term rhs = clause.isClause() ? clause.getElement(0) : clause;

    final List<String> foundKeyVars = rhs.stream()
        .filter(x -> x.getTermType() == TermType.VAR && !((TermVar) x).isAnonymous())
        .map(Term::getText)
        .collect(Collectors.toList());

    boolean complex = false;
    if (foundKeyVars.size() > 1) {
      complex = foundKeyVars.size() != foundKeyVars.stream().distinct().count();
    }

    return complex ? new RhsItemComplex(clause) : new RhsItemSimple(clause);
  }

  public abstract boolean matches(final Term rightHandSide);

  public void write(final PrintWriter writer) {
    requireNonNull(writer, "Writer must not be null")
        .write(String.format("%s.%n", this.clause.toSrcString()));
  }

  @Override
  public String toString() {
    return this.rightHandSide.toString();
  }

  public boolean isFact() {
    return !this.leftHandSidePresented && this.rightHandSide.isGround();
  }

  public boolean isRule() {
    return this.leftHandSidePresented || !this.rightHandSide.isGround();
  }

} 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 enum JProlTriggerType {

  TRIGGER_ASSERT,
  TRIGGER_RETRACT,
  TRIGGER_ASSERT_RETRACT
} public interface CloseableIterator<T> extends Iterator<T>, Closeable {
} public class OperatorIterator implements CloseableIterator<TermOperator> {
  private final Iterator<TermOperatorContainer> iterator;
  private Iterator<TermOperator> operatorIterator;

  public OperatorIterator(final Iterator<TermOperatorContainer> iterator) {
    this.iterator = iterator;
    if (this.iterator.hasNext()) {
      this.operatorIterator = this.iterator.next().toList().iterator();
    }
  }

  @Override
  public boolean hasNext() {
    return this.operatorIterator != null && this.operatorIterator.hasNext();
  }

  @Override
  public void close() throws IOException {

  }

  @Override
  public TermOperator next() {
    if (this.operatorIterator == null || !this.operatorIterator.hasNext()) {
      throw new NoSuchElementException();
    }
    final TermOperator result = this.operatorIterator.next();
    if (!this.operatorIterator.hasNext()) {
      if (this.iterator.hasNext()) {
        this.operatorIterator = this.iterator.next().toList().iterator();
      }
    }
    return result;
  }
} 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.VAR;
import static com.igormaznitsa.jprol.data.Terms.newStruct;
import static com.igormaznitsa.jprol.utils.Utils.makeCloseableIterator;
import static java.lang.Integer.parseInt;
import static java.util.Objects.requireNonNull;
import com.igormaznitsa.jprol.data.Term;
import com.igormaznitsa.jprol.data.TermOperator;
import com.igormaznitsa.jprol.data.TermOperatorContainer;
import com.igormaznitsa.jprol.data.TermStruct;
import com.igormaznitsa.jprol.data.Terms;
import com.igormaznitsa.jprol.exceptions.ProlInstantiationErrorException;
import com.igormaznitsa.jprol.exceptions.ProlKnowledgeBaseException;
import com.igormaznitsa.jprol.kbase.IteratorType;
import com.igormaznitsa.jprol.kbase.KnowledgeBase;
import com.igormaznitsa.jprol.kbase.inmemory.items.InMemoryItem;
import com.igormaznitsa.jprol.logic.JProlContext;
import com.igormaznitsa.jprol.logic.triggers.JProlTriggerType;
import com.igormaznitsa.jprol.utils.CloseableIterator;
import com.igormaznitsa.jprol.utils.OperatorIterator;
import com.igormaznitsa.jprol.utils.ProlAssertions;
import com.igormaznitsa.jprol.utils.Utils;
import com.igormaznitsa.prologparser.tokenizer.OpAssoc;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/*
 * Copyright 2014 Igor Maznitsa (http://www.igormaznitsa.com).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.igormaznitsa.jprol.kbase.inmemory;




public final class InMemoryKnowledgeBase implements KnowledgeBase {

  private final String knowledgeBaseId;
  private final Map<String, TermOperatorContainer> operatorTable = new ConcurrentHashMap<>();
  private final Map<String, List<InMemoryItem>> predicateTable = new ConcurrentHashMap<>();

  public InMemoryKnowledgeBase(final String id) {
    this.knowledgeBaseId = requireNonNull(id, "Id must not be null");
  }

  private InMemoryKnowledgeBase(final String baseId, final InMemoryKnowledgeBase etalon) {
    this.knowledgeBaseId = baseId;
    for (final Entry<String, TermOperatorContainer> item : etalon.operatorTable.entrySet()) {
      operatorTable.put(item.getKey(), item.getValue().makeCopy());
    }
    etalon.predicateTable.forEach((key, value) -> this.predicateTable.put(key, makeClone(value)));
  }

  private static List<InMemoryItem> makeClone(final List<InMemoryItem> src) {
    return new CopyOnWriteArrayList<>(src);
  }

  public long printStateAsSrc(final PrintWriter writer) {
    return this.predicateTable.entrySet().stream()
        .peek(e -> writer.println(String.format("%n%% signature '%s'", e.getKey())))
        .flatMap(e -> e.getValue().stream())
        .peek(i -> writer.println(String.format("%s.", i.getClause().toSrcString())))
        .count();
  }

  public Map<String, List<InMemoryItem>> getStorage() {
    return Collections.unmodifiableMap(this.predicateTable);
  }

  @Override
  public String getId() {
    return this.knowledgeBaseId;
  }

  @Override
  public boolean removeOperator(final String name, final OpAssoc type) {
    TermOperatorContainer opContainer;
    opContainer = this.operatorTable.get(name);

    boolean result = false;

    if (opContainer != null) {
      result = opContainer.removeOperatorForType(type);
    }
    return result;
  }

  @Override
  public void addOperator(final JProlContext context, final TermOperator operator) {
    final String operatorName = operator.getText();
    if (context.isSystemOperator(operator.getText())) {
      throw new SecurityException(
          "Attemption to override a system operator [" + operator.getText() + ']');
    }

    TermOperatorContainer list = this.operatorTable.get(operatorName);
    if (list == null) {
      list = new TermOperatorContainer(operator);
      this.operatorTable.put(operatorName, list);
    } else {
      if (!list.setOperator(operator)) {
        throw new SecurityException(
            "Such or a compatible operator is already presented [" + operatorName + ']');
      }
    }
  }

  @Override
  public TermOperatorContainer findOperatorForName(final JProlContext context, final String name) {
    final TermOperatorContainer systemOperator = context.getSystemOperatorForName(name);
    TermOperatorContainer result;

    if (systemOperator == null) {
      result = operatorTable.get(name);
    } else {
      result = systemOperator;
    }
    return result;
  }

  @Override
  public boolean hasOperatorStartsWith(final JProlContext context, final String str) {
    boolean result = false;
    if (context.hasSystemOperatorStartsWith(str)) {
      result = true;
    } else {
      for (String s : operatorTable.keySet()) {
        if (s.startsWith(str)) {
          result = true;
          break;
        }
      }
    }
    return result;
  }

  @Override
  public CloseableIterator<TermOperator> makeOperatorIterator() {