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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class LogConsumer {
  private final List expectedExceptions = new ArrayList();
  private boolean skipLogMsgs = false;
  private boolean infoMsgFlag = false;
  private int eatLines = 0;
  private boolean tmpErrFlag = false;
  private int tmpErrLines = 0;
  private boolean saveFlag = false;
  private int savelinenum = 0;
  private final List testExpectStrs;
  StringBuilder all = null;
  private int lineNumber;
  private String fileName;
  HashMap individalErrorCount = new HashMap();
  private final int repeatLimit;

  private static final Pattern ExpectedExceptionPattern =
      Pattern.compile("<ExpectedException action=(add|remove)>(.*)</ExpectedException>");
  private static final Pattern logPattern =
      Pattern.compile("^\\[(?:fatal|error|warn|info|debug|trace|severe|warning|fine|finer|finest)");
  private static final Pattern blankPattern = Pattern.compile("^\\s*$");
  /**
   * Any messages at these levels will be skipped
   */
  private static final Pattern skipLevelPattern =
      Pattern.compile("^\\[(?:warn|warning|info|debug|trace|fine|finer|finest)");
  private static final Pattern fatalOrErrorPattern = Pattern.compile("^\\[(?:fatal|error|severe)");
  private static final Pattern causedByPattern = Pattern.compile("Caused by");
  private static final Pattern shortErrPattern =
      Pattern.compile("^\\[[^\\]]+\\](.*)$", Pattern.MULTILINE | Pattern.DOTALL);
  private static final Pattern wroteExceptionPattern =
      Pattern.compile("\\[debug.*Wrote exception:");
  private static final Pattern rmiWarnPattern = Pattern.compile(
      "^WARNING: Failed to .*java.rmi.ConnectException: Connection refused to host: .*; nested exception is:");
  private static final Pattern javaLangErrorPattern = Pattern.compile("^java\\.lang\\.\\S+Error$");
  private static final Pattern exceptionPattern = Pattern.compile("Exception:");
  private static final Pattern exceptionPattern2 =
      Pattern.compile("( [\\w\\.]+Exception: (([\\S]+ ){0,6}))");
  private static final Pattern exceptionPattern3 = Pattern.compile("( [\\w\\.]+Exception)$");
  private static final Pattern exceptionPattern4 = Pattern.compile("^([^:]+: (([\\w\"]+ ){0,6}))");
  private static final Pattern misformatedI18nMessagePattern = Pattern.compile("[^\\d]\\{\\d+\\}");
  private static final Pattern rvvBitSetMessagePattern =
      Pattern.compile("RegionVersionVector.+bsv\\d+.+bs=\\{\\d+\\}");
  /** Limit long errors to this many lines */
  private static int ERROR_BUFFER_LIMIT = 128;



  public LogConsumer(boolean skipLogMsgs, List testExpectStrs, String fileName, int repeatLimit) {
    super();
    this.skipLogMsgs = skipLogMsgs;
    this.testExpectStrs = testExpectStrs;
    this.fileName = fileName;
    this.repeatLimit = repeatLimit;
  }

  public StringBuilder consume(CharSequence line) {
    {
      lineNumber++;
      Matcher m = ExpectedExceptionPattern.matcher(line);
      if (m.find()) {
        if (m.group(1).equals("add")) {
          expectedExceptions.add(Pattern.compile(m.group(2)));
        } else {
          // assume add and remove are the only choices
          expectedExceptions.remove(Pattern.compile(m.group(2)));
        }
        return null;
      }
    }
    if (skipLogMsgs) {
      if (infoMsgFlag) {
        if (logPattern.matcher(line).find()) {
          infoMsgFlag = false;
        } else if (blankPattern.matcher(line).matches()) {
          infoMsgFlag = false;
          return null;
        } else {
          return null;
        }
      }
      if (skipLevelPattern.matcher(line).find()) {
        infoMsgFlag = true;
        return null;
      }
    }

    if (eatLines != 0) {
      eatLines--;
      return null;
    } else {
      if (saveFlag || fatalOrErrorPattern.matcher(line).find()) {
        if (!saveFlag) {
          saveFlag = true;
          tmpErrFlag = true;
          if (checkExpectedStrs(line, expectedExceptions)) {
            saveFlag = false;
            tmpErrFlag = false;
            tmpErrLines = 0;
          }
          if (tmpErrFlag) {
            tmpErrLines = 1;
            all = new StringBuilder(line);
            all.append("\n");
            savelinenum = lineNumber;
          }
        } else {
          if (causedByPattern.matcher(line).find()) {
            // This code used to stop appending if a causedBy was seen.
            // But we want the causedBy stack trace to also be included
            // in the suspect StringBuilder.
            // The main thing is we do not want to call checkExpectedStrs
            // with this "caused by" line.
          } else if (checkExpectedStrs(line, expectedExceptions)) {
            // reset the counters and throw it all away if it matches
            // one of the registered expected strings
            tmpErrFlag = false;
            tmpErrLines = 0;
            saveFlag = false;
          }

          // We save all the lines up to the next blank line so we're
          // looking for a blank line here
          if (blankPattern.matcher(line).matches()) {
            // we found a blank line so print the suspect string
            // and reset the savetag flag
            saveFlag = false;
            Matcher m = shortErrPattern.matcher(all.toString());
            if (m.matches()) {
              String shortName = m.group(1);
              Integer i = (Integer) individalErrorCount.get(shortName);
              Integer occurances = new Integer((i == null) ? 1 : i.intValue() + 1);
              individalErrorCount.put(shortName, occurances);
              return enforceErrorLimit(occurances.intValue(), all.toString(),
                  // reader.getLineNumber(),
                  savelinenum, fileName);

            } else {
              // error in determining shortName, wing it
              return enforceErrorLimit(1, all.toString(), lineNumber, fileName);
            }
          }

          // we're still saving lines to append them on to all which contains
          // all the lines we're trying to save
          if (tmpErrFlag) {
            if (tmpErrLines < ERROR_BUFFER_LIMIT) {
              tmpErrLines++;
              all.append(line).append("\n");
            }
            if (tmpErrLines == ERROR_BUFFER_LIMIT) {
              tmpErrLines++; // increment to prevent this line from repeating
              all.append("GrepLogs: ERROR_BUFFER_LIMIT limit reached,")
                  .append(" the error was too long to display completely.\n");
            }

          }
        }
        // unique condition for when bridge server see log exception and
        // logging level is set to fine. Message looks like this:
        // [fine 2005/10/25 17:53:13.586 PDT gemfire2 Server connection from
        // hobbes.gemstone.com:34466-0xf4 nid=0x23e40f1] Server connection from
        // hobbes.gemstone.com:34466: Wrote exception:
        // org.apache.geode.cache.EntryNotFoundException: remote-destroy-key
        // also now handles a JMX WARNING
      } else if (wroteExceptionPattern.matcher(line).find()
          || rmiWarnPattern.matcher(line).find()) {
        // Eat only the single EntryNotFound Exception
        eatLines = 1;
        // if we are here then the line didn't have severe or error in it and
        // didn't meet any special cases that require eating lines
        // Check for other kinds of exceptions. This is by no means inclusive
        // of all types of exceptions that could occur and some ARE missed.
      } else if (exceptionPattern.matcher(line).find() || javaLangErrorPattern.matcher(line).find()
          || (misformatedI18nMessagePattern.matcher(line).find()
              && !(skipLevelPattern.matcher(line).find()
                  && rvvBitSetMessagePattern.matcher(line).find()))) {
        if (!checkExpectedStrs(line, expectedExceptions)) {
          // it's the Exception colon that we want to find
          // along with the next six words and define to shortline
          // shortline is only used for the unique sting to count the
          // number of times an exception match occurs. This is so
          // we can suppress further printing if we hit the limit
          Matcher m2 = exceptionPattern2.matcher(line);
          Matcher m3 = exceptionPattern3.matcher(line);
          Matcher m4 = exceptionPattern4.matcher(line);
          String shortName = null;

          if (m2.find()) {
            shortName = m2.group(1);
          } else if (m3.find()) {
            shortName = m3.group(1);
          } else if (m4.find()) {
            shortName = m4.group(1);
          }
          if (shortName != null) {
            Integer i = (Integer) individalErrorCount.get(shortName);
            Integer occurances = new Integer((i == null) ? 1 : i.intValue() + 1);
            individalErrorCount.put(shortName, occurances);
            return enforceErrorLimit(occurances.intValue(), line + "\n", lineNumber, fileName);
          } else {
            return enforceErrorLimit(1, line + "\n", lineNumber, fileName);
          }
        }
      }
    }

    return null;
  }

  public StringBuilder close() {
    if (saveFlag) {
      // Bug fix for severe that occurs at the end of a log file. Since we
      // collect lines up to a blank line that never happens this prints the
      // collection of in process suspect strings if we close the file and
      // we're still trying to save lines

      saveFlag = false;
      return enforceErrorLimit(1, all.toString(), savelinenum, fileName);
    }
    return null;
  }

  private boolean checkExpectedStrs(CharSequence line, List expectedExceptions) {
    for (int i = 0; i < expectedExceptions.size(); i++) {
      Pattern p = (Pattern) expectedExceptions.get(i);
      if (p.matcher(line).find())
        return true;
    }
    for (int i = 0; i < testExpectStrs.size(); i++) {
      Pattern p = (Pattern) testExpectStrs.get(i);
      if (p.matcher(line).find())
        return true;
    }
    return false;
  }

  private StringBuilder enforceErrorLimit(int hits, String line, int linenum, String filename) {
    if (hits <= repeatLimit) {
      StringBuilder buffer = new StringBuilder();
      buffer.append("-----------------------------------------------------------------------\n")
          .append("Found suspect string in ").append(filename).append(" at line ").append(linenum)
          .append("\n\n").append(line).append("\n");
      return buffer;
    }
    if (hits == repeatLimit) {
      StringBuilder buffer = new StringBuilder();
      buffer.append("\n\nHit occurrence limit of ").append(hits).append(" for this string.\n")
          .append("Further reporting of this type of error will be suppressed.\n");
      return buffer;
    }
    return null;
  }

}
