
// JGrep.java
/* From "Java Cookbook" by Ian Darwin
   Chapter 4. "Pattern Matching with Regular Expressions"
   https://www.oreilly.com/library/view/java-cookbook/0596001703/ch04s14.html
   https://github.com/IanDarwin/javasrc/blob/master/main/src/main/java/regex/Grep2.java
*/
// Modified by Andrew Davison, ad@coe.psu.ac.th, June 2023

/* A simplified version of grep
   Man page: https://man7.org/linux/man-pages/man1/grep.1.html


Usage: JGrep [-chHilnsvV] [--color] PATTERN [FILE...] [COMMAND]
A simple Java version of grep.
      PATTERN                PATTERN is a basic regular expression.
      [FILE...]              File(s) to be searched for PATTERN
  -c, --count                print only a count of matching lines per FILE
      --color, --colour      show PATTERN matches in red
  -h, --help                 Show this help message and exit.
  -H, --no-filename          suppress the prefixing filename on output
  -i, --ignore-case          ignore case distinctions
  -l, --files-with-matches   print only names of FILEs containing matches
  -n, --line-number          print line number with output lines
  -s, --no-messages          suppress error messages
  -v, --invert-match         select non-matching lines
  -V, --version              Print version information and exit.
Commands:
  gen-manpage  Generates man pages for all commands in the specified directory.


  Picocli features:
    * named options and positional parameters
    * POSIX-style grouped short options 
    * automatic help and version options
    * exit codes
    * ANSI colors

   * generation of AsciiDoc documentation using a manpage
     document structure
      - requires picocli-codegen-4.7.4.jar in libs/
      - see ch 27 in manual
        https://picocli.info/#_generate_man_page_documentation


   Usage examples:
    > compile JGrep.java

    > run JGrep -h
    > run JGrep "private" JGrep.java
    > run JGrep --color "private" JGrep.java
    > run JGrep "^public" *.java
    > run JGrep -c "private" JGrep.java
    > run JGrep -Hl "private" JGrep.java
    > run JGrep -l "concurrent" *.java

    > cat JGrep.java | run JGrep -n "private"
    > cat JGrep.java | run JGrep -Hc "private"

    > run JGrep gen-manpage -v JGrep
        -- generates JGrep.adoc AsciiDoc
           (and JGrep-gen-manpage.adoc)
*/


import java.io.*;
import java.util.*;
import java.util.regex.*;
import java.util.concurrent.*;

import picocli.CommandLine;
import picocli.CommandLine.*;

import picocli.codegen.docgen.manpage.ManPageGenerator;


@Command(name = "JGrep", 
         mixinStandardHelpOptions = true,
         version = "JGrep 1.0",
         description = "A simple Java version of grep.",
         subcommands = ManPageGenerator.class)
public class JGrep implements Callable<Integer>
{

  @Parameters(index = "0", // arity = "1",   // required
              paramLabel = "PATTERN", 
              description = "PATTERN is a basic regular expression.")
  private String patternStr; 

  @Parameters(index = "1..*",
              paramLabel = "FILE", 
              description = "File(s) to be searched for PATTERN")
  private List<String> fnms;   // could use List<File>

  @Option(names = { "-c", "--count" }, 
          description = "print only a count of matching lines per FILE")
  private boolean countOnly = false;

  @Option(names = { "--color", "--colour" }, 
          description = "show PATTERN matches in red")
  private boolean useColor = false;

  @Option(names = { "-H", "--no-filename" },   // was "-h"
          description = "suppress the prefixing filename on output")
  private boolean printFileName = true;  // will toggle to false

  @Option(names = { "-i", "--ignore-case" }, 
          description = "ignore case distinctions")
  private boolean ignoreCase = false;

  @Option(names = { "-l", "--files-with-matches" }, 
          description = "print only names of FILEs containing matches")
  private boolean onlyFnms = false;

  @Option(names = { "-n", "--line-number" }, 
          description = "print line number with output lines")
  private boolean numbered = false;

  @Option(names = { "-s", "--no-messages" }, 
          description = "suppress error messages")
  private boolean silent = false;

  @Option(names = { "-v", "--invert-match" }, 
          description = "select non-matching lines")
  private boolean invert = false;

  private Pattern pattern;
  private int numLines, numMatches;
  private boolean foundMatch;


  public Integer call()
  { 
    try { // compile the regex
      if (ignoreCase)
        pattern = Pattern.compile(patternStr, 
                             Pattern.CASE_INSENSITIVE);
      else
        pattern = Pattern.compile(patternStr);
    }
    catch (PatternSyntaxException ex) {
     if (!silent)
        System.err.println("Regex syntax error in \"" + 
                                           patternStr + "\"");
      return 1;
    }

    if (fnms == null)
      applyRegex(new InputStreamReader(System.in), "(stdin)");
    else {
      for (String fnm : fnms) {
        try {
          applyRegex(new FileReader(fnm), fnm);
        }
        catch(Exception e) 
        { if (!silent)
            System.err.println(e);
        }
      }
    }

    return 0;
  } /* call */


  private void applyRegex(Reader ifile, String fnm)
  // scan one file
  {
    // System.out.println("fnm: " + fnm);
    String line;
    Matcher matcher;
    foundMatch = false;

    numLines = 0;
    numMatches = 0;
    try {
      BufferedReader br = new BufferedReader(ifile);
      while ((line = br.readLine()) != null) {
        // System.out.println(line);
        numLines++;
        matcher = pattern.matcher(line);
        if (matcher.find()) {
          if (!invert)
            reportMatch(matcher, fnm, line);
        }
        else if (invert) 
          reportInvert(fnm, line);
      }
      br.close(); // end of file

      if (onlyFnms && foundMatch)
        System.out.println(fnm);
      else if (countOnly) {
        if (printFileName)
          System.out.println(fnm + ": " + numMatches);
        else
          System.out.println(numMatches);
      }
    }
    catch (IOException e) 
    { if (!silent)
        System.err.println(e);
    }
  } /* applyRegex */


  private void reportMatch(Matcher matcher, String fnm, String line)
  /* count every match on a line, and optionally annotate the 
     matches in red */
  {
    foundMatch = true;
    String colorLine = "";
    int currIdx = 0;

    matcher.reset();
    while (matcher.find()) {
      if (countOnly) 
        numMatches++; 
      else if (useColor) {
        int startIdx = matcher.start();
        int endIdx = matcher.end();
        colorLine += line.substring(currIdx, startIdx) +
              "@|red " + line.substring(startIdx, endIdx) + "|@";
        currIdx = endIdx;
      }
    }
    if (useColor)
      colorLine += line.substring(currIdx);

    if (!onlyFnms && !countOnly) {
      if (printFileName)
        System.out.print(fnm + ": ");
      if (numbered)
        System.out.print(numLines + ": ");
      if (useColor)
        printlnAnsi(colorLine);
      else 
        System.out.println(line);
    }
  }  // end of reportMatch()


  private void printlnAnsi(String s)
  {  System.out.println(Help.Ansi.AUTO.string(s));  }


  private void reportInvert(String fnm, String line)
  {
    foundMatch = true;
    if (countOnly) 
      numMatches++;   // doesn't deal with multiple matches per line
    else {
      if (!onlyFnms) {
        if (printFileName)
          System.out.print(fnm + ": ");
        if (numbered)
          System.out.print(numLines + ": ");
        System.out.println(line);   // no use of color
      }
    }
  }  // end of reportInvert()


  // ---------------------------------------------

  public static void main(String[] args)
  {
    int exitCode = new CommandLine(new JGrep()).execute(args);
    System.exit(exitCode);
  }

}  // end of JGrep class
