/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.uima.ruta.rule;

import static java.util.Arrays.asList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import org.apache.uima.cas.text.AnnotationFS;
import org.apache.uima.jcas.JCas;
import org.apache.uima.ruta.RutaEnvironment;
import org.apache.uima.ruta.RutaStream;
import org.apache.uima.ruta.action.AbstractRutaAction;
import org.apache.uima.ruta.block.RutaBlock;
import org.apache.uima.ruta.condition.AbstractRutaCondition;
import org.apache.uima.ruta.rule.quantifier.RuleElementQuantifier;
import org.apache.uima.ruta.type.RutaFrame;
import org.apache.uima.ruta.visitor.InferenceCrowd;

public class ComposedRuleElement extends AbstractRuleElement implements RuleElementContainer {

  protected List<RuleElement> elements;

  protected RuleElementContainer caretaker;

  private Boolean conjunct = null;

  private Comparator<RuleMatch> ruleMatchComparator = new RuleMatchComparator();

  public ComposedRuleElement(List<RuleElement> elements, RuleElementQuantifier quantifier,
          List<AbstractRutaCondition> conditions, List<AbstractRutaAction> actions,
          RuleElementContainer container, RutaBlock parent) {
    super(quantifier, conditions, actions, container, parent);
    this.elements = elements;
    caretaker = new RuleElementCaretaker(this);
  }

  @Override
  public void apply(RuleMatch match, RutaStream symbolStream, InferenceCrowd crowd) {
    applyRuleElements(match, symbolStream, crowd);
    super.apply(match, symbolStream, crowd);
  }

