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

import org.apache.uima.cas.CAS;
import org.apache.uima.cas.text.AnnotationFS;
import org.apache.uima.ruta.RutaElement;
import org.apache.uima.ruta.RutaStream;
import org.apache.uima.ruta.ScriptApply;
import org.apache.uima.ruta.engine.RutaEngine;
import org.apache.uima.ruta.type.RutaOptional;

public class RuleMatch extends AbstractRuleMatch<RutaRule> {

  private boolean applied = false;

  private Map<RutaElement, ScriptApply> delegateApply;

  private ComposedRuleElementMatch rootMatch;

  public RuleMatch(RutaRule rule) {
    super(rule);
    delegateApply = new HashMap<>(0);
  }

  @Override
  public boolean matchedCompletely() {
    return matched && rootMatch.matched();
  }

  public List<AnnotationFS> getMatchedAnnotationsOfElement(RuleElement element) {
    return getMatchedAnnotations(element.getSelfIndexList(), element.getContainer());
  }

  public AnnotationFS getLastMatchedAnnotation(MatchContext context, RutaStream stream) {
    RuleElement element = context.getElement();
    AnnotationFS annotation = context.getAnnotation();
    boolean direction = context.getDirection();
    List<AnnotationFS> matchedAnnotations = getMatchedAnnotationsOfElement(element);
    if (matchedAnnotations.isEmpty()) {
      if (element.getQuantifier().isOptional(context, stream)) {
        return annotation;
      } else {
        return null;
      }
    }
    if (direction) {
      AnnotationFS matched = matchedAnnotations.get(matchedAnnotations.size() - 1);
      if (annotation != null && matched.getEnd() < annotation.getEnd()) {
        // multiple matches, but none last time
        return annotation;
      } else {
        return matched;
      }
    } else {
      AnnotationFS matched = matchedAnnotations.get(0);
      if (annotation != null && matched.getBegin() > annotation.getBegin()) {
        // multiple matches, but none last time
        return annotation;
      } else {
        return matched;
      }
    }
  }

  @Override
  public List<AnnotationFS> getMatchedAnnotationsOfRoot() {
    return getMatchedAnnotationsOfElement(getRule().getRoot());
  }

