Cursors.java

package com.github.dakusui.pcond.experimentals.cursor;

import com.github.dakusui.pcond.core.Evaluable;
import com.github.dakusui.pcond.core.Evaluator;
import com.github.dakusui.pcond.core.printable.PrintablePredicate;
import com.github.dakusui.pcond.forms.Predicates;
import com.github.dakusui.pcond.forms.Printables;
import com.github.dakusui.pcond.internals.InternalUtils;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static com.github.dakusui.pcond.forms.Printables.function;
import static com.github.dakusui.pcond.forms.Printables.predicate;
import static com.github.dakusui.pcond.internals.InternalUtils.formatObject;
import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

public enum Cursors {
  ;
  
  /**
   * Note that a predicate returned by this method is stateful and not to be re-used.
   *
   * @param locatorFactory A function to return a cursor which points the location where a given token appears in an original string.
   * @param tokens         Tokens to be found in a given string passed to the returned predicate.
   * @param <T>            The type of token to be searched for.
   * @return A predicate that checks if `tokens` are all contained in a given string
   * in the order, where they appear in the argument.
   */
  @SuppressWarnings("unchecked")
  static <T> Predicate<String> findTokens(Function<T, Function<String, Cursor>> locatorFactory, T... tokens) {
    AtomicBoolean result = new AtomicBoolean(true);
    AtomicInteger lastTestedPosition = new AtomicInteger(0);
    StringBuilder bExpectation = new StringBuilder();
    StringBuilder bActual = new StringBuilder();
    class CursoredString implements Evaluator.Snapshottable {
      public int previousFailingPosition;
      String originalString;
      int    position;

      CursoredString(String originalString) {
        this.originalString = originalString;
        this.position = 0;
      }

      CursoredString findNext(T token) {
        Function<String, Cursor> locator = locatorFactory.apply(token);
        Cursor cursor = locator.apply(originalString.substring(this.position));
        if (cursor.position >= 0) {
          updateOngoingExplanation(bExpectation, token, cursor, (lf, t) -> "found for:" + locatorFactory + "[" + t + "]");
          updateOngoingExplanation(bActual, token, cursor, (lf, t) -> "found for:" + locatorFactory + "[" + t + "]");

          this.position += cursor.position + cursor.length;
        } else {
          this.previousFailingPosition = this.position;
        }
        lastTestedPosition.set(this.position);
        return this;
      }

      private void updateOngoingExplanation(StringBuilder b, T token, Cursor cursor, BiFunction<Object, T, String> locatorFactoryFormatter) {
        b.append(this.originalString, this.position, this.position + cursor.position);
        b.append("<");
        b.append(formatObject(this.originalString.substring(this.position + cursor.position, this.position + cursor.position + cursor.length)));
        b.append(":");
        b.append(locatorFactoryFormatter.apply(locatorFactory, token));
        b.append(">");
      }

      @Override
      public Object snapshot() {
        return originalString.substring(position);
      }

      @Override
      public String toString() {
        return "CursoredString:[" + originalString + "]";
      }
    }
    CursoredString cursoredStringForSnapshotting = new CursoredString(null);
    class CursoredStringPredicate extends PrintablePredicate<CursoredString> implements
        Predicate<CursoredString>,
        Evaluable.LeafPred<CursoredString>,
        Evaluator.Explainable {
      final T each;

      CursoredStringPredicate(T each) {
        super(new Object(), emptyList(), () -> "findTokenBy[" + locatorFactory + "[" + each + "]]", cursoredString -> {
          cursoredStringForSnapshotting.previousFailingPosition = cursoredString.previousFailingPosition;
          cursoredStringForSnapshotting.position = cursoredString.position;
          cursoredStringForSnapshotting.originalString = cursoredString.originalString;
          return cursoredString.position != cursoredString.findNext(each).position;
        });
        this.each = each;
      }

      @Override
      public boolean test(CursoredString v) {
        boolean ret = super.test(v);
        result.set(ret && result.get());
        return ret;
      }

      @Override
      public String toString() {
        return "findTokenBy[" + locatorFactoryName() + "]";
      }

      private String locatorFactoryName() {
        return locatorFactory + "[" + each + "]";
      }

      @Override
      public Predicate<? super CursoredString> predicate() {
        return this;
      }


      @Override
      public Object explainOutputExpectation() {
        return formatExplanation(bExpectation, "SHOULD BE FOUND AFTER THIS POSITION");
      }

      @Override
      public Object explainActual(Object actualValue) {
        return formatExplanation(bActual, "BUT NOT FOUND");
      }

      private String formatExplanation(StringBuilder b, String keyword) {
        String ret = b.toString() + format("%n") + "<" + this.locatorFactoryName() + ":" + keyword + ">";
        b.delete(0, b.length());
        return ret;
      }
    }
    return Predicates.transform(function("findTokens" + formatObject(tokens), CursoredString::new))
        .check(Predicates.allOf(
            Stream.concat(
                    Arrays.stream(tokens).map(CursoredStringPredicate::new),
                    Stream.of(endMarkPredicateForString(lastTestedPosition, bExpectation, bActual, result, () -> cursoredStringForSnapshotting.originalString)))
                .toArray(Predicate[]::new)));

  }
  
