/*
 * 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.visitor;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.uima.cas.text.AnnotationFS;
import org.apache.uima.fit.util.FSCollectionFactory;
import org.apache.uima.jcas.JCas;
import org.apache.uima.jcas.cas.FSArray;
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.block.BlockApply;
import org.apache.uima.ruta.rule.AbstractRule;
import org.apache.uima.ruta.rule.AbstractRuleMatch;
import org.apache.uima.ruta.rule.ComposedRuleElement;
import org.apache.uima.ruta.rule.ComposedRuleElementMatch;
import org.apache.uima.ruta.rule.EvaluatedCondition;
import org.apache.uima.ruta.rule.RegExpRule;
import org.apache.uima.ruta.rule.RegExpRuleMatch;
import org.apache.uima.ruta.rule.RuleApply;
import org.apache.uima.ruta.rule.RuleElement;
import org.apache.uima.ruta.rule.RuleElementMatch;
import org.apache.uima.ruta.rule.RuleMatch;
import org.apache.uima.ruta.rule.RutaRuleElement;
import org.apache.uima.ruta.type.DebugBlockApply;
import org.apache.uima.ruta.type.DebugEvaluatedCondition;
import org.apache.uima.ruta.type.DebugFailedRuleMatch;
import org.apache.uima.ruta.type.DebugInlinedBlock;
import org.apache.uima.ruta.type.DebugMatchedRuleMatch;
import org.apache.uima.ruta.type.DebugRuleApply;
import org.apache.uima.ruta.type.DebugRuleElementMatch;
import org.apache.uima.ruta.type.DebugRuleElementMatches;
import org.apache.uima.ruta.type.DebugRuleMatch;
import org.apache.uima.ruta.type.DebugScriptApply;
import org.apache.uima.ruta.verbalize.RutaVerbalizer;

public class DebugInfoFactory {

  private RutaVerbalizer verbalizer;

  public DebugInfoFactory(RutaVerbalizer verbalizer) {
    super();
    this.verbalizer = verbalizer;
  }

  public DebugBlockApply createDummyBlockApply(RuleMatch ruleMatch, RutaStream stream,
          boolean addToIndex, boolean withMatches, Map<RutaElement, Long> timeInfo) {
    JCas cas = stream.getJCas();
    DebugBlockApply dba = new DebugBlockApply(cas);
    AnnotationFS matchedAnnotation = ruleMatch
            .getMatchedAnnotationsOfElement(ruleMatch.getRule().getRoot()).get(0);
    dba.setElement(matchedAnnotation.getCoveredText());
    dba.setBegin(matchedAnnotation.getBegin());
    dba.setEnd(matchedAnnotation.getEnd());
    return dba;
  }

  public DebugBlockApply createDebugBlockApply(BlockApply blockApply, RutaStream stream,
          boolean addToIndex, boolean withMatches, Map<RutaElement, Long> timeInfo) {
    JCas cas = stream.getJCas();
    DebugBlockApply dba = new DebugBlockApply(cas);
    List<DebugScriptApply> innerApply = new ArrayList<DebugScriptApply>();
    // TODO refactor and remove counting hotfix
    int applied = blockApply.getRuleApply().getApplied();
    RutaElement element = blockApply.getElement();
    String verbalize = "";
    verbalize = verbalizer.verbalize(element);
    if (applied > 1) {
      List<ScriptApply> innerApplies = blockApply.getInnerApplies();
      List<List<ScriptApply>> loops = new ArrayList<List<ScriptApply>>();
      for (int i = 0; i < applied; i++) {
        loops.add(new ArrayList<ScriptApply>());
      }
      int counter = 0;
      int size = innerApplies.size();
      int parts = size / applied;
      for (ScriptApply each : innerApplies) {
        int listIndex = Math.max(0, counter / parts);
        List<ScriptApply> list = loops.get(listIndex);
        list.add(each);
        counter++;
      }
      counter = 0;
      for (List<ScriptApply> list : loops) {
        AbstractRuleMatch<? extends AbstractRule> ruleMatch = blockApply.getRuleApply().getList()
                .get(counter);
        DebugBlockApply dummyBlockApply = createDummyBlockApply((RuleMatch) ruleMatch, stream,
                addToIndex, withMatches, timeInfo);
        List<DebugRuleMatch> ruleMatches = new ArrayList<DebugRuleMatch>();
        ruleMatches.add(createDebugRuleMatch(ruleMatch, stream, addToIndex, withMatches, timeInfo));
        dummyBlockApply.setApplied(1);
        dummyBlockApply.setTried(1);
        dummyBlockApply.setRules(FSCollectionFactory.createFSArray(cas, ruleMatches));
        List<DebugScriptApply> innerInnerApply = new ArrayList<DebugScriptApply>();
        for (ScriptApply each : list) {
          DebugScriptApply eachInnerInner = createDebugScriptApply(each, stream, addToIndex,
                  withMatches, timeInfo);
          innerInnerApply.add(eachInnerInner);
        }
        dummyBlockApply.setInnerApply(FSCollectionFactory.createFSArray(cas, innerInnerApply));
        innerApply.add(dummyBlockApply);
        counter++;
      }
      dba.setInnerApply(FSCollectionFactory.createFSArray(cas, innerApply));
      dba.setElement(verbalize);
      DebugRuleApply ruleApply = createDebugRuleApply(blockApply.getRuleApply(), stream, addToIndex,
              withMatches, timeInfo);
      dba.setApplied(ruleApply.getApplied());
      dba.setTried(ruleApply.getTried());
      dba.setRules(ruleApply.getRules());
      dba.setBegin(ruleApply.getBegin());
      dba.setEnd(ruleApply.getEnd());
      if (timeInfo != null) {
        long time = timeInfo.get(element);
        dba.setTime(time);
      }
      if (addToIndex)
        dba.addToIndexes();
      return dba;
    } else {

      for (ScriptApply each : blockApply.getInnerApplies()) {
        innerApply.add(createDebugScriptApply(each, stream, addToIndex, withMatches, timeInfo));
      }
      dba.setInnerApply(FSCollectionFactory.createFSArray(cas, innerApply));
      dba.setElement(verbalize);
      DebugRuleApply ruleApply = createDebugRuleApply(blockApply.getRuleApply(), stream, addToIndex,
              withMatches, timeInfo);
      dba.setApplied(ruleApply.getApplied());
      dba.setTried(ruleApply.getTried());
      dba.setRules(ruleApply.getRules());
      dba.setBegin(ruleApply.getBegin());
      dba.setEnd(ruleApply.getEnd());
      if (timeInfo != null) {
        Long time = timeInfo.get(element);
        if (time != null) {
          dba.setTime(time);
        }
      }
      if (addToIndex)
        dba.addToIndexes();
      return dba;
    }
  }

  public DebugRuleApply createDebugRuleApply(RuleApply ruleApply, RutaStream stream,
          boolean addToIndex, boolean withMatches, Map<RutaElement, Long> timeInfo) {
    JCas cas = stream.getJCas();
    DebugRuleApply dra = new DebugRuleApply(cas);
    List<DebugRuleMatch> ruleMatches = new ArrayList<DebugRuleMatch>();
    int begin = Integer.MAX_VALUE;
    int end = 0;
    if (withMatches) {
      for (AbstractRuleMatch<? extends AbstractRule> match : ruleApply.getList()) {
        DebugRuleMatch debugRuleMatch = createDebugRuleMatch(match, stream, addToIndex, withMatches,
                timeInfo);
        begin = Math.min(begin, debugRuleMatch.getBegin());
        end = Math.max(end, debugRuleMatch.getEnd());
        ruleMatches.add(debugRuleMatch);
      }
    }
    if (begin >= end) {
      begin = end;
    }
    dra.setRules(FSCollectionFactory.createFSArray(cas, ruleMatches));
    RutaElement element = ruleApply.getElement();
    String namespace = "";
    if (element instanceof RutaStatement) {
      RutaStatement rs = (RutaStatement) element;
      namespace = rs.getParent().getScript().getRootBlock().getNamespace();
    }
    dra.setElement(verbalizer.verbalize(element));
    dra.setApplied(ruleApply.getApplied());
    dra.setTried(ruleApply.getTried());
    dra.setId(((AbstractRule) element).getId());
    dra.setScript(namespace);
    dra.setBegin(begin);
    dra.setEnd(end);
    if (timeInfo != null) {
      Long time = timeInfo.get(element);
      if (time != null) {
        dra.setTime(time);
      }
    }
    if (addToIndex)
      dra.addToIndexes();
    return dra;
  }

  public DebugRuleMatch createDebugRuleMatch(AbstractRuleMatch<? extends AbstractRule> match,
          RutaStream stream, boolean addToIndex, boolean withMatches,
          Map<RutaElement, Long> timeInfo) {
    JCas cas = stream.getJCas();
    DebugRuleMatch drm = null;
    if (match.matchedCompletely()) {
      drm = new DebugMatchedRuleMatch(cas);
    } else {
      drm = new DebugFailedRuleMatch(cas);
    }
    drm.setMatched(match.matchedCompletely());
    if (match instanceof RuleMatch) {
      ComposedRuleElementMatch rootMatch = ((RuleMatch) match).getRootMatch();
      setInnerMatches(stream, addToIndex, withMatches, timeInfo, drm, rootMatch);
      // if (match.matched()) {
      List<DebugScriptApply> delegates = new ArrayList<DebugScriptApply>();
      for (ScriptApply rem : ((RuleMatch) match).getDelegateApply().values()) {
        delegates.add(createDebugScriptApply(rem, stream, addToIndex, withMatches, timeInfo));
      }
      drm.setDelegates(FSCollectionFactory.createFSArray(cas, delegates));
      // }
    } else if (match instanceof RegExpRuleMatch) {
      RegExpRuleMatch rerm = (RegExpRuleMatch) match;
      Map<Integer, List<AnnotationFS>> map = rerm.getMap();
      Set<Entry<Integer, List<AnnotationFS>>> entrySet = map.entrySet();
      List<DebugRuleElementMatches> ruleElementMatches = new ArrayList<DebugRuleElementMatches>();
      for (Entry<Integer, List<AnnotationFS>> entry : entrySet) {
        DebugRuleElementMatches drems = new DebugRuleElementMatches(cas);
        RegExpRule rule = rerm.getRule();
        Integer key = entry.getKey();
        List<AnnotationFS> value = entry.getValue();
        drems.setElement(verbalizer.verbalize(rule));
        List<DebugRuleElementMatch> remList = new ArrayList<DebugRuleElementMatch>();
        if (value != null) {
          for (AnnotationFS each : value) {
            DebugRuleElementMatch drem = new DebugRuleElementMatch(cas);

            DebugEvaluatedCondition base = new DebugEvaluatedCondition(cas);
            base.setValue(true);
            String baseString = "Group " + key;
            base.setElement(baseString);
            drem.setBaseCondition(base);
            if (each.getBegin() <= each.getEnd()) {
              drem.setBegin(each.getBegin());
              drem.setEnd(each.getEnd());
            }
            if (addToIndex) {
              drem.addToIndexes();
            }
            remList.add(drem);
          }
        }
        drems.setMatches(FSCollectionFactory.createFSArray(cas, remList));
        if (addToIndex) {
          drems.addToIndexes();
        }
        ruleElementMatches.add(drems);
      }

      drm.setElements(FSCollectionFactory.createFSArray(cas, ruleElementMatches));
    }
    if (timeInfo != null) {
      long time = timeInfo.get(match.getRule());
      drm.setTime(time);
    }
    List<AnnotationFS> matchedAnnotationsOfRoot = match.getMatchedAnnotationsOfRoot();
    if (!matchedAnnotationsOfRoot.isEmpty()) {
      AnnotationFS matchedAnnotation = matchedAnnotationsOfRoot.get(0);
      if (matchedAnnotation != null) {
        drm.setBegin(matchedAnnotation.getBegin());
        drm.setEnd(matchedAnnotation.getEnd());
        if (addToIndex || withMatches)
          drm.addToIndexes();
      }
    }
    return drm;
  }

  private void setInnerMatches(RutaStream stream, boolean addToIndex, boolean withMatches,
          Map<RutaElement, Long> timeInfo, DebugRuleMatch drm, ComposedRuleElementMatch rootMatch) {
    Set<Entry<RuleElement, List<RuleElementMatch>>> entrySet = rootMatch.getInnerMatches()
            .entrySet();
    List<DebugRuleElementMatches> ruleElementMatches = new ArrayList<DebugRuleElementMatches>();
    for (Entry<RuleElement, List<RuleElementMatch>> entry : entrySet) {
      RuleElement re = entry.getKey();
      List<RuleElementMatch> rems = entry.getValue();
      ruleElementMatches.add(
              createDebugRuleElementMatches(re, rems, stream, addToIndex, withMatches, timeInfo));
    }

    drm.setElements(FSCollectionFactory.createFSArray(stream.getJCas(), ruleElementMatches));
  }

  private void setInnerMatches(RutaStream stream, boolean addToIndex, boolean withMatches,
          Map<RutaElement, Long> timeInfo, DebugRuleElementMatch drm,
          ComposedRuleElementMatch rootMatch) {
    Set<Entry<RuleElement, List<RuleElementMatch>>> entrySet = rootMatch.getInnerMatches()
            .entrySet();
    List<DebugRuleElementMatches> ruleElementMatches = new ArrayList<DebugRuleElementMatches>();
    for (Entry<RuleElement, List<RuleElementMatch>> entry : entrySet) {
      RuleElement re = entry.getKey();
      List<RuleElementMatch> rems = entry.getValue();
      ruleElementMatches.add(
              createDebugRuleElementMatches(re, rems, stream, addToIndex, withMatches, timeInfo));
    }
    drm.setElements(FSCollectionFactory.createFSArray(stream.getJCas(), ruleElementMatches));
  }

  public DebugRuleElementMatches createDebugRuleElementMatches(RuleElement re,
          List<RuleElementMatch> rems, RutaStream stream, boolean addToIndex, boolean withMatches,
          Map<RutaElement, Long> timeInfo) {
    JCas cas = stream.getJCas();
    DebugRuleElementMatches drems = new DebugRuleElementMatches(cas);
    drems.setElement(verbalizer.verbalize(re));
    List<DebugRuleElementMatch> remList = new ArrayList<DebugRuleElementMatch>();
    if (rems != null) {
      for (RuleElementMatch each : rems) {
        DebugRuleElementMatch rem = null;
        if (each instanceof ComposedRuleElementMatch) {
          rem = createDebugComposedRuleElementMatch((ComposedRuleElementMatch) each, stream,
                  addToIndex, withMatches, timeInfo);
        } else {
          rem = createDebugRuleElementMatch(each, stream, addToIndex);
        }
        FSArray<DebugInlinedBlock> inlinedConditionBlocks = createInlinedBlocks(
                each.getInlinedConditionRules(), stream, true, addToIndex, withMatches, timeInfo);
        rem.setInlinedConditionBlocks(inlinedConditionBlocks);
        if (rem != null) {
          remList.add(rem);
        }
      }
    }
    if (rems != null && !rems.isEmpty()) {
      drems.setRuleAnchor(rems.get(0).isRuleAnchor());
    }
    drems.setMatches(FSCollectionFactory.createFSArray(cas, remList));

    FSArray<DebugInlinedBlock> inlinedActionBlocks = createInlinedActionBlocks(rems, stream,
            addToIndex, withMatches, timeInfo);
    drems.setInlinedActionBlocks(inlinedActionBlocks);

    if (addToIndex)
      drems.addToIndexes();
    return drems;
  }

  private FSArray<DebugInlinedBlock> createInlinedBlocks(List<List<ScriptApply>> blocks,
          RutaStream stream, boolean asCondition, boolean addToIndex, boolean withMatches,
          Map<RutaElement, Long> timeInfo) {
    JCas jcas = stream.getJCas();
    if (blocks == null || blocks.isEmpty()) {
      return null;
    }

    List<DebugInlinedBlock> blockList = new ArrayList<>();
    for (List<ScriptApply> block : blocks) {
      List<DebugScriptApply> list = new ArrayList<>();
      boolean oneRuleApplied = false;
      for (ScriptApply ruleApply : block) {
        if (ruleApply instanceof RuleApply) {
          if (((RuleApply) ruleApply).getApplied() > 0) {
            oneRuleApplied = true;
          }
        }
        DebugScriptApply debugScriptApply = createDebugScriptApply(ruleApply, stream, addToIndex,
                withMatches, timeInfo);
        list.add(debugScriptApply);
      }
      DebugInlinedBlock debugInlinedBlock = new DebugInlinedBlock(jcas);
      debugInlinedBlock.setInlinedRules(FSCollectionFactory.createFSArray(jcas, list));
      debugInlinedBlock.setAsCondition(asCondition);
      if (asCondition) {
        debugInlinedBlock.setElement(verbalizer.verbalizeInlinedConditionRuleBlock(block));
        debugInlinedBlock.setMatched(oneRuleApplied);
      } else {
        debugInlinedBlock.setElement(verbalizer.verbalizeInlinedActionRuleBlock(block));
      }
      blockList.add(debugInlinedBlock);
    }
    return FSCollectionFactory.createFSArray(jcas, blockList);
  }

  private FSArray<DebugInlinedBlock> createInlinedActionBlocks(List<RuleElementMatch> rems,
          RutaStream stream, boolean addToIndex, boolean withMatches,
          Map<RutaElement, Long> timeInfo) {
    if (rems == null || rems.isEmpty()) {
      return null;
    }

    return createInlinedBlocks(rems.get(0).getInlinedActionRules(), stream, false, addToIndex,
            withMatches, timeInfo);
  }

  public DebugRuleElementMatch createDebugComposedRuleElementMatch(ComposedRuleElementMatch rem,
          RutaStream stream, boolean addToIndex, boolean withMatches,
          Map<RutaElement, Long> timeInfo) {
    JCas cas = stream.getJCas();
    DebugRuleElementMatch drem = new DebugRuleElementMatch(cas);

    DebugEvaluatedCondition base = new DebugEvaluatedCondition(cas);
    base.setValue(rem.isBaseConditionMatched());

    setInnerMatches(stream, addToIndex, withMatches, timeInfo, drem, rem);

    String baseString = verbalizer.verbalize(rem.getRuleElement());
    base.setElement(baseString);
    drem.setBaseCondition(base);
    drem.setConditions(createEvaluatedConditions(rem, stream, addToIndex));
    List<AnnotationFS> annotations = rem.getTextsMatched();
    if (!annotations.isEmpty()) {
      int begin = annotations.get(0).getBegin();
      int end = annotations.get(annotations.size() - 1).getEnd();
      if (begin <= end) {
        drem.setBegin(begin);
        drem.setEnd(end);
      }
    }
    if (addToIndex)
      drem.addToIndexes();
    return drem;
  }

  public DebugRuleElementMatch createDebugRuleElementMatch(RuleElementMatch rem, RutaStream stream,
          boolean addToIndex) {
    JCas cas = stream.getJCas();
    DebugRuleElementMatch drem = new DebugRuleElementMatch(cas);

    DebugEvaluatedCondition base = new DebugEvaluatedCondition(cas);
    base.setValue(rem.isBaseConditionMatched());
    RuleElement ruleElement = rem.getRuleElement();
    String baseString = "";
    if (ruleElement instanceof RutaRuleElement) {
      baseString = verbalizer.verbalizeMatcher((RutaRuleElement) ruleElement);
    } else if (ruleElement instanceof ComposedRuleElement) {
      baseString = verbalizer.verbalizeComposed((ComposedRuleElement) ruleElement);
    }
    base.setElement(baseString);
    drem.setBaseCondition(base);

    drem.setConditions(createEvaluatedConditions(rem, stream, addToIndex));
    List<AnnotationFS> annotations = rem.getTextsMatched();
    if (!annotations.isEmpty()) {
      int begin = annotations.get(0).getBegin();
      int end = annotations.get(annotations.size() - 1).getEnd();
      if (begin <= end) {
        drem.setBegin(begin);
        drem.setEnd(end);
      }
    }

    if (addToIndex) {
      drem.addToIndexes();
    }
    return drem;
  }

  private FSArray<DebugEvaluatedCondition> createEvaluatedConditions(RuleElementMatch rem,
          RutaStream stream, boolean addToIndex) {
    JCas cas = stream.getJCas();
    List<DebugEvaluatedCondition> ecs = new ArrayList<DebugEvaluatedCondition>();
    if (rem.getConditions() != null) {
      for (EvaluatedCondition each : rem.getConditions()) {
        DebugEvaluatedCondition ec = new DebugEvaluatedCondition(cas);
        ec.setValue(each.isValue());
        ec.setElement(verbalizer.verbalize(each.getCondition()));
        ec.setConditions(createEvaluatedConditions(each, stream, addToIndex));
        ecs.add(ec);
      }
    }
    FSArray<DebugEvaluatedCondition> result = FSCollectionFactory.createFSArray(cas, ecs);
    return result;
  }

  private FSArray<DebugEvaluatedCondition> createEvaluatedConditions(EvaluatedCondition eval,
          RutaStream stream, boolean addToIndex) {
    JCas cas = stream.getJCas();
    List<DebugEvaluatedCondition> ecs = new ArrayList<DebugEvaluatedCondition>();
    for (EvaluatedCondition each : eval.getConditions()) {
      DebugEvaluatedCondition ec = new DebugEvaluatedCondition(cas);
      ec.setValue(each.isValue());
      ec.setElement(verbalizer.verbalize(each.getCondition()));
      ec.setConditions(createEvaluatedConditions(each, stream, addToIndex));
      ecs.add(ec);
    }
    FSArray<DebugEvaluatedCondition> result = FSCollectionFactory.createFSArray(cas, ecs);
    return result;
  }

  public DebugScriptApply createDebugScriptApply(ScriptApply apply, RutaStream stream,
          boolean addToIndex, boolean withMatches, Map<RutaElement, Long> timeInfo) {
    DebugScriptApply debug = null;
    if (apply instanceof BlockApply) {
      debug = createDebugBlockApply((BlockApply) apply, stream, addToIndex, withMatches, timeInfo);
    } else if (apply instanceof RuleApply) {
      debug = createDebugRuleApply((RuleApply) apply, stream, addToIndex, withMatches, timeInfo);
    }
    if (addToIndex)
      debug.addToIndexes();
    return debug;
  }

}