  @Override
  public List<RuleMatch> startMatch(RuleMatch ruleMatch, RuleApply ruleApply,
          ComposedRuleElementMatch containerMatch, RuleElement entryPoint, RutaStream stream,
          InferenceCrowd crowd) {
    List<RuleMatch> result = new ArrayList<>();
    if (conjunct == null) {
      ComposedRuleElementMatch composedMatch = createComposedMatch(ruleMatch, containerMatch,
              stream);
      RuleElement anchorElement = getAnchoringRuleElement(stream);
      result = anchorElement.startMatch(ruleMatch, ruleApply, composedMatch, entryPoint, stream,
              crowd);
    } else if (!conjunct) {
      // disjunctive
      Map<RuleMatch, ComposedRuleElementMatch> ruleMatches = new LinkedHashMap<>();
      for (RuleElement each : elements) {
        ComposedRuleElementMatch extendedContainerMatch = containerMatch.copy();
        RuleMatch extendedMatch = ruleMatch.copy(extendedContainerMatch, true);
        ComposedRuleElementMatch composedMatch = createComposedMatch(extendedMatch,
                extendedContainerMatch, stream);
        List<RuleMatch> startRuleMatches = each.startMatch(extendedMatch, null, composedMatch, this,
                stream, crowd);
        for (RuleMatch startRuleMatch : startRuleMatches) {

          ComposedRuleElementMatch startElementMatch = (ComposedRuleElementMatch) startRuleMatch
                  .getLastMatch(this, true);

          ruleMatches.put(startRuleMatch, startElementMatch);
        }
      }

      Map<RuleMatch, ComposedRuleElementMatch> mergedMatches = mergeDisjunctiveRuleMatches(
              ruleMatches, true, stream);
      Set<Entry<RuleMatch, ComposedRuleElementMatch>> entrySet = mergedMatches.entrySet();
      for (Entry<RuleMatch, ComposedRuleElementMatch> entry : entrySet) {
        RuleMatch eachRuleMatch = entry.getKey();
        ComposedRuleElementMatch eachComposedMatch = entry.getValue();
        MatchContext context = new MatchContext(null, this, eachRuleMatch, true);
        AnnotationFS lastAnnotation = eachRuleMatch.getLastMatchedAnnotation(context, stream);
        boolean failed = !eachComposedMatch.matched();

        RuleElement sideStepOrigin = hasAncestor(false) ? this : null;

        List<RuleMatch> fallbackContinue = fallbackContinue(true, failed, lastAnnotation,
                eachRuleMatch, ruleApply, eachComposedMatch, sideStepOrigin, entryPoint, stream,
                crowd);
        result.addAll(fallbackContinue);
      }
    } else if (conjunct) {
      // conjunctive
      Map<RuleMatch, ComposedRuleElementMatch> ruleMatches = new LinkedHashMap<>();
      RuleElement anchoringRuleElement = getAnchoringRuleElement(stream);

      ComposedRuleElementMatch composedMatch = createComposedMatch(ruleMatch, containerMatch,
              stream);
      List<RuleMatch> startRuleMatches = anchoringRuleElement.startMatch(ruleMatch, null,
              composedMatch, this, stream, crowd);
      for (RuleMatch eachStartRuleMatch : startRuleMatches) {
        if (eachStartRuleMatch.matched()) {
          AnnotationFS prefixAnnotation = getPrefixAnnotation(eachStartRuleMatch, stream);
          for (RuleElement each : elements) {
            if (each.equals(anchoringRuleElement)) {
              continue;
            }
            ComposedRuleElementMatch startElementMatch = (ComposedRuleElementMatch) eachStartRuleMatch
                    .getLastMatch(this, true);
            List<RuleMatch> continueMatch = each.continueMatch(true, prefixAnnotation,
                    eachStartRuleMatch, null, startElementMatch, null, this, stream, crowd);
            for (RuleMatch startRuleMatch : continueMatch) {
              ComposedRuleElementMatch elementMatch = (ComposedRuleElementMatch) startRuleMatch
                      .getLastMatch(this, true);
              ruleMatches.put(startRuleMatch, elementMatch);
            }
          }
        }
      }

      Map<RuleMatch, ComposedRuleElementMatch> mergedMatches = mergeConjunctiveRuleMatches(
              ruleMatches, true);
      Set<Entry<RuleMatch, ComposedRuleElementMatch>> entrySet = mergedMatches.entrySet();
      for (Entry<RuleMatch, ComposedRuleElementMatch> entry : entrySet) {
        RuleMatch eachRuleMatch = entry.getKey();
        ComposedRuleElementMatch eachComposedMatch = entry.getValue();
        MatchContext context = new MatchContext(this, eachRuleMatch);
        AnnotationFS lastAnnotation = eachRuleMatch.getLastMatchedAnnotation(context, stream);
        boolean failed = !eachComposedMatch.matched();
        List<AnnotationFS> textsMatched = eachComposedMatch.getTextsMatched();
        if ((!stream.isGreedyAnchoring() && !stream.isOnlyOnce())
                || !earlyExit(textsMatched.get(0), ruleApply, stream)) {

          RuleElement sideStepOrigin = hasAncestor(false) ? this : null;

          List<RuleMatch> fallbackContinue = fallbackContinue(true, failed, lastAnnotation,
                  eachRuleMatch, ruleApply, eachComposedMatch, sideStepOrigin, entryPoint, stream,
                  crowd);
          result.addAll(fallbackContinue);
        }
      }
    }
    return result;
  }

  private AnnotationFS getPrefixAnnotation(RuleMatch ruleMatch, RutaStream stream) {
    MatchContext context = new MatchContext(this, ruleMatch);
    AnnotationFS lastMatchedAnnotation = ruleMatch.getLastMatchedAnnotation(context, stream);
    if (lastMatchedAnnotation.getBegin() == 0) {
      JCas jCas = stream.getJCas();
      AnnotationFS dummy = new RutaFrame(jCas, 0, 0);
      return dummy;
    }
    return stream.getEndAnchor(lastMatchedAnnotation.getBegin());
  }

  protected ComposedRuleElementMatch createComposedMatch(RuleMatch ruleMatch,
          ComposedRuleElementMatch containerMatch, RutaStream stream) {
    ComposedRuleElementMatch composedMatch = new ComposedRuleElementMatch(this, containerMatch);
    includeMatch(ruleMatch, containerMatch, composedMatch, stream);
    return composedMatch;
  }

