/**
  * Indicates if a Copy Trans found match should overwrite the currently stored one based on their
  * states.
  */
 private static boolean shouldOverwrite(HTextFlowTarget currentlyStored, ContentState matchState) {
   if (matchState == ContentState.New) {
     return false;
   } else if (currentlyStored != null) {
     if (currentlyStored.getState().isRejectedOrFuzzy() && matchState.isTranslated()) {
       return true; // If it's fuzzy, replace only with approved ones
     } else if (currentlyStored.getState() == ContentState.Translated && matchState.isApproved()) {
       return true; // If it's Translated and found an Approved one
     } else if (currentlyStored.getState() == ContentState.New) {
       return true; // If it's new, replace always
     } else {
       return false;
     }
   }
   return true;
 }
  /**
   * Indicates if a given text flow should have a match found for a given target locale, or if it is
   * already good enough.
   */
  private boolean shouldFindMatch(
      HTextFlow textFlow, HLocale locale, boolean requireTranslationReview) {
    // TODO getTargets will fill up ehcache for large textflows and locales. Check which one is more
    // efficient
    HTextFlowTarget targetForLocale = textFlow.getTargets().get(locale.getId());
    //        HTextFlowTarget targetForLocale = textFlowTargetDAO.getTextFlowTarget(
    //                textFlow, locale);

    if (targetForLocale == null || targetForLocale.getState() == ContentState.NeedReview) {
      return true;
    } else if (requireTranslationReview && targetForLocale.getState() != ContentState.Approved) {
      return true;
    } else if (!requireTranslationReview && targetForLocale.getState() != ContentState.Translated) {
      return true;
    } else {
      return false;
    }
  }
  // @formatter:on
  public static boolean shouldMerge(
      HTextFlowTarget sourceTft, HTextFlowTarget targetTft, boolean useNewerTranslation) {
    // should NOT merge is source tft is not translated/approved
    if (!sourceTft.getState().isTranslated()) {
      return false;
    }

    // should merge if target is not in translated/approved state
    if (!targetTft.getState().isTranslated()) {
      return true;
    }

    // should NOT merge if both state and contents are the same
    if (sourceTft.getState().equals(targetTft.getState())
        && sourceTft.getContents().equals(targetTft.getContents())) {
      return false;
    }

    // if both in translated state return latest if enabled
    return useNewerTranslation && sourceTft.getLastChanged().after(targetTft.getLastChanged());
  }
  private void mergeTextFlowTarget(HTextFlowTarget sourceTft, HTextFlowTarget targetTft) {
    targetTft.setContents(sourceTft.getContents());
    targetTft.setState(sourceTft.getState());
    targetTft.setLastChanged(sourceTft.getLastChanged());
    targetTft.setLastModifiedBy(sourceTft.getLastModifiedBy());
    targetTft.setTranslator(sourceTft.getTranslator());

    if (sourceTft.getComment() == null) {
      targetTft.setComment(null);
    } else {
      HSimpleComment hComment = targetTft.getComment();
      if (hComment == null) {
        hComment = new HSimpleComment();
        targetTft.setComment(hComment);
      }
      hComment.setComment(sourceTft.getComment().getComment());
    }
    targetTft.setRevisionComment(TranslationUtil.getMergeTranslationMessage(sourceTft));
    targetTft.setSourceType(TranslationSourceType.MERGE_VERSION);
    TranslationUtil.copyEntity(sourceTft, targetTft);
  }
  private void signalCopiedTranslation(
      HTextFlowTarget target, ContentState previousState, Long wordCount) {
    /*
     * Using a direct method call instead of an event because it's easier to
     * read. Since these events are being called synchronously (as opposed
     * to an 'after Transaction' events), there is no big performance gain
     * and makes the code easier to read and navigate.
     */
    // TODO how was this not causing duplicate events?  Is this bypassing TranslationServiceImpl?
    // FIXME other observers may not be notified
    HDocument document = target.getTextFlow().getDocument();

    DocumentLocaleKey key = new DocumentLocaleKey(document.getId(), target.getLocaleId());

    Map<ContentState, Long> contentStates = Maps.newHashMap();
    DocStatsEvent.updateContentStateDeltas(
        contentStates, target.getState(), previousState, wordCount);

    DocStatsEvent docEvent =
        new DocStatsEvent(
            key, document.getProjectIteration().getId(), contentStates, target.getId());

    versionStateCacheImpl.docStatsUpdated(docEvent);
  }
  private Integer mergeTranslations(
      final Long sourceVersionId,
      final Long targetVersionId,
      final int batchStart,
      final int batchLength,
      final boolean useNewerTranslation,
      final List<HLocale> supportedLocales)
      throws Exception {

    final Stopwatch stopwatch = Stopwatch.createUnstarted();
    stopwatch.start();

    List<HTextFlow[]> matches =
        textFlowDAO.getSourceByMatchedContext(
            sourceVersionId, targetVersionId, batchStart, batchLength);

    Multimap<DocumentLocaleKey, TextFlowTargetStateChange> eventMap = HashMultimap.create();

    Map<DocumentLocaleKey, Map<ContentState, Long>> docStatsMap = Maps.newHashMap();

    Map<DocumentLocaleKey, Long> lastUpdatedTargetId = Maps.newHashMap();
    ;

    for (HTextFlow[] results : matches) {
      HTextFlow sourceTf = results[0];
      HTextFlow targetTf = results[1];
      boolean foundChange = false;
      Map<Long, ContentState> localeContentStateMap = Maps.newHashMap();

      for (HLocale hLocale : supportedLocales) {
        HTextFlowTarget sourceTft = sourceTf.getTargets().get(hLocale.getId());
        // only process translated state
        if (sourceTft == null || !sourceTft.getState().isTranslated()) {
          continue;
        }

        HTextFlowTarget targetTft = targetTf.getTargets().get(hLocale.getId());
        if (targetTft == null) {
          targetTft = new HTextFlowTarget(targetTf, hLocale);
          targetTft.setVersionNum(0);
          targetTf.getTargets().put(hLocale.getId(), targetTft);
        }

        if (MergeTranslationsServiceImpl.shouldMerge(sourceTft, targetTft, useNewerTranslation)) {
          foundChange = true;

          ContentState oldState = targetTft.getState();
          localeContentStateMap.put(hLocale.getId(), oldState);
          mergeTextFlowTarget(sourceTft, targetTft);
        }
      }
      if (foundChange) {
        translationStateCacheImpl.clearDocumentStatistics(targetTf.getDocument().getId());
        textFlowDAO.makePersistent(targetTf);
        textFlowDAO.flush();

        for (Map.Entry<Long, ContentState> entry : localeContentStateMap.entrySet()) {
          HTextFlowTarget updatedTarget = targetTf.getTargets().get(entry.getKey());

          DocumentLocaleKey key =
              new DocumentLocaleKey(
                  targetTf.getDocument().getId(), updatedTarget.getLocale().getLocaleId());

          eventMap.put(
              key,
              new TextFlowTargetStateEvent.TextFlowTargetStateChange(
                  targetTf.getId(),
                  updatedTarget.getId(),
                  updatedTarget.getState(),
                  entry.getValue()));

          lastUpdatedTargetId.put(key, updatedTarget.getId());

          Map<ContentState, Long> contentStateDeltas =
              docStatsMap.get(key) == null ? Maps.newHashMap() : docStatsMap.get(key);

          DocStatsEvent.updateContentStateDeltas(
              contentStateDeltas,
              updatedTarget.getState(),
              entry.getValue(),
              targetTf.getWordCount());

          docStatsMap.put(key, contentStateDeltas);
        }
      }
    }
    Long actorId = authenticatedAccount.getPerson().getId();
    for (Map.Entry<DocumentLocaleKey, Collection<TextFlowTargetStateChange>> entry :
        eventMap.asMap().entrySet()) {
      TextFlowTargetStateEvent tftUpdatedEvent =
          new TextFlowTargetStateEvent(
              entry.getKey(), targetVersionId, actorId, ImmutableList.copyOf(entry.getValue()));
      textFlowTargetStateEvent.fire(tftUpdatedEvent);
    }
    for (Map.Entry<DocumentLocaleKey, Map<ContentState, Long>> entry : docStatsMap.entrySet()) {
      DocStatsEvent docEvent =
          new DocStatsEvent(
              entry.getKey(),
              targetVersionId,
              entry.getValue(),
              lastUpdatedTargetId.get(entry.getKey()));
      docStatsEvent.fire(docEvent);
    }
    stopwatch.stop();
    log.info(
        "Complete merge translations of {} in {}",
        matches.size() * supportedLocales.size(),
        stopwatch);
    return matches.size() * supportedLocales.size();
  }
  private void saveCopyTransMatch(
      Long actorId,
      final HTextFlowTarget matchingTarget,
      final HTextFlow originalTf,
      final HCopyTransOptions options,
      final boolean requireTranslationReview) {
    final HProjectIteration matchingTargetProjectIteration =
        matchingTarget.getTextFlow().getDocument().getProjectIteration();
    // lazy evaluation of some conditions
    Supplier<Boolean> contextMatches =
        new Supplier<Boolean>() {
          @Override
          public Boolean get() {
            return originalTf.getResId().equals(matchingTarget.getTextFlow().getResId());
          }
        };
    Supplier<Boolean> projectMatches =
        new Supplier<Boolean>() {
          public Boolean get() {
            return originalTf
                .getDocument()
                .getProjectIteration()
                .getProject()
                .getId()
                .equals(matchingTargetProjectIteration.getProject().getId());
          }
        };
    Supplier<Boolean> docIdMatches =
        new Supplier<Boolean>() {
          @Override
          public Boolean get() {
            return originalTf
                .getDocument()
                .getDocId()
                .equals(matchingTarget.getTextFlow().getDocument().getDocId());
          }
        };
    final ContentState copyState =
        determineContentState(
            contextMatches,
            projectMatches,
            docIdMatches,
            options,
            requireTranslationReview,
            matchingTarget.getState());

    boolean hasValidationError =
        validationTranslations(
            copyState,
            matchingTargetProjectIteration,
            originalTf.getContents(),
            matchingTarget.getContents());

    if (hasValidationError) {
      return;
    }

    HTextFlowTarget hTarget =
        textFlowTargetDAO.getOrCreateTarget(originalTf, matchingTarget.getLocale());
    ContentState prevState = hTarget.getId() == null ? ContentState.New : hTarget.getState();
    if (shouldOverwrite(hTarget, copyState)) {
      // NB we don't touch creationDate
      hTarget.setTextFlowRevision(originalTf.getRevision());
      hTarget.setLastChanged(matchingTarget.getLastChanged());
      hTarget.setLastModifiedBy(matchingTarget.getLastModifiedBy());
      hTarget.setTranslator(matchingTarget.getTranslator());
      // TODO rhbz953734 - will need a new copyTran option for
      // review state
      if (copyState == ContentState.Approved) {
        hTarget.setReviewer(matchingTarget.getReviewer());
      }
      hTarget.setContents(matchingTarget.getContents());
      hTarget.setState(copyState);
      if (matchingTarget.getComment() == null) {
        hTarget.setComment(null);
      } else {
        HSimpleComment hComment = hTarget.getComment();
        if (hComment == null) {
          hComment = new HSimpleComment();
          hTarget.setComment(hComment);
        }
        hComment.setComment(matchingTarget.getComment().getComment());
      }
      hTarget.setRevisionComment(TranslationUtil.getCopyTransMessage(matchingTarget));
      hTarget.setSourceType(TranslationSourceType.COPY_TRANS);

      TranslationUtil.copyEntity(matchingTarget, hTarget);

      // TODO Maybe we should think about registering a Hibernate
      // integrator for these updates
      signalCopiedTranslation(hTarget, prevState, originalTf.getWordCount());
    }
  }