/*
 * 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.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.uima.cas.text.AnnotationFS;
import org.apache.uima.ruta.RutaProcessRuntimeException;
import org.apache.uima.ruta.RutaStream;

public class ComposedRuleElementMatch extends RuleElementMatch {

  private Map<RuleElement, List<RuleElementMatch>> innerMatches;

  private boolean textsMatchedUpdated = false;

  public ComposedRuleElementMatch(ComposedRuleElement ruleElement,
          ComposedRuleElementMatch containerMatch) {
    super(ruleElement, containerMatch);
    baseConditionMatched = false;
    ComposedRuleElement cre = ruleElement;
    innerMatches = new TreeMap<RuleElement, List<RuleElementMatch>>(new RuleElementComparator(cre));
    List<RuleElement> ruleElements = cre.getRuleElements();
    for (RuleElement eachRuleElement : ruleElements) {
      innerMatches.put(eachRuleElement, null);
    }
  }

  protected void enforceUpdate() {
    textsMatchedUpdated = false;
    ComposedRuleElementMatch cm = getContainerMatch();
    if (cm != null) {
      cm.enforceUpdate();
    }
  }

  private void setInnerMatches(Map<RuleElement, List<RuleElementMatch>> innerMatches) {
    this.innerMatches = innerMatches;
    enforceUpdate();
  }

  public Map<RuleElement, List<RuleElementMatch>> getInnerMatches() {
    return innerMatches;
  }

  public void addInnerMatch(RuleElement ruleElement, RuleElementMatch ruleElementMatch,
          RutaStream stream) {
    addInnerMatch(ruleElement, ruleElementMatch, true, stream);
  }

  public void addInnerMatch(RuleElement ruleElement, RuleElementMatch ruleElementMatch,
          boolean included, RutaStream stream) {
    List<RuleElementMatch> list = innerMatches.get(ruleElement);
    if (list == null) {
      list = new ArrayList<RuleElementMatch>();
      innerMatches.put(ruleElement, list);
    }
    list.add(ruleElementMatch);

    if (list.size() > stream.getMaxRuleElementMatches()) {
      throw new RutaProcessRuntimeException("Rule element exceeded the allowed amount of matches ("
              + stream.getMaxRuleElementMatches() + "): " + ruleElement.toString());
    }
    evaluateInnerMatches(included, stream);
    enforceUpdate();
  }

  public void evaluateInnerMatches(boolean included, RutaStream stream) {
    boolean allDone = true;
    boolean oneDone = false;
    Set<Entry<RuleElement, List<RuleElementMatch>>> entrySet = innerMatches.entrySet();
    for (Entry<RuleElement, List<RuleElementMatch>> entry : entrySet) {
      RuleElement element = entry.getKey();
      List<RuleElementMatch> value = entry.getValue();
      MatchContext context = new MatchContext(element, null, true);
      allDone &= (element.getQuantifier().isOptional(context, stream) || value != null);
      if (value != null && !value.isEmpty() && included) {
        for (RuleElementMatch ruleElementMatch : value) {
          allDone &= ruleElementMatch.matched();
          oneDone |= ruleElementMatch.matched();
        }
      }
    }
    ComposedRuleElement cre = (ComposedRuleElement) ruleElement;
    if (cre.getConjunct() != null && !cre.getConjunct()) {
      baseConditionMatched = oneDone;
    } else {
      baseConditionMatched = allDone;
    }
  }

  @Override
  public ComposedRuleElementMatch copy() {
    ComposedRuleElementMatch copy = new ComposedRuleElementMatch((ComposedRuleElement) ruleElement,
            containerMatch);
    copy.setBaseConditionMatched(baseConditionMatched);
    copy.setConditions(conditions);
    copy.setConditionsMatched(conditionsMatched);
    copy.setTextsMatched(textsMatched);
    Map<RuleElement, List<RuleElementMatch>> newMap = new TreeMap<RuleElement, List<RuleElementMatch>>(
            new RuleElementComparator((ComposedRuleElement) ruleElement));
    for (Entry<RuleElement, List<RuleElementMatch>> entry : innerMatches.entrySet()) {
      List<RuleElementMatch> value = entry.getValue();
      if (value != null) {
        List<RuleElementMatch> newValue = new ArrayList<RuleElementMatch>();
        for (RuleElementMatch each : value) {
          newValue.add(each.copy());
        }
        newMap.put(entry.getKey(), newValue);
      } else {
        newMap.put(entry.getKey(), null);
      }
    }
    copy.setInnerMatches(newMap);
    return copy;
  }

  public ComposedRuleElementMatch copy(ComposedRuleElementMatch extendedContainerMatch,
          boolean after) {
    ComposedRuleElementMatch copy = new ComposedRuleElementMatch((ComposedRuleElement) ruleElement,
            containerMatch);
    copy.setBaseConditionMatched(baseConditionMatched);
    copy.setConditions(conditions);
    copy.setConditionsMatched(conditionsMatched);
    copy.setTextsMatched(textsMatched);
    Map<RuleElement, List<RuleElementMatch>> newMap = new TreeMap<RuleElement, List<RuleElementMatch>>(
            new RuleElementComparator((ComposedRuleElement) ruleElement));
    for (Entry<RuleElement, List<RuleElementMatch>> entry : innerMatches.entrySet()) {
      RuleElement key = entry.getKey();
      List<RuleElementMatch> value = entry.getValue();
      if (key.equals(extendedContainerMatch.getRuleElement())) {
        // if (value.isEmpty()) {
        extendedContainerMatch.setContainerMatch(copy);
        if (value != null) {
          List<RuleElementMatch> newValue = new ArrayList<RuleElementMatch>();
          newValue.addAll(value);
          newValue.set(newValue.size() - 1, extendedContainerMatch);
          newMap.put(extendedContainerMatch.getRuleElement(), newValue);
        }
        // } else {
        // List<RuleElementMatch> newValue = new ArrayList<RuleElementMatch>();
        // newValue.addAll(value);
        // newValue.add(extendedContainerMatch);
        // newMap.put(extendedContainerMatch.getRuleElement(), newValue);
        // }
      } else {
        if (value != null) {
          List<RuleElementMatch> newValue = new ArrayList<RuleElementMatch>();
          for (RuleElementMatch each : value) {
            each.setContainerMatch(copy);
            if (each instanceof ComposedRuleElementMatch) {
              newValue.add(((ComposedRuleElementMatch) each).copy(extendedContainerMatch, after));
            } else {
              newValue.add(each.copy());
            }
          }
          newMap.put(entry.getKey(), newValue);
        } else {
          newMap.put(entry.getKey(), null);
        }
      }
    }
    copy.setInnerMatches(newMap);
    return copy;
  }

  public ComposedRuleElementMatch copy2(ComposedRuleElementMatch extendedContainerMatch,
          boolean after) {
    ComposedRuleElementMatch copy = new ComposedRuleElementMatch((ComposedRuleElement) ruleElement,
            containerMatch);
    copy.setBaseConditionMatched(baseConditionMatched);
    copy.setConditions(conditions);
    copy.setConditionsMatched(conditionsMatched);
    copy.setTextsMatched(textsMatched);
    Map<RuleElement, List<RuleElementMatch>> newMap = new TreeMap<RuleElement, List<RuleElementMatch>>(
            new RuleElementComparator((ComposedRuleElement) ruleElement));
    for (Entry<RuleElement, List<RuleElementMatch>> entry : innerMatches.entrySet()) {
      RuleElement key = entry.getKey();
      List<RuleElementMatch> value = entry.getValue();
      if (key.equals(extendedContainerMatch.getRuleElement())) {
        extendedContainerMatch.setContainerMatch(copy);
        if (value != null) {
          List<RuleElementMatch> newValue = new ArrayList<RuleElementMatch>();
          newValue.addAll(value);
          if (!newValue.isEmpty()) {
            // there was no match
            newValue.set(newValue.size() - 1, extendedContainerMatch);
          }
          newMap.put(extendedContainerMatch.getRuleElement(), newValue);
        }
      } else {
        if (value != null && !value.isEmpty()) {
          List<RuleElementMatch> newValue = new ArrayList<RuleElementMatch>();
          int counter = 0;
          for (RuleElementMatch each : value) {
            each.setContainerMatch(copy);
            // really need to copy all?
            boolean isCurrentOne = false;
            if (counter == value.size() - 1) {
              isCurrentOne = true;
            }
            // should not be neccessary because empty matches are listed last
            // if (after && counter == value.size() - 1) {
            // isCurrentOne = true;
            // }
            // if (!after && counter == 0) {
            // isCurrentOne = true;
            // }
            if (each instanceof ComposedRuleElementMatch && isCurrentOne) {
              newValue.add(((ComposedRuleElementMatch) each).copy2(extendedContainerMatch, after));
            } else {
              newValue.add(each.copy());
            }
            counter++;
          }
          newMap.put(entry.getKey(), newValue);
        } else {
          newMap.put(entry.getKey(), null);
        }
      }
    }
    copy.setInnerMatches(newMap);
    return copy;
  }

  public void update(ComposedRuleElementMatch extendedContainerMatch) {
    for (Entry<RuleElement, List<RuleElementMatch>> entry : innerMatches.entrySet()) {
      RuleElement key = entry.getKey();
      List<RuleElementMatch> value = entry.getValue();
      if (key.equals(extendedContainerMatch.getRuleElement())) {
        extendedContainerMatch.setContainerMatch(this);
        if (value != null) {
          value.set(value.size() - 1, extendedContainerMatch);
        }
      } else {
        if (value != null) {
          for (RuleElementMatch each : value) {
            if (each instanceof ComposedRuleElementMatch) {
              ((ComposedRuleElementMatch) each).update(extendedContainerMatch);
            }
          }
        }
      }
    }
    enforceUpdate();
  }

  @Override
  public String toString() {
    return "CREM" + innerMatches.toString();
  }

  @Override
  public List<AnnotationFS> getTextsMatched() {
    if (!textsMatchedUpdated || textsMatched == null) {
      Collection<AnnotationFS> set = new TreeSet<AnnotationFS>(new AnnotationComparator());
      Collection<List<RuleElementMatch>> values = innerMatches.values();
      for (List<RuleElementMatch> list : values) {
        if (list != null) {
          for (RuleElementMatch ruleElementMatch : list) {
            set.addAll(ruleElementMatch.getTextsMatched());
          }
        }
      }
      textsMatched = new ArrayList<AnnotationFS>(set);
      textsMatchedUpdated = true;
    }
    return textsMatched;
  }

  public void setConditionInfo(List<EvaluatedCondition> evaluatedConditions) {
    conditions = evaluatedConditions;
    for (EvaluatedCondition each : conditions) {
      conditionsMatched = conditionsMatched && each.isValue();
    }

  }

}