  private static Predicate<Object> endMarkPredicateForString(AtomicInteger lastTestedPosition, StringBuilder ongoingExpectationExplanation, StringBuilder ongoingActualExplanation, AtomicBoolean result, Supplier<String> originalStringSupplier) {
    return makeExplainable((PrintablePredicate<? super Object>) predicate("(end)", v -> result.get()), new Evaluator.Explainable() {

      @Override
      public Object explainOutputExpectation() {
        return ongoingExpectationExplanation.toString() + originalStringSupplier.get().substring(lastTestedPosition.get());
      }

      @Override
      public Object explainActual(Object actualValue) {
        return ongoingActualExplanation.toString() + originalStringSupplier.get().substring(lastTestedPosition.get());
      }
    });
  }
  
  private static <T> Predicate<T> makeExplainable(PrintablePredicate<? super T> p, Evaluator.Explainable explainable) {
    class ExplainablePredicate extends PrintablePredicate<T> implements
        Predicate<T>,
        Evaluable.LeafPred<T>,
        Evaluator.Explainable {

      protected ExplainablePredicate() {
        super(new Object(), emptyList(), p::toString, p);
      }

      @Override
      public Predicate<? super T> predicate() {
        return predicate;
      }

      @Override
      public Object explainOutputExpectation() {
        return explainable.explainOutputExpectation();
      }

      @Override
      public Object explainActual(Object actualValue) {
        return explainable.explainActual(actualValue);
      }
    }

    return new ExplainablePredicate();
  }
  
  public static Predicate<String> findSubstrings(String... tokens) {
    return findTokens(Printables.function("substring", token -> string -> new Cursor(string.indexOf(token), token.length())), tokens);
  }
  
  public static Predicate<String> findRegexPatterns(Pattern... patterns) {
    return findTokens(function("matchesRegex", token -> string -> {
      java.util.regex.Matcher m = token.matcher(string);
      if (m.find()) {
        return new Cursor(m.start(), m.end() - m.start());
      } else
        return new Cursor(-1, 0);

    }), patterns);
  }
  
  public static Predicate<String> findRegexes(String... regexes) {
    return findRegexPatterns(Arrays.stream(regexes).map(Pattern::compile).toArray(Pattern[]::new));
  }
  
  @SuppressWarnings("unchecked")
  @SafeVarargs
  public static <E> Predicate<List<E>> findElements(Predicate<? super E>... predicates) {
    AtomicBoolean result = new AtomicBoolean(true);
    List<Object> expectationExplanationList = new LinkedList<>();
    List<Object> actualExplanationList = new LinkedList<>();
    List<Object> rest = new LinkedList<>();
    AtomicInteger previousPosition = new AtomicInteger(0);
    Function<Predicate<? super E>, Predicate<CursoredList<E>>> predicatePredicateFunction = (Predicate<? super E> p) -> (Predicate<CursoredList<E>>) cursoredList -> {
      AtomicInteger j = new AtomicInteger(0);
      boolean isFound = cursoredList.currentList().stream()
          .peek((E each) -> j.getAndIncrement())
          .anyMatch(p);
      if (isFound) {
        updateExplanationsForFoundElement(
            expectationExplanationList, actualExplanationList,
            cursoredList.currentList().get(j.get() - 1),
            p, (List<Object>) cursoredList.currentList().subList(0, j.get() - 1));
        rest.clear();
        rest.add(cursoredList.currentList().subList(j.get(), cursoredList.currentList().size()));
        cursoredList.position += j.get();
        previousPosition.set(cursoredList.position);
        return true;
      }
      updateExplanationsForMissedPredicateIfCursorMoved(
          expectationExplanationList, actualExplanationList,
          cursoredList.position > previousPosition.get(),
          p, cursoredList.currentList().subList(0, j.get()));
      result.set(false);
      previousPosition.set(cursoredList.position);
      return false;
    };
    return Predicates.transform(function("toCursoredList", (List<E> v)-> new CursoredList<>(v)))
        .check(Predicates.allOf(Stream.concat(
                Arrays.stream(predicates)
                    .map((Predicate<? super E> each) -> predicate("findElementBy[" + each + "]", predicatePredicateFunction.apply(each))),
                Stream.of(endMarkPredicateForList(result, expectationExplanationList, actualExplanationList, rest)))
            .toArray(Predicate[]::new)));
  }
  