  @Override
  public List<RuleMatch> continueMatch(boolean after, AnnotationFS annotation, RuleMatch ruleMatch,
          RuleApply ruleApply, ComposedRuleElementMatch containerMatch, RuleElement sideStepOrigin,
          RuleElement entryPoint, RutaStream stream, InferenceCrowd crowd) {
    List<RuleMatch> result = new ArrayList<>();
    if (conjunct == null) {
      // inner next sequential
      RuleElement nextElement = getNextElement(after, this);
      if (nextElement != null) {
        ComposedRuleElementMatch composedMatch = createComposedMatch(ruleMatch, containerMatch,
                stream);
        result = nextElement.continueMatch(after, annotation, ruleMatch, ruleApply, composedMatch,
                sideStepOrigin, entryPoint, stream, crowd);
      } else {
        result = fallback(after, false, annotation, ruleMatch, ruleApply, containerMatch,
                sideStepOrigin, entryPoint, stream, crowd);
      }
    } else if (!conjunct) {
      // disjunctive
      Map<RuleMatch, ComposedRuleElementMatch> ruleMatches = new HashMap<>();
      for (RuleElement each : elements) {
        ComposedRuleElementMatch extendedContainerMatch = containerMatch.copy();
        RuleMatch extendedMatch = ruleMatch.copy(extendedContainerMatch, after);
        ComposedRuleElementMatch composedMatch = createComposedMatch(extendedMatch,
                extendedContainerMatch, stream);
        List<RuleMatch> continueRuleMatches = each.continueMatch(after, annotation, extendedMatch,
                null, composedMatch, sideStepOrigin, this, stream, crowd);
        for (RuleMatch continueRuleMatch : continueRuleMatches) {
          ComposedRuleElementMatch startElementMatch = (ComposedRuleElementMatch) continueRuleMatch
                  .getLastMatch(this, true);
          ruleMatches.put(continueRuleMatch, startElementMatch);
        }
      }
      // TODO sort matches, no need to merge them, right?!
      Map<RuleMatch, ComposedRuleElementMatch> mergedMatches = mergeDisjunctiveRuleMatches(
              ruleMatches, after, stream);
      Set<Entry<RuleMatch, ComposedRuleElementMatch>> entrySet = mergedMatches.entrySet();
      for (Entry<RuleMatch, ComposedRuleElementMatch> entry : entrySet) {
        RuleMatch eachRuleMatch = entry.getKey();
        ComposedRuleElementMatch eachComposedMatch = entry.getValue();
        MatchContext context = new MatchContext(annotation, this, eachRuleMatch, after);
        AnnotationFS lastAnnotation = eachRuleMatch.getLastMatchedAnnotation(context, stream);
        boolean failed = !eachComposedMatch.matched();
        List<AnnotationFS> textsMatched = eachRuleMatch.getMatchedAnnotationsOfRoot();
        if ((!stream.isGreedyAnchoring() && !stream.isOnlyOnce()) || (!textsMatched.isEmpty()
                && !earlyExit(textsMatched.get(0), ruleApply, stream))) {
          List<RuleMatch> fallbackContinue = fallbackContinue(after, failed, lastAnnotation,
                  eachRuleMatch, ruleApply, eachComposedMatch, sideStepOrigin, entryPoint, stream,
                  crowd);
          result.addAll(fallbackContinue);
        }
      }
    } else if (conjunct) {
      // conjunctive
      Map<RuleMatch, ComposedRuleElementMatch> ruleMatches = new HashMap<>();

      RuleElement anchoringRuleElement = getAnchoringRuleElement(stream);
      ComposedRuleElementMatch composedMatch = createComposedMatch(ruleMatch, containerMatch,
              stream);
      List<RuleMatch> startRuleMatches = anchoringRuleElement.continueMatch(after, annotation,
              ruleMatch, null, composedMatch, sideStepOrigin, this, stream, crowd);

      for (RuleMatch eachStartRuleMatch : startRuleMatches) {
        for (RuleElement each : elements) {
          if (each.equals(anchoringRuleElement)) {
            continue;
          }
          ComposedRuleElementMatch startElementMatch = (ComposedRuleElementMatch) eachStartRuleMatch
                  .getLastMatch(this, after);
          List<RuleMatch> continueMatch = each.continueMatch(after, annotation, eachStartRuleMatch,
                  null, startElementMatch, null, this, stream, crowd);
          for (RuleMatch startRuleMatch : continueMatch) {
            ComposedRuleElementMatch elementMatch = (ComposedRuleElementMatch) startRuleMatch
                    .getLastMatch(this, after);
            ruleMatches.put(startRuleMatch, elementMatch);
          }
        }
      }
      Map<RuleMatch, ComposedRuleElementMatch> mergedMatches = mergeConjunctiveRuleMatches(
              ruleMatches, after);
      Set<Entry<RuleMatch, ComposedRuleElementMatch>> entrySet = mergedMatches.entrySet();
      for (Entry<RuleMatch, ComposedRuleElementMatch> entry : entrySet) {
        RuleMatch eachRuleMatch = entry.getKey();
        ComposedRuleElementMatch eachComposedMatch = entry.getValue();
        MatchContext context = new MatchContext(this, eachRuleMatch);
        AnnotationFS lastAnnotation = eachRuleMatch.getLastMatchedAnnotation(context, stream);

        boolean failed = !eachComposedMatch.matched();

        List<AnnotationFS> textsMatched = eachRuleMatch.getMatchedAnnotationsOfRoot();
        if ((!stream.isGreedyAnchoring() && !stream.isOnlyOnce()) || (!textsMatched.isEmpty()
                && !earlyExit(textsMatched.get(0), ruleApply, stream))) {
          List<RuleMatch> fallbackContinue = fallbackContinue(after, failed, lastAnnotation,
                  eachRuleMatch, ruleApply, eachComposedMatch, sideStepOrigin, entryPoint, stream,
                  crowd);
          result.addAll(fallbackContinue);
        }
      }
    }
    return result;
  }

