/*
 * 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 java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.uima.cas.text.AnnotationFS;
import org.apache.uima.ruta.RutaConstants;
import org.apache.uima.ruta.RutaEnvironment;
import org.apache.uima.ruta.RutaStatement;
import org.apache.uima.ruta.RutaStream;
import org.apache.uima.ruta.action.AbstractRutaAction;
import org.apache.uima.ruta.action.MacroAction;
import org.apache.uima.ruta.block.RutaBlock;
import org.apache.uima.ruta.visitor.InferenceCrowd;

public class RutaRule extends AbstractRule {

  private ComposedRuleElement root;

  /**
   * labels of all rule elements including those in inlined rules. The values store the values of
   * overridden variables.
   */
  private Map<String, Object> labels;

  private Collection<String> ownLabels;

  public RutaRule(List<RuleElement> elements, RutaBlock parent, int id) {
    super(parent, id);
    this.root = new ComposedRuleElement(elements, null, null, null, null, parent);
    this.labels = new LinkedHashMap<>();
    this.ownLabels = new ArrayList<>();
  }

  @Override
  public RuleApply apply(RutaStream stream, InferenceCrowd crowd) {
    return apply(stream, crowd, stream.isGreedyAnchoring());
  }

  public RuleApply apply(RutaStream stream, InferenceCrowd crowd, boolean remember) {
    RuleApply ruleApply = new RuleApply(this, remember);
    MatchContext context = new MatchContext(getParent());
    prepareEnvironment(context, stream);
    crowd.beginVisit(this, ruleApply);
    RuleMatch ruleMatch = new RuleMatch(this);
    root.startMatch(ruleMatch, ruleApply, null, null, stream, crowd);
    crowd.endVisit(this, ruleApply);
    cleanupEnvironment(context, stream);
    return ruleApply;
  }

  @Override
  public String toString() {
    return root == null ? "<empty>" : root.toString();
  }

  public final List<RuleElement> getRuleElements() {
    return root.getRuleElements();
  }

  @Override
  public RutaEnvironment getEnvironment() {
    return getParent().getEnvironment();
  }

  public void setRuleElements(List<RuleElement> elements) {
    if (elements != null && elements.size() == 1
            && elements.get(0) instanceof ConjunctRulesRuleElement) {
      root = (ComposedRuleElement) elements.get(0);
    } else {
      root.setRuleElements(elements);
    }
    if (elements != null) {
      // update label map
      for (RuleElement ruleElement : elements) {
        fillLabelMap(ruleElement, true);
      }
    }

  }

  private void fillLabelMap(RuleElement ruleElement, boolean own) {
    if (!StringUtils.isBlank(ruleElement.getLabel())) {
      labels.put(ruleElement.getLabel(), null);
      if (own) {
        ownLabels.add(ruleElement.getLabel());
      }
    }
    fillLabelMapWithActions(ruleElement.getActions(), own, new HashSet<>());
    if (ruleElement instanceof ComposedRuleElement) {
      ComposedRuleElement cre = (ComposedRuleElement) ruleElement;
      List<RuleElement> ruleElements = cre.getRuleElements();
      for (RuleElement each : ruleElements) {
        fillLabelMap(each, own);
      }
    }
    fillLabelMapWithInlinedRules(ruleElement.getInlinedConditionRuleBlocks());
    fillLabelMapWithInlinedRules(ruleElement.getInlinedActionRuleBlocks());
  }

  private void fillLabelMapWithActions(List<AbstractRutaAction> actions, boolean own,
          Set<AbstractRutaAction> processedActions) {
    if (actions != null) {
      for (AbstractRutaAction action : actions) {
        // TODO recursive declarations are not supported right now, but we should check it anyways
        if (processedActions.contains(action)) {
          continue;
        }
        processedActions.add(action);

        if (action != null && !StringUtils.isBlank(action.getLabel())) {
          labels.put(action.getLabel(), null);
          if (own) {
            ownLabels.add(action.getLabel());
          }
        }
        if (action instanceof MacroAction) {
          MacroAction macroAction = (MacroAction) action;
          fillLabelMapWithActions(macroAction.getActions(), own, processedActions);
        }
      }
    }
  }

  private void fillLabelMapWithInlinedRules(List<List<RutaStatement>> blocks) {
    if (blocks != null) {
      for (List<RutaStatement> list : blocks) {
        for (RutaStatement eachInlined : list) {
          if (eachInlined instanceof RutaRule) {
            RutaRule inlinedRule = (RutaRule) eachInlined;
            inlinedRule.setInlined(true);
            fillLabelMap(inlinedRule.getRoot(), false);
          }
        }
      }
    }
  }

  private void prepareEnvironment(MatchContext context, RutaStream stream) {
    if (isInlined()) {
      // only the actual rule may setup the environment
      return;
    }
    RutaBlock parent = context.getParent();
    RutaEnvironment environment = parent.getEnvironment();
    for (Entry<String, Object> entry : labels.entrySet()) {
      String label = entry.getKey();
      if (environment.isVariable(label)) {
        Class<?> variableType = environment.getVariableType(label);
        Class<?> variableGenericType = environment.getVariableGenericType(label);
        if (variableType != null && variableGenericType != null
                && variableType.isAssignableFrom(List.class)
                && variableGenericType.isAssignableFrom(AnnotationFS.class)) {
          labels.put(label, environment.getVariableValue(label, stream));
        } else if (variableType != null && variableType.isAssignableFrom(AnnotationFS.class)) {
        } else {
          String type = variableType == null ? "unknown" : variableType.getSimpleName();
          throw new RuntimeException("Overriding global variable '" + label + "' of type '" + type
                  + "' with a local label variable is not allowed (in script "
                  + context.getParent().getName() + ")!");
        }
      } else {
        environment.addVariable(label, RutaConstants.RUTA_VARIABLE_ANNOTATION_LIST);
      }
    }
  }

  private void cleanupEnvironment(MatchContext context, RutaStream stream) {
    if (isInlined()) {
      // only the actual rule may revert the environment
      return;
    }
    RutaBlock parent = context.getParent();
    RutaEnvironment environment = parent.getEnvironment();
    for (Entry<String, Object> entry : labels.entrySet()) {
      String label = entry.getKey();
      Object value = entry.getValue();
      if (value == null) {
        environment.removeVariable(label);
      } else {
        environment.setVariableValue(label, value);
      }
    }
  }

  public ComposedRuleElement getRoot() {
    return root;
  }

  public Collection<String> getLabels() {
    return labels.keySet();
  }

  public Collection<String> getOwnLabels() {
    return ownLabels;
  }

  public void clearOwnLabels() {
    RutaEnvironment environment = getParent().getEnvironment();
    environment.clearTempVariables(ownLabels);
  }

}