  public List<AnnotationFS> getMatchedAnnotations(List<Integer> indexes,
          RuleElementContainer container) {
    List<AnnotationFS> result = new ArrayList<>();
    indexes = extendIndexes(indexes);
    if (container == null) {
      container = rule.getRoot();
    }

    // TODO refactor this!
    if (indexes == null) {
      List<RuleElement> ruleElements = container.getRuleElements();
      indexes = new ArrayList<>();
      for (RuleElement ruleElement : ruleElements) {
        indexes.add(ruleElements.indexOf(ruleElement) + 1);
      }
    }
    // TODO refactor this
    // this method should not be called by elements that need the actual annotation, e.g, for
    // accessing theirs features.
    // this is a hotfix for that case...
    if (indexes.size() == 1) {
      RuleElement ruleElement = container.getRuleElements().get(indexes.get(0) - 1);
      List<List<RuleElementMatch>> matchInfo = getMatchInfo(ruleElement);
      if (matchInfo.size() == 1) {
        List<RuleElementMatch> list = matchInfo.get(0);
        if (list != null && list.size() == 1) {
          List<AnnotationFS> textsMatched = list.get(0).getTextsMatched();
          if (textsMatched.size() == 1) {
            AnnotationFS firstNormal = getFirstNormal(textsMatched);
            if (firstNormal != null) {
              return textsMatched;
            }
          }
        }
      }
    }

    // TODO rethink the reverse
    List<List<List<RuleElementMatch>>> reverseList = new ArrayList<>();
    for (Integer index : indexes) {
      if (index > container.getRuleElements().size()) {
        continue;
      }
      RuleElement ruleElement = container.getRuleElements().get(index - 1);
      List<List<RuleElementMatch>> matchInfo = getMatchInfo(ruleElement);
      int i = 0;
      for (List<RuleElementMatch> list : matchInfo) {
        if (reverseList.size() <= i) {
          reverseList.add(new ArrayList<List<RuleElementMatch>>());
        }
        List<List<RuleElementMatch>> l = reverseList.get(i);
        l.add(list);
        i++;
      }
    }
    CAS cas = null;
    for (List<List<RuleElementMatch>> list : reverseList) {
      int begin = Integer.MAX_VALUE;
      int end = 0;
      AnnotationFS singleAnnotation = null;
      boolean mergingRequired = list.size() > 1;
      for (List<RuleElementMatch> list2 : list) {
        if (list2 != null) {
          if (list2.size() > 1) {
            mergingRequired = true;
          }
          if (list2.size() == 1 && !mergingRequired) {
            RuleElementMatch ruleElementMatch = list2.get(0);
            List<AnnotationFS> textsMatched = ruleElementMatch.getTextsMatched();
            if (textsMatched != null && textsMatched.size() == 1) {
              singleAnnotation = textsMatched.get(0);
            } else {
              mergingRequired = true;
            }
          }
          if (mergingRequired) {
            for (RuleElementMatch ruleElementMatch : list2) {
              List<AnnotationFS> textsMatched = ruleElementMatch.getTextsMatched();
              if (textsMatched != null && !textsMatched.isEmpty()) {
                AnnotationFS first = getFirstNormal(textsMatched);
                if (first != null) {
                  begin = Math.min(first.getBegin(), begin);
                }
                AnnotationFS last = getLastNormal(textsMatched);
                if (last != null) {
                  end = Math.max(last.getEnd(), end);
                }
                if (cas == null && first != null) {
                  cas = first.getCAS();
                }
              }
            }
          }
        }
      }
      if (singleAnnotation != null && !(singleAnnotation instanceof RutaOptional)) {
        result.add(singleAnnotation);
      } else if (cas != null && end != 0) {
        AnnotationFS annotation = cas.createAnnotation(cas.getAnnotationType(), begin, end);
        result.add(annotation);
      }
    }
    return result;
  }

  private AnnotationFS getFirstNormal(List<AnnotationFS> textsMatched) {
    // hotfix for invisible dummy matches
    int pointer = 0;
    AnnotationFS annotationFS = null;

    if (textsMatched.size() == 0) {
      return null;
    } else if (textsMatched.size() == 1) {
      AnnotationFS fs = textsMatched.get(0);
      if (fs.getType().getName().equals(RutaEngine.OPTIONAL_TYPE)) {
        return null;
      }
    }

    while (pointer < textsMatched.size() && (annotationFS = textsMatched.get(pointer)) != null
            && annotationFS.getType().getName().equals(RutaEngine.OPTIONAL_TYPE)) {
      pointer++;
    }
    if (pointer < textsMatched.size()) {
      return annotationFS;
    }
    return null;
  }

  private AnnotationFS getLastNormal(List<AnnotationFS> textsMatched) {
    // hotfix for invisible dummy matches
    int pointer = textsMatched.size() - 1;
    AnnotationFS annotationFS = null;
    while (pointer >= 0 && (annotationFS = textsMatched.get(pointer)) != null
            && annotationFS.getType().getName().equals(RutaEngine.OPTIONAL_TYPE)) {
      pointer--;
    }
    if (pointer >= 0) {
      return annotationFS;
    }
    return null;
  }

  public static List<Integer> extendIndexes(List<Integer> indexes) {
    if (indexes == null || indexes.size() <= 1) {
      return indexes;
    }
    List<Integer> result = new ArrayList<>();
    int pointer = indexes.get(0);
    for (Integer each : indexes) {
      while (pointer < each - 1) {
        pointer++;
        result.add(pointer);
      }
      result.add(each);
      pointer = each;
    }
    return result;
  }