  private Map<RuleMatch, ComposedRuleElementMatch> mergeConjunctiveRuleMatches(
          Map<RuleMatch, ComposedRuleElementMatch> ruleMatches, boolean direction) {
    // TODO hotfix: this needs a correct implementation
    return ruleMatches;
    // Map<RuleMatch, ComposedRuleElementMatch> result = new HashMap<RuleMatch,
    // ComposedRuleElementMatch>();
    // Set<Entry<RuleMatch, ComposedRuleElementMatch>> entrySet = ruleMatches.entrySet();
    // Entry<RuleMatch, ComposedRuleElementMatch> largestEntry = null;
    // boolean allMatched = true;
    // AnnotationFS largestAnnotation = null;
    // for (Entry<RuleMatch, ComposedRuleElementMatch> entry : entrySet) {
    // RuleMatch ruleMatch = entry.getKey();
    // ComposedRuleElementMatch elementMatch = entry.getValue();
    // allMatched &= elementMatch.matched();
    // AnnotationFS lastMatchedAnnotation = ruleMatch.getLastMatchedAnnotation(getFirstElement(),
    // direction);
    // if (largestEntry == null) {
    // largestEntry = entry;
    // largestAnnotation = lastMatchedAnnotation;
    // } else {
    // if (lastMatchedAnnotation != null
    // && largestAnnotation != null
    // && lastMatchedAnnotation.getCoveredText().length() > largestAnnotation
    // .getCoveredText().length()) {
    // largestEntry = entry;
    // largestAnnotation = lastMatchedAnnotation;
    // }
    // }
    // }
    // if (allMatched) {
    // result.put(largestEntry.getKey(), largestEntry.getValue());
    // }
    // return result;
  }