  private static <E> void updateExplanationsForFoundElement(List<Object> expectationExplanationList, List<Object> actualExplanationList, E foundElement, Predicate<? super E> matchedPredicate, List<Object> skippedElements) {
    if (!skippedElements.isEmpty()) {
      //      expectationExplanationList.add(skippedElements);
      actualExplanationList.add(skippedElements);
    }
    actualExplanationList.add(new Explanation(foundElement, "<%s:found for:" + matchedPredicate + ">"));
    expectationExplanationList.add(new Explanation(matchedPredicate, "<matching element for:%s>"));
  }
  
  private static <E> void updateExplanationsForMissedPredicateIfCursorMoved(List<Object> expectationExplanationList, List<Object> actualExplanationList, boolean isCursorMoved, Predicate<? super E> missedPredicate, List<E> scannedElements) {
    if (isCursorMoved) {
      //expectationExplanationList.add(scannedElements);
      actualExplanationList.add(scannedElements);
    }
    Explanation missedInExpectation = new Explanation(missedPredicate, "<matching element for:%s>");
    expectationExplanationList.add(missedInExpectation);

    Explanation missedInActual = new Explanation(missedPredicate, "<NOT FOUND:matching element for:%s>");
    actualExplanationList.add(missedInActual);
  }
  
  private static Predicate<Object> endMarkPredicateForList(AtomicBoolean result, List<Object> expectationExplanationList, List<Object> actualExplanationList, List<?> rest) {
    return makeExplainable((PrintablePredicate<? super Object>) predicate("(end)", v -> result.get()), new Evaluator.Explainable() {

      @Override
      public Object explainOutputExpectation() {
        return renderExplanationString(expectationExplanationList);
      }

      @Override
      public Object explainActual(Object actualValue) {
        return renderExplanationString(createFullExplanationList(actualExplanationList, rest));
      }

      private List<Object> createFullExplanationList(List<Object> explanationList, List<?> rest) {
        return Stream.concat(explanationList.stream(), rest.stream()).collect(toList());
      }

      private String renderExplanationString(List<Object> fullExplanationList) {
        return fullExplanationList
            .stream()
            .map(e -> {
              if (e instanceof List) {
                return String.format("<%s:skipped>",
                    ((List<?>) e).stream()
                        .map(InternalUtils::formatObject)
                        .collect(joining(",")));
              }
              return e;
            })
            .map(Object::toString)
            .collect(joining(String.format("%n")));
      }
    });
  }
  
  static class Cursor {
    /**
     * The "relative" position, where the token was found, from the beginning of the string passed to a locator.
     * By convention, it is designed to pass a substring of the original string, which starts from the position,
     * where a token (element) searching attempt was made.
     */
    final int position;
    /**
     * A length of a token to be searched.
     */
    final int length;

    Cursor(int position, int length) {
      this.position = position;
      this.length = length;
    }
  }
  
  static class CursoredList<EE> extends AbstractList<EE> implements Evaluator.Snapshottable, Collection<EE> {
    int position;
    final List<EE> originalList;

    CursoredList(List<EE> originalList) {
      this.originalList = originalList;
    }

    List<EE> currentList() {
      return this.originalList.subList(position, this.originalList.size());
    }

    @Override
    public Object snapshot() {
      return originalList.subList(position, originalList.size());
    }

    @Override
    public int size() {
      return originalList.size() - position;
    }

    @Override
    public EE get(int index) {
      return originalList.get(position + index);
    }

    @Override
    public String toString() {
      return "CursoredList:" + originalList;
    }
  }
  
  private static class Explanation {
    final         Object value;
    private final String formatString;

    private Explanation(Object value, String formatString) {
      this.value = value;
      this.formatString = formatString;
    }

    @Override
    public String toString() {
      return format(formatString, formatObject(this.value));
    }
  }
}