  @Override
  public String toString() {
    StringBuilder result = new StringBuilder();
    result.append((matched ? "Matched : " : "Not Matched :"));
    result.append(getRootMatch());
    return result.toString();
  }

  public Map<RutaElement, ScriptApply> getDelegateApply() {
    return delegateApply;
  }

  public void addDelegateApply(RutaElement element, ScriptApply scriptApply) {
    delegateApply.put(element, scriptApply);
  }

  public void setMatched(boolean matched) {
    this.matched = matched;
  }

  public RuleMatch copy(ComposedRuleElementMatch extendedContainerMatch, boolean after) {
    RuleMatch copy = new RuleMatch(rule);
    copy.setMatched(matched);
    if (extendedContainerMatch.getContainerMatch() == null) {
      copy.setRootMatch(extendedContainerMatch);
    } else {
      copy.setRootMatch(rootMatch.copy2(extendedContainerMatch, after));
    }

    Map<RutaElement, ScriptApply> newDelegateApply = new HashMap<>(delegateApply);
    copy.setDelegateApply(newDelegateApply);
    return copy;
  }

  public RuleMatch copy() {
    RuleMatch copy = new RuleMatch(rule);
    copy.setMatched(matched);
    copy.setRootMatch(rootMatch.copy());
    Map<RutaElement, ScriptApply> newDelegateApply = new HashMap<>(delegateApply);
    copy.setDelegateApply(newDelegateApply);
    return copy;
  }

  public void update(ComposedRuleElementMatch extendedContainerMatch) {
    if (extendedContainerMatch.getContainerMatch() == null) {
      setRootMatch(extendedContainerMatch);
    } else {
      rootMatch.update(extendedContainerMatch);
    }
  }

  public void setDelegateApply(Map<RutaElement, ScriptApply> delegateApply) {
    this.delegateApply = delegateApply;
  }

  public boolean isApplied() {
    return applied;
  }

  public void setApplied(boolean applied) {
    this.applied = applied;
  }

  public ComposedRuleElementMatch getRootMatch() {
    return rootMatch;
  }

  public void setRootMatch(ComposedRuleElementMatch rootMatch) {
    this.rootMatch = rootMatch;
  }

  public List<List<RuleElementMatch>> getMatchInfo(RuleElement element) {
    return getMatchInfo(rootMatch, element);
  }

  public List<List<RuleElementMatch>> getMatchInfo(RuleElementMatch rootMatch,
          RuleElement element) {
    if (element.equals(rootMatch.getRuleElement())) {
      return asList(asList(rootMatch));
    }

    if (rootMatch instanceof ComposedRuleElementMatch) {
      List<List<RuleElementMatch>> result = new ArrayList<>();
      ComposedRuleElementMatch crem = (ComposedRuleElementMatch) rootMatch;
      Set<Entry<RuleElement, List<RuleElementMatch>>> entrySet = crem.getInnerMatches().entrySet();
      for (Entry<RuleElement, List<RuleElementMatch>> entry : entrySet) {
        List<RuleElementMatch> value = entry.getValue();
        if (element.equals(entry.getKey())) {
          result.add(value);
        } else if (value != null) {
          for (RuleElementMatch eachMatch : value) {
            result.addAll(getMatchInfo(eachMatch, element));
          }
        }
      }
      return result;
    }

    return Collections.emptyList();
  }

  public RuleElementMatch getLastMatch(RuleElement ruleElement, boolean direction) {
    List<List<RuleElementMatch>> matchInfo = getMatchInfo(ruleElement);
    if (matchInfo == null || matchInfo.isEmpty()) {
      return null;
    }
    if (direction) {
      List<RuleElementMatch> list = matchInfo.get(matchInfo.size() - 1);
      if (list != null && !list.isEmpty()) {
        return list.get(list.size() - 1);
      }
    } else {
      List<RuleElementMatch> list = matchInfo.get(0);
      if (list != null && !list.isEmpty()) {
        return list.get(0);
      }
    }
    return null;
  }

}