  private Map<RuleMatch, ComposedRuleElementMatch> mergeDisjunctiveRuleMatches(
          Map<RuleMatch, ComposedRuleElementMatch> ruleMatches, boolean direction,
          RutaStream stream) {
    // TODO hotfix: this needs a correct implementation
    Map<RuleMatch, ComposedRuleElementMatch> result = new TreeMap<>(ruleMatchComparator);
    Set<Entry<RuleMatch, ComposedRuleElementMatch>> entrySet = ruleMatches.entrySet();
    Entry<RuleMatch, ComposedRuleElementMatch> largestEntry = null;
    AnnotationFS largestAnnotation = null;
    for (Entry<RuleMatch, ComposedRuleElementMatch> entry : entrySet) {
      RuleMatch ruleMatch = entry.getKey();
      ComposedRuleElementMatch elementMatch = entry.getValue();
      if (elementMatch.matched()) {
        result.putIfAbsent(ruleMatch, elementMatch);
      } else {
        MatchContext context = new MatchContext(getFirstElement(), ruleMatch, direction);
        AnnotationFS lastMatchedAnnotation = ruleMatch.getLastMatchedAnnotation(context, stream);
        if (largestEntry == null) {
          largestEntry = entry;
          largestAnnotation = lastMatchedAnnotation;
        } else {
          if (lastMatchedAnnotation != null && largestAnnotation != null && lastMatchedAnnotation
                  .getCoveredText().length() > largestAnnotation.getCoveredText().length()) {
            largestEntry = entry;
            largestAnnotation = lastMatchedAnnotation;
          }
        }
      }
    }
    if (result.isEmpty()) {
      if (largestEntry != null) {
        result.put(largestEntry.getKey(), largestEntry.getValue());
      } else {
        result = ruleMatches;
      }
    }
    return result;
  }

  @Override
  public List<RuleMatch> continueOwnMatch(boolean after, AnnotationFS annotation,
          RuleMatch ruleMatch, RuleApply ruleApply, ComposedRuleElementMatch containerMatch,
          RuleElement sideStepOrigin, RuleElement entryPoint, RutaStream stream,
          InferenceCrowd crowd) {
    List<RuleMatch> result = new ArrayList<>();
    if (!stream.isSimpleGreedyForComposed()) {
      result = continueMatch(after, annotation, ruleMatch, ruleApply, containerMatch,
              sideStepOrigin, entryPoint, stream, crowd);
    } else {
      // HOTFIX
      boolean stopMatching = false;
      boolean failed = false;
      AnnotationFS nextAnnotation = annotation;
      while (!stopMatching) {
        RuleElement nextElement = getNextElement(after, this);
        if (nextElement != null) {
          ComposedRuleElementMatch composedMatch = createComposedMatch(ruleMatch, containerMatch,
                  stream);
          nextElement.continueMatch(after, nextAnnotation, ruleMatch, ruleApply, composedMatch,
                  sideStepOrigin, this, stream, crowd);
          ComposedRuleElementMatch parentContainerMatch = containerMatch.getContainerMatch();
          List<RuleElementMatch> match = getMatch(ruleMatch, parentContainerMatch);
          int lenghtBefore = match.size();
          MatchContext context = new MatchContext(this, ruleMatch, after);
          List<RuleElementMatch> evaluateMatches = quantifier.evaluateMatches(match, context,
                  stream, crowd);
          ruleMatch.setMatched(ruleMatch.matched() && evaluateMatches != null);
          if (evaluateMatches != null && evaluateMatches.size() != lenghtBefore) {
            failed = true;
            stopMatching = true;
          }
          if (!quantifier.continueMatch(after, context, nextAnnotation, containerMatch, stream,
                  crowd)) {
            stopMatching = true;
          }
          if (evaluateMatches != null) {
            List<AnnotationFS> textsMatched = evaluateMatches.get(evaluateMatches.size() - 1)
                    .getTextsMatched();
            nextAnnotation = textsMatched.get(textsMatched.size() - 1);
          }
        } else {
          stopMatching = true;
        }
      }
      result = fallback(after, failed, nextAnnotation, ruleMatch, ruleApply, containerMatch,
              sideStepOrigin, entryPoint, stream, crowd);
    }
    return result;
  }

