/*
 * 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.Collections;
import java.util.List;

import org.apache.uima.cas.text.AnnotationFS;
import org.apache.uima.ruta.RutaElement;
import org.apache.uima.ruta.RutaStatement;
import org.apache.uima.ruta.RutaStream;
import org.apache.uima.ruta.ScriptApply;
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.NormalQuantifier;
import org.apache.uima.ruta.rule.quantifier.RuleElementQuantifier;
import org.apache.uima.ruta.visitor.InferenceCrowd;

public abstract class AbstractRuleElement extends RutaElement implements RuleElement {

  protected static final InferenceCrowd EMPTY_CROWD = new InferenceCrowd(Collections.emptyList());

  protected RuleElementQuantifier quantifier;

  protected List<AbstractRutaCondition> conditions;

  protected List<AbstractRutaAction> actions;

  private boolean startAnchor;

  private String label;

  private RuleElementContainer container;

  protected RutaBlock parent;

  protected List<List<RutaStatement>> inlinedConditionRuleBlocks = new ArrayList<>();

  protected List<List<RutaStatement>> inlinedActionRuleBlocks = new ArrayList<>();

  public AbstractRuleElement(RuleElementQuantifier quantifier,
          List<AbstractRutaCondition> conditions, List<AbstractRutaAction> actions,
          RuleElementContainer container, RutaBlock parent) {
    super();
    this.quantifier = quantifier;
    this.conditions = conditions;
    this.actions = actions;
    this.container = container;
    this.parent = parent;
    if (this.conditions == null) {
      this.conditions = new ArrayList<>();
    }
    if (this.actions == null) {
      this.actions = new ArrayList<>();
    }
    if (this.quantifier == null) {
      this.quantifier = new NormalQuantifier();
    }
  }

  @Override
  public List<RuleMatch> continueSideStep(boolean after, RuleMatch ruleMatch, RuleApply ruleApply,
          ComposedRuleElementMatch containerMatch, RuleElement entryPoint, RutaStream stream,
          InferenceCrowd crowd) {
    List<RuleMatch> result = new ArrayList<>();
    boolean newDirection = !after;
    List<AnnotationFS> matchedAnnotationsOf = ruleMatch.getMatchedAnnotationsOfElement(this);
    AnnotationFS annotation = null;
    if (!matchedAnnotationsOf.isEmpty()) {
      if (newDirection) {
        annotation = matchedAnnotationsOf.get(matchedAnnotationsOf.size() - 1);
      } else {
        annotation = matchedAnnotationsOf.get(0);
      }
      ComposedRuleElementMatch sideStepContainerMatch = containerMatch;
      if (!containerMatch.getRuleElement().equals(getContainer())) {
        List<List<RuleElementMatch>> matchInfo = ruleMatch
                .getMatchInfo((ComposedRuleElement) getContainer());
        if (newDirection) {
          List<RuleElementMatch> list = matchInfo.get(matchInfo.size() - 1);
          sideStepContainerMatch = (ComposedRuleElementMatch) list.get(list.size() - 1);
        } else {
          List<RuleElementMatch> list = matchInfo.get(0);
          sideStepContainerMatch = (ComposedRuleElementMatch) list.get(0);
        }
      }
      MatchContext context = new MatchContext(this, ruleMatch, newDirection);
      if (quantifier.continueMatch(newDirection, context, annotation, sideStepContainerMatch,
              stream, crowd)) {
        continueMatch(newDirection, annotation, ruleMatch, ruleApply, sideStepContainerMatch, null,
                entryPoint, stream, crowd);
      } else {
        RuleElement nextRuleElement = getContainer().getNextElement(newDirection, this);
        if (nextRuleElement != null) {
          RuleElement sideStepOrigin = null;
          if (getContainer() instanceof RuleElement) {
            sideStepOrigin = (RuleElement) getContainer();
          }
          result = nextRuleElement.continueMatch(newDirection, annotation, ruleMatch, ruleApply,
                  sideStepContainerMatch, sideStepOrigin, entryPoint, stream, crowd);
        } else if (getContainer() instanceof ComposedRuleElement) {
          ComposedRuleElement composed = (ComposedRuleElement) getContainer();
          result = composed.fallbackContinue(newDirection, false, annotation, ruleMatch, ruleApply,
                  sideStepContainerMatch, composed, entryPoint, stream, crowd);
        }
      }
    }
    return result;
  }

  protected void doneMatching(RuleMatch ruleMatch, RuleApply ruleApply, RutaStream stream,
          InferenceCrowd crowd) {
    // do not execute actions if they already have been or if this is just a lookahead
    // (ruleApply==null)
    if (!ruleMatch.isApplied() && ruleApply != null) {
      ruleApply.add(ruleMatch, stream);
      if (ruleMatch.matchedCompletely()) {
        RutaRule rule = ruleMatch.getRule();
        rule.getEnvironment().acceptTempVariableValues(rule.getOwnLabels());
        rule.getRoot().applyRuleElements(ruleMatch, stream, crowd);
      }
      ruleMatch.setApplied(true);
    }
  }

  protected void processInlinedActionRules(RuleMatch ruleMatch, RutaStream stream,
          InferenceCrowd crowd) {
    List<List<RuleElementMatch>> matchInfo = ruleMatch.getMatchInfo(this);
    // TODO: which rule element match should be used? all? should context matter?
    if (matchInfo == null || matchInfo.isEmpty() || matchInfo.get(0) == null
            || matchInfo.get(0).isEmpty()) {
      return;
    }
    RuleElementMatch ruleElementMatch = matchInfo.get(0).get(0);
    processInlinedActionRules(ruleMatch, ruleElementMatch, stream, crowd);
  }

  protected List<List<ScriptApply>> processInlinedActionRules(RuleMatch ruleMatch,
          RuleElementMatch ruleElementMatch, RutaStream stream, InferenceCrowd crowd) {
    if (inlinedActionRuleBlocks != null && !inlinedActionRuleBlocks.isEmpty()) {
      List<List<ScriptApply>> inlinedBlocksApplies = processInlinedRules(inlinedActionRuleBlocks,
              ruleMatch, stream, crowd);
      ruleElementMatch.setInlinedActionRules(inlinedBlocksApplies);
      return inlinedBlocksApplies;
    }
    return null;
  }

  protected List<List<ScriptApply>> processInlinedConditionRules(RuleMatch ruleMatch,
          RuleElementMatch ruleElementMatch, RutaStream stream, InferenceCrowd crowd) {
    if (inlinedConditionRuleBlocks != null && !inlinedConditionRuleBlocks.isEmpty()) {
      List<List<ScriptApply>> inlinedBlocksApplies = processInlinedRules(inlinedConditionRuleBlocks,
              ruleMatch, stream, crowd);
      ruleElementMatch.setInlinedConditionRules(inlinedBlocksApplies);
      return inlinedBlocksApplies;
    }
    return null;
  }

  protected List<List<ScriptApply>> processInlinedRules(List<List<RutaStatement>> inlinedRuleBlocks,
          RuleMatch ruleMatch, RutaStream stream, InferenceCrowd crowd) {
    List<List<ScriptApply>> result = new ArrayList<>();

    List<AnnotationFS> matchedAnnotationsOf = ruleMatch.getMatchedAnnotationsOfElement(this);
    for (AnnotationFS annotationFS : matchedAnnotationsOf) {
      RutaStream windowStream = stream.getWindowStream(annotationFS, annotationFS.getType());
      for (List<RutaStatement> inlinedRules : inlinedRuleBlocks) {
        List<ScriptApply> blockResult = new ArrayList<>();
        for (RutaStatement each : inlinedRules) {
          ScriptApply apply = each.apply(windowStream, crowd);
          blockResult.add(apply);
          if (each instanceof RutaRule) {
            // clean up temp variables produced by failed rules
            ((RutaRule) each).clearOwnLabels();
          }
        }
        result.add(blockResult);
      }
    }

    return result;
  }

  @Override
  public void apply(RuleMatch ruleMatch, RutaStream stream, InferenceCrowd crowd) {
    for (AbstractRutaAction action : actions) {
      crowd.beginVisit(action, null);
      action.execute(new MatchContext(this, ruleMatch), stream, crowd);
      crowd.endVisit(action, null);
    }

    processInlinedActionRules(ruleMatch, stream, crowd);
  }

  protected boolean matchInlinedRules(RuleMatch ruleMatch, RuleElementMatch ruleElementMatch,
          RutaStream stream, InferenceCrowd crowd) {

    List<List<ScriptApply>> blockResults = processInlinedConditionRules(ruleMatch, ruleElementMatch,
            stream, crowd);
    if (blockResults == null) {
      return true;
    }

    boolean matched = true;
    for (List<ScriptApply> list : blockResults) {
      matched &= atLeastOneRuleMatched(list);
    }
    return matched;
  }

  private boolean atLeastOneRuleMatched(List<ScriptApply> list) {
    for (ScriptApply scriptApply : list) {
      if (scriptApply instanceof RuleApply) {
        RuleApply ra = (RuleApply) scriptApply;
        if (ra.applied > 0) {
          return true;
        }
      }
    }
    return false;
  }

  protected List<RuleElementMatch> getMatch(RuleMatch ruleMatch,
          ComposedRuleElementMatch containerMatch) {
    List<RuleElementMatch> matchInfo;
    if (containerMatch != null) {
      matchInfo = containerMatch.getInnerMatches().get(this);
    } else {
      matchInfo = ruleMatch.getMatchInfo(this).get(0);
    }
    return matchInfo;
  }

  @Override
  public List<RuleElementMatch> evaluateMatches(List<RuleElementMatch> matches,
          MatchContext context, RutaStream stream) {
    return quantifier.evaluateMatches(matches, context, stream, EMPTY_CROWD);
  }

  @Override
  public List<Integer> getSelfIndexList() {
    if (getContainer() == null) {
      return null;
    }

    return asList(getContainer().getRuleElements().indexOf(this) + 1);
  }

  @Override
  public boolean hasAncestor(boolean after) {
    RuleElementContainer c = getContainer();
    if (c == null) {
      return false;
    }
    RuleElement nextElement = c.getNextElement(after, this);
    if (nextElement != null) {
      return true;
    }
    if (c instanceof ComposedRuleElement) {
      return ((ComposedRuleElement) c).hasAncestor(after);
    }
    return false;
  }

  protected boolean earlyExit(AnnotationFS eachAnchor, RuleApply ruleApply, RutaStream stream) {
    if (stream.isGreedyAnchoring() && ruleApply != null
            && isAlreadyCovered(eachAnchor, ruleApply, stream)) {
      // skip if next matched should not overlap
      return true;
    }
    if (stream.isOnlyOnce() && ruleApply != null && ruleApply.getApplied() > 0) {
      // skip if the rule should only be applied once, on the first successful match
      return true;
    }
    return false;
  }

  private boolean isAlreadyCovered(AnnotationFS eachAnchor, RuleApply ruleApply,
          RutaStream stream) {
    if (eachAnchor == null) {
      return false;
    }

    List<AbstractRuleMatch<? extends AbstractRule>> list = ruleApply.getList();
    Collections.reverse(list);
    for (AbstractRuleMatch<? extends AbstractRule> each : list) {
      if (each instanceof RuleMatch) {
        RuleMatch rm = (RuleMatch) each;
        List<AnnotationFS> matchedAnnotationsOf = Collections.emptyList();
        if (stream.isGreedyRule()) {
          matchedAnnotationsOf = rm.getMatchedAnnotationsOfRoot();
        } else if (stream.isGreedyRuleElement()) {
          matchedAnnotationsOf = rm.getMatchedAnnotationsOfElement(this);
        }
        for (AnnotationFS annotationFS : matchedAnnotationsOf) {
          if (eachAnchor.getBegin() >= annotationFS.getBegin()
                  && eachAnchor.getEnd() <= annotationFS.getEnd()) {
            return true;
          } else if (eachAnchor.getBegin() < annotationFS.getEnd()) {
            // overlapping annotations of composed rule elements with null ruleApply lookahead
            return true;
          }
        }
      }
    }
    return false;
  }

  @Override
  public RuleElementQuantifier getQuantifier() {
    return quantifier;
  }

  @Override
  public RutaBlock getParent() {
    return parent;
  }

  @Override
  public List<AbstractRutaCondition> getConditions() {
    return conditions;
  }

  public void setConditions(List<AbstractRutaCondition> conditions) {
    this.conditions = conditions;
  }

  @Override
  public List<AbstractRutaAction> getActions() {
    return actions;
  }

  public void setActions(List<AbstractRutaAction> actions) {
    this.actions = actions;
  }

  public void setQuantifier(RuleElementQuantifier quantifier) {
    this.quantifier = quantifier;
  }

  @Override
  public RuleElementContainer getContainer() {
    return container;
  }

  @Override
  public void setContainer(RuleElementContainer container) {
    this.container = container;
  }

  @Override
  public void setStartAnchor(boolean start) {
    startAnchor = start;
  }

  @Override
  public boolean isStartAnchor() {
    return startAnchor;
  }

  @Override
  public String getLabel() {
    return label;
  }

  @Override
  public void setLabel(String label) {
    this.label = label;
  }

  @Override
  public void addInlinedConditionRules(List<RutaStatement> innerRules) {
    inlinedConditionRuleBlocks.add(innerRules);
  }

  @Override
  public List<List<RutaStatement>> getInlinedConditionRuleBlocks() {
    return inlinedConditionRuleBlocks;
  }

  @Override
  public void addInlinedActionRules(List<RutaStatement> innerRules) {
    inlinedActionRuleBlocks.add(innerRules);
  }

  @Override
  public List<List<RutaStatement>> getInlinedActionRuleBlocks() {
    return inlinedActionRuleBlocks;
  }

}
