// Copyright (C) 2016 The Android Open Source Project
//
// Licensed 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 com.google.gerrit.mail;

import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Iterators;
import com.google.common.collect.PeekingIterator;
import com.google.gerrit.entities.HumanComment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/** Provides parsing functionality for plaintext email. */
public class TextParser {
  private TextParser() {}

  /**
   * Parses comments from plaintext email.
   *
   * @param email @param email the message as received from the email service
   * @param comments list of {@link HumanComment}s previously persisted on the change that caused
   *     the original notification email to be sent out. Ordering must be the same as in the
   *     outbound email
   * @param changeUrl canonical change url that points to the change on this Gerrit instance.
   *     Example: https://go-review.googlesource.com/#/c/91570
   * @return list of MailComments parsed from the plaintext part of the email
   */
  public static List<MailComment> parse(
      MailMessage email, Collection<HumanComment> comments, String changeUrl) {
    String body = email.textContent();
    // Replace CR-LF by \n
    body = body.replace("\r\n", "\n");

    List<MailComment> parsedComments = new ArrayList<>();

    // Some email clients (like GMail) use >> for enquoting text when there are
    // inline comments that the users typed. These will then be enquoted by a
    // single >. We sanitize this by unifying it into >. Inline comments typed
    // by the user will not be enquoted.
    //
    // Example:
    // Some comment
    // >> Quoted Text
    // >> Quoted Text
    // > A comment typed in the email directly
    String singleQuotePattern = "\n> ";
    String doubleQuotePattern = "\n>> ";
    if (countOccurrences(body, doubleQuotePattern) > countOccurrences(body, singleQuotePattern)) {
      body = body.replace(doubleQuotePattern, singleQuotePattern);
    }

    PeekingIterator<HumanComment> iter = Iterators.peekingIterator(comments.iterator());

    MailComment currentComment = null;
    String lastEncounteredFileName = null;
    HumanComment lastEncounteredComment = null;
    for (String line : Splitter.on('\n').split(body)) {
      if (line.equals(">")) {
        // Skip empty lines
        continue;
      }
      if (line.startsWith("> ")) {
        line = line.substring("> ".length()).trim();
        // This is not a comment, try to advance the file/comment pointers and
        // add previous comment to list if applicable
        if (currentComment != null) {
          if (currentComment.type == MailComment.CommentType.CHANGE_MESSAGE) {
            currentComment.message = ParserUtil.trimQuotation(currentComment.message);
          }
          if (!Strings.isNullOrEmpty(currentComment.message)) {
            ParserUtil.appendOrAddNewComment(currentComment, parsedComments);
          }
          currentComment = null;
        }

        if (!iter.hasNext()) {
          continue;
        }
        HumanComment perspectiveComment = iter.peek();
        if (line.equals(ParserUtil.filePath(changeUrl, perspectiveComment))) {
          if (lastEncounteredFileName == null
              || !lastEncounteredFileName.equals(perspectiveComment.key.filename)) {
            // This is the annotation of a file
            lastEncounteredFileName = perspectiveComment.key.filename;
            lastEncounteredComment = null;
          } else if (perspectiveComment.lineNbr == 0) {
            // This was originally a file-level comment
            lastEncounteredComment = perspectiveComment;
            iter.next();
          }
        } else if (ParserUtil.isCommentUrl(line, changeUrl, perspectiveComment)) {
          lastEncounteredComment = perspectiveComment;
          iter.next();
        }
      } else {
        // This is a comment. Try to append to previous comment if applicable or
        // create a new comment.
        if (currentComment == null) {
          // Start new comment
          currentComment = new MailComment();
          currentComment.message = line;
          if (lastEncounteredComment == null) {
            if (lastEncounteredFileName == null) {
              // Change message
              currentComment.type = MailComment.CommentType.CHANGE_MESSAGE;
            } else {
              // File comment not sent in reply to another comment
              currentComment.type = MailComment.CommentType.FILE_COMMENT;
              currentComment.fileName = lastEncounteredFileName;
            }
          } else {
            // Comment sent in reply to another comment
            currentComment.inReplyTo = lastEncounteredComment;
            currentComment.type = MailComment.CommentType.INLINE_COMMENT;
          }
        } else {
          // Attach to previous comment
          currentComment.message += "\n" + line;
        }
      }
    }
    // There is no need to attach the currentComment after this loop as all
    // emails have footers and other enquoted text after the last comment
    // appeared and the last comment will have already been added to the list
    // at this point.

    return parsedComments;
  }

  /** Counts the occurrences of pattern in s */
  private static int countOccurrences(String s, String pattern) {
    return (s.length() - s.replace(pattern, "").length()) / pattern.length();
  }
}