  public List<RuleMatch> fallbackContinue(boolean after, boolean failed, AnnotationFS annotation,
          RuleMatch ruleMatch, RuleApply ruleApply, ComposedRuleElementMatch containerMatch,
          RuleElement sideStepOrigin, RuleElement entryPoint, RutaStream stream,
          InferenceCrowd crowd) {
    List<RuleMatch> result = new ArrayList<>();
    RuleElementContainer container = getContainer();
    doMatch(after, annotation, ruleMatch, containerMatch, isStartAnchor(), stream, crowd);
    if (equals(entryPoint) && ruleApply == null) {
      if (failed) {
        // inform caller about the failed state using the matched info
        ruleMatch.setMatched(false);
      }
      result.add(ruleMatch);
    } else if (container == null) {
      result = fallback(after, failed, annotation, ruleMatch, ruleApply, containerMatch,
              sideStepOrigin, entryPoint, stream, crowd);
    } else {
      ComposedRuleElementMatch parentContainerMatch = containerMatch.getContainerMatch();
      RuleElement nextElement = container.getNextElement(after, this);
      List<RuleElementMatch> match = getMatch(ruleMatch, parentContainerMatch);
      int sizeBefore = match.size();
      MatchContext context = new MatchContext(annotation, this, ruleMatch, after);
      boolean continueMatch = quantifier.continueMatch(after, context, annotation,
              parentContainerMatch, stream, crowd);
      List<RuleElementMatch> evaluateMatches = quantifier.evaluateMatches(match, context, stream,
              crowd);
      int sizeAfter = evaluateMatches != null ? evaluateMatches.size() : sizeBefore;
      boolean removedFailedMatches = sizeAfter < sizeBefore;
      if (removedFailedMatches) {
        containerMatch.enforceUpdate();
      }
      ruleMatch.setMatched((ruleMatch.matched() || removedFailedMatches)
              && (evaluateMatches != null || continueMatch || sideStepOrigin != null));
      if (failed) {
        // TODO failed was caused by a child: should here failed = false?
        if (!removedFailedMatches && evaluateMatches != null && continueMatch) {
          result = continueOwnMatch(after, annotation, ruleMatch, ruleApply, parentContainerMatch,
                  sideStepOrigin, entryPoint, stream, crowd);
        } else if (nextElement != null) {
          AnnotationFS backtrackedAnnotation = getBacktrackedAnnotation(after, evaluateMatches,
                  annotation);
          if (backtrackedAnnotation != null) {
            result = nextElement.continueMatch(after, backtrackedAnnotation, ruleMatch, ruleApply,
                    parentContainerMatch, sideStepOrigin, entryPoint, stream, crowd);
          } else {
            result = fallback(after, failed, annotation, ruleMatch, ruleApply, parentContainerMatch,
                    sideStepOrigin, entryPoint, stream, crowd);
          }
        } else {
          if (equals(entryPoint)) {
            // hotfix for UIMA-3820
            result.add(ruleMatch);
          } else {
            // TODO: do we need to backtrack the annotation?
            result = fallback(after, !removedFailedMatches, annotation, ruleMatch, ruleApply,
                    parentContainerMatch, sideStepOrigin, entryPoint, stream, crowd);
            // was:
            // [Peter]: really failed, even if the failed match was removed?
            // result = fallback(after, failed, annotation, ruleMatch, ruleApply,
            // parentContainerMatch, sideStepOrigin, entryPoint, stream, crowd);
          }
        }
      } else {
        if (continueMatch && !removedFailedMatches) {
          result = continueOwnMatch(after, annotation, ruleMatch, ruleApply, parentContainerMatch,
                  sideStepOrigin, entryPoint, stream, crowd);
        } else if (nextElement != null) {
          result = nextElement.continueMatch(after, annotation, ruleMatch, ruleApply,
                  parentContainerMatch, sideStepOrigin, entryPoint, stream, crowd);
        } else {
          result = fallback(after, failed, annotation, ruleMatch, ruleApply, parentContainerMatch,
                  sideStepOrigin, entryPoint, stream, crowd);
        }
      }
    }
    return result;
  }

  private AnnotationFS getBacktrackedAnnotation(boolean after,
          List<RuleElementMatch> evaluateMatches, AnnotationFS annotation) {
    if (evaluateMatches == null) {
      return null;
    }
    if (evaluateMatches.isEmpty()) {
      return annotation;
    }

    // ensure correct ordering
    Collections.sort(evaluateMatches, new Comparator<RuleElementMatch>() {

      @Override
      public int compare(RuleElementMatch rem1, RuleElementMatch rem2) {
        List<AnnotationFS> textsMatched1 = rem1.getTextsMatched();
        List<AnnotationFS> textsMatched2 = rem2.getTextsMatched();
        if ((textsMatched1 == null || textsMatched1.isEmpty())
                && (textsMatched2 == null || textsMatched2.isEmpty())) {
          return 0;
        }
        if (textsMatched1 == null
                || textsMatched1.isEmpty() && !(textsMatched2 == null || textsMatched2.isEmpty())) {
          return -1;
        }
        if (!(textsMatched1 == null || textsMatched1.isEmpty())
                && (textsMatched2 == null || textsMatched2.isEmpty())) {
          return 1;
        }
        if (textsMatched1.equals(textsMatched2)) {
          return 0;
        }
        AnnotationFS first1 = textsMatched1.get(0);
        AnnotationFS last1 = textsMatched1.get(textsMatched1.size() - 1);
        AnnotationFS first2 = textsMatched2.get(0);
        AnnotationFS last2 = textsMatched2.get(textsMatched2.size() - 1);
        int compareBegin = Integer.compare(first1.getBegin(), first2.getBegin());
        if (compareBegin != 0) {
          return compareBegin;
        }
        int compareEnd = Integer.compare(last1.getEnd(), last2.getEnd());
        return compareEnd;
      }
    });

    if (after) {
      List<AnnotationFS> textsMatched = evaluateMatches.get(evaluateMatches.size() - 1)
              .getTextsMatched();
      if (textsMatched.isEmpty()) {
        return null;
      }
      AnnotationFS backtrackedAnnotation = textsMatched.get(textsMatched.size() - 1);
      return backtrackedAnnotation;
    } else {
      List<AnnotationFS> textsMatched = evaluateMatches.get(0).getTextsMatched();
      if (textsMatched.isEmpty()) {
        return null;
      }
      AnnotationFS backtrackedAnnotation = textsMatched.get(0);
      return backtrackedAnnotation;
    }
  }

  private List<RuleMatch> fallback(boolean after, boolean failed, AnnotationFS annotation,
          RuleMatch ruleMatch, RuleApply ruleApply, ComposedRuleElementMatch containerMatch,
          RuleElement sideStepOrigin, RuleElement entryPoint, RutaStream stream,
          InferenceCrowd crowd) {
    RuleElementContainer parentContainer = getContainer();
    if (parentContainer instanceof ComposedRuleElement) {
      ComposedRuleElement parentElement = (ComposedRuleElement) parentContainer;
      return parentElement.fallbackContinue(after, failed, annotation, ruleMatch, ruleApply,
              containerMatch, sideStepOrigin, entryPoint, stream, crowd);
    }

    if (sideStepOrigin != null && !failed && sideStepOrigin.getContainer() != null) {
      // only continue sidestep if we did not yet reach the root
      return sideStepOrigin.continueSideStep(after, ruleMatch, ruleApply, containerMatch,
              entryPoint, stream, crowd);
    }

    // take care that failed matches wont be applied
    ruleMatch.setMatched(ruleMatch.matched && !failed);
    doneMatching(ruleMatch, ruleApply, stream, crowd);
    return asList(ruleMatch);
  }

  private void includeMatch(RuleMatch ruleMatch, ComposedRuleElementMatch containerMatch,
          ComposedRuleElementMatch composedMatch, RutaStream stream) {
    if (containerMatch == null) {
      ruleMatch.setRootMatch(composedMatch);
    } else {
      containerMatch.addInnerMatch(this, composedMatch, false, stream);
    }
  }

  @Override
  public void doMatch(boolean after, AnnotationFS annotation, RuleMatch ruleMatch,
          ComposedRuleElementMatch containerMatch, boolean ruleAnchor, RutaStream stream,
          InferenceCrowd crowd) {

    List<AnnotationFS> textsMatched = containerMatch.getTextsMatched();
    if (textsMatched == null || textsMatched.isEmpty()) {
      getParent().getEnvironment().addMatchToVariable(ruleMatch, this,
              new MatchContext(getParent()), stream);
      containerMatch.evaluateInnerMatches(true, stream);
      return;
    }
    int begin = textsMatched.get(0).getBegin();
    int end = textsMatched.get(textsMatched.size() - 1).getEnd();
    AnnotationFS implicitAnnotation = stream.getCas()
            .createAnnotation(stream.getCas().getAnnotationType(), begin, end);

    MatchContext context = new MatchContext(implicitAnnotation, this, ruleMatch, after);
    RutaEnvironment environment = context.getParent().getEnvironment();
    environment.addMatchToVariable(ruleMatch, this, context, stream);

    List<EvaluatedCondition> evaluatedConditions = new ArrayList<>(conditions.size());
    for (AbstractRutaCondition condition : conditions) {
      crowd.beginVisit(condition, null);
      EvaluatedCondition eval = condition.eval(context, stream, crowd);
      crowd.endVisit(condition, null);
      evaluatedConditions.add(eval);
      if (!eval.isValue()) {
        break;
      }
    }
    containerMatch.setConditionInfo(evaluatedConditions);
    containerMatch.evaluateInnerMatches(true, stream);
    if (containerMatch.matched()) {
      boolean inlinedRulesMatched = matchInlinedRules(ruleMatch, containerMatch, stream, crowd);
      containerMatch.setInlinedRulesMatched(inlinedRulesMatched);
    } else {
      // update label for failed match after evaluating conditions
      environment.removeVariableValue(getLabel(), context);
    }
  }

  @Override
  public Collection<? extends AnnotationFS> getAnchors(RutaStream stream) {
    RuleElement anchorElement = getAnchoringRuleElement(stream);
    Collection<? extends AnnotationFS> anchors = anchorElement.getAnchors(stream);
    return anchors;
  }

  @Override
  public long estimateAnchors(RutaStream stream) {
    long result = 1;
    for (RuleElement each : elements) {
      result += each.estimateAnchors(stream);
    }
    MatchContext context = new MatchContext(null, null);
    if (quantifier.isOptional(context, stream)) {
      // three times since sibling elements maybe need to be checked
      result *= 3 * (int) stream.getIndexPenalty();
    }
    return result;
  }

  @Override
  public RuleElement getAnchoringRuleElement(RutaStream stream) {
    return caretaker.getAnchoringRuleElement(stream);
  }

  @Override
  public List<RuleElement> getRuleElements() {
    return elements;
  }

  public void setRuleElements(List<RuleElement> elements) {
    this.elements = elements;
  }

  @Override
  public RuleElement getFirstElement() {
    return caretaker.getFirstElement();
  }

  @Override
  public RuleElement getLastElement() {
    return caretaker.getLastElement();
  }

  @Override
  public void applyRuleElements(RuleMatch ruleMatch, RutaStream stream, InferenceCrowd crowd) {
    caretaker.applyRuleElements(ruleMatch, stream, crowd);
  }

  @Override
  public String toString() {
    String con = "";
    if (conjunct != null) {
      con = conjunct ? "&" : "|";
    }
    String simpleName = getQuantifier().getClass().getSimpleName();
    return "(" + con + (elements == null ? "null" : elements.toString()) + ")"
            + (simpleName.equals("NormalQuantifier") ? "" : simpleName)
    // + (conditions.isEmpty() ? "" : "(" + conditions.toString() + ")" + "\\n")
    // + (actions.isEmpty() ? "" : "{" + actions.toString() + "}")
    ;
  }

  @Override
  public RuleElement getNextElement(boolean after, RuleElement ruleElement) {
    // return caretaker.getNextElement(after, ruleElement);
    if (conjunct == null || equals(ruleElement)) {
      return caretaker.getNextElement(after, ruleElement);
    } else {
      return null;
    }
  }

  public void setConjunct(Boolean conjunct) {
    this.conjunct = conjunct;
  }

  public Boolean getConjunct() {
    return conjunct;
  }

}
