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

import java.nio.charset.Charset
import org.apache.lucene.gradle.plugins.help.ExecOperationsProvider

// This adds javacc generation support.

configure(rootProject) {
  tasks.register("javacc", {
    description = "Regenerate sources for corresponding javacc grammar files."
    group = "generation"

    dependsOn allprojects.collect { prj -> prj.tasks.withType(JavaCCTask) }
  })
}

def commonCleanups = { FileTree generatedFiles ->
  // This is a minor typo in a comment that nonetheless people have hand-corrected in the past.
  generatedFiles.matching({ include "CharStream.java" }).each {file ->
    modifyFile(file, { text ->
      return text.replace(
          "implemetation",
          "implementation");
    })
  }

  generatedFiles.each {file ->
    modifyFile(file, { text ->
      // Normalize EOLs and tabs (EOLs are a side-effect of modifyFile).
      text = text.replace("\t", "    ");
      text = text.replaceAll("JavaCC - OriginalChecksum=[^*]+", "(filtered)")
      text = text.replace("StringBuffer", "StringBuilder")
      return text
    })
  }

  generatedFiles.matching({ include "*TokenManager.java" }).each { file ->
    modifyFile(file, { text ->
      // Remove redundant imports.
      text = text.replaceAll(
          /(?m)^import .+/,
          "")
      // Add CharStream imports.
      text = text.replaceAll(
          /package (.+)/,
          '''
          package $1
          import org.apache.lucene.queryparser.charstream.CharStream;
          '''.trim())
      // Eliminates redundant cast message.
      text = text.replace(
          "int hiByte = (int)(curChar >> 8);",
          "int hiByte = curChar >> 8;")
      // Access to forbidden APIs.
      text = text.replace(
          "public  java.io.PrintStream debugStream = System.out;",
          "// (debugStream omitted).")
      text = text.replace(
          "public  void setDebugStream(java.io.PrintStream ds) { debugStream = ds; }",
          "// (setDebugStream omitted).")
      // Remove dangling doc comments
      text = text.replace(
          "/** Debug output. */",
          "// Debug output.")
      text = text.replace(
          "/** Set debug output. */",
          "// Set debug output.")
      return text
    })
  }
}

configure(project(":lucene:queryparser")) {
  configurations.create("javacc")

  dependencies {
    add("javacc", deps.javacc)
  }

  task javaccParserClassicInternal(type: JavaCCTask) {
    description = "Regenerate classic query parser from lucene/queryparser/classic/QueryParser.jj"
    group = "generation"

    javaccFile = file('src/java/org/apache/lucene/queryparser/classic/QueryParser.jj')

    afterGenerate << commonCleanups
    afterGenerate << { FileTree generatedFiles ->
      generatedFiles.matching { include "QueryParser.java" }.each { file ->
        modifyFile(file, { text ->
          text = text.replace(
              "public QueryParser(CharStream ",
              "protected QueryParser(CharStream ")
          text = text.replace(
              "public QueryParser(QueryParserTokenManager ",
              "protected QueryParser(QueryParserTokenManager ")
          text = text.replace(
              "new java.util.ArrayList<int[]>",
              "new java.util.ArrayList<>")
          text = text.replace(
              "final private LookaheadSuccess jj_ls =",
              "static final private LookaheadSuccess jj_ls =")
          text = text.replace(
              "public class QueryParser ",
              '@SuppressWarnings({"unused","null"}) public class QueryParser ')
          text = text.replace(
              "final public Query TopLevelQuery(",
              "@Override final public Query TopLevelQuery(")
          text = text.replace(
              "public void ReInit(CharStream ",
              "@Override public void ReInit(CharStream ")
          return text
        })
      }
    }
  }

  task javaccParserSurroundInternal(type: JavaCCTask) {
    description = "Regenerate surround query parser from lucene/queryparser/surround/parser/QueryParser.jj"
    group = "generation"

    javaccFile = file('src/java/org/apache/lucene/queryparser/surround/parser/QueryParser.jj')

    afterGenerate << commonCleanups
    afterGenerate << { FileTree generatedFiles ->
      generatedFiles.matching { include "QueryParser.java" }.each { file ->
        modifyFile(file, { text ->
          text = text.replace(
              "import org.apache.lucene.analysis.TokenStream;",
              "")
          text = text.replace(
              "new java.util.ArrayList<int[]>",
              "new java.util.ArrayList<>")
          text = text.replace(
              "public class QueryParser ",
              '@SuppressWarnings({"unused","null"}) public class QueryParser ')
          return text
        })
      }
    }
  }

  task javaccParserFlexibleInternal(type: JavaCCTask) {
    description = "Regenerate flexible query parser from queryparser/flexible/standard/parser/StandardSyntaxParser.jj"
    group = "generation"

    javaccFile = file('src/java/org/apache/lucene/queryparser/flexible/standard/parser/StandardSyntaxParser.jj')

    afterGenerate << commonCleanups
    afterGenerate << { FileTree generatedFiles ->
      generatedFiles.matching { include "ParseException.java" }.each { file ->
        modifyFile(file, { text ->
          // Modify constructor.
          text = text.replace(
              "class ParseException extends Exception",
              "class ParseException extends QueryNodeParseException")

          // Modify imports.
          text = text.replace(
              "package org.apache.lucene.queryparser.flexible.standard.parser;", '''\
            package org.apache.lucene.queryparser.flexible.standard.parser;

            import org.apache.lucene.queryparser.flexible.core.QueryNodeParseException;
            import org.apache.lucene.queryparser.flexible.core.messages.QueryParserMessages;
            import org.apache.lucene.queryparser.flexible.messages.Message;
            import org.apache.lucene.queryparser.flexible.messages.MessageImpl;
            ''')

          // Modify constructors and code bits
          text = text.replaceAll(
              /(?s)[ ]*public ParseException\(Token currentTokenVal[^}]+[}]/, '''\
            public ParseException(Token currentTokenVal,
              int[][] expectedTokenSequencesVal, String[] tokenImageVal)
            {
              super(new MessageImpl(QueryParserMessages.INVALID_SYNTAX, initialise(
              currentTokenVal, expectedTokenSequencesVal, tokenImageVal)));
              this.currentToken = currentTokenVal;
              this.expectedTokenSequences = expectedTokenSequencesVal;
              this.tokenImage = tokenImageVal;
            }
            ''')

          text = text.replaceAll(
              /(?s)[ ]*public ParseException\(String message\)[^}]+[}]/, '''\
            public ParseException(Message message)
            {
              super(message);
            }
            ''')

          text = text.replaceAll(
              /(?s)[ ]*public ParseException\(\)[^}]+[}]/, '''\
            public ParseException()
            {
              super(new MessageImpl(QueryParserMessages.INVALID_SYNTAX, "Error"));
            }
            ''')
          return text
        })
      }

      generatedFiles.matching { include "StandardSyntaxParser.java" }.each { file ->
        modifyFile(file, { text ->
          // Remove redundant cast
          text = text.replace(
              "new java.util.ArrayList<int[]>",
              "new java.util.ArrayList<>")
          text = text.replace(
              "new ArrayList<QueryNode>()",
              "new ArrayList<>()")
          text = text.replace(
              "Collections.<QueryNode> singletonList",
              "Collections.singletonList")
          text = text.replace(
              "public class StandardSyntaxParser ",
              '@SuppressWarnings({"unused","null"}) public class StandardSyntaxParser ')
          return text
        })
      }
    }
  }

  task javacc() {
    description = "Regenerate query parsers (javacc syntax definitions)."
    group = "generation"

    def extraConfig = [
      andThenTasks: [
        "applyGoogleJavaFormat"
      ],
      mustRunBefore: ["compileJava"]
    ]

    dependsOn wrapWithPersistentChecksums(javaccParserClassicInternal, extraConfig),
        wrapWithPersistentChecksums(javaccParserSurroundInternal, extraConfig),
        wrapWithPersistentChecksums(javaccParserFlexibleInternal, extraConfig)
  }

  regenerate.dependsOn "javacc"
}

// We always regenerate, no need to declare outputs.
class JavaCCTask extends DefaultTask {
  @InputFile
  File javaccFile

  /**
   * Apply closures to all generated files before they're copied back
   * to mainline code.
   */
  // A subtle bug here is that this makes it not an input... should be a list of replacements instead?
  @Internal
  List<Closure<FileTree>> afterGenerate = new ArrayList<>()

  @OutputFiles
  List<File> getGeneratedSources() {
    // Return the list of generated files.
    def baseDir = javaccFile.parentFile
    def baseName = javaccFile.name.replace(".jj", "")

    return [
      project.file("${baseDir}/${baseName}.java"),
      project.file("${baseDir}/${baseName}Constants.java"),
      project.file("${baseDir}/${baseName}TokenManager.java"),
      project.file("${baseDir}/ParseException.java"),
      project.file("${baseDir}/Token.java"),
      project.file("${baseDir}/TokenMgrError.java")
    ]
  }

  JavaCCTask() {
    dependsOn(project.configurations.javacc)
  }

  @TaskAction
  def generate() {
    if (!javaccFile || !javaccFile.exists()) {
      throw new GradleException("Input file does not exist: ${javaccFile}")
    }

    // Run javacc generation into temporary folder so that we know all the generated files
    // and can post-process them easily.
    def tempDir = this.getTemporaryDir()
    tempDir.mkdirs()
    project.delete project.fileTree(tempDir, { include: "**/*.java" })

    def targetDir = javaccFile.parentFile
    logger.lifecycle("Recompiling JavaCC: ${project.rootDir.relativePath(javaccFile)}")

    def output = new ByteArrayOutputStream()
    def execOps = project.objects.newInstance(ExecOperationsProvider).execOperations
    def result = execOps.javaexec({
      classpath {
        project.configurations.javacc
      }

      ignoreExitValue = true
      standardOutput = output
      errorOutput = output

      mainClass = "org.javacc.parser.Main"
      args += [
        "-OUTPUT_DIRECTORY=${tempDir}",
        javaccFile
      ]
    })

    // Unless we request verbose logging, don't emit javacc output.
    if (result.exitValue != 0) {
      throw new GradleException("JavaCC failed to compile ${javaccFile}, here is the compilation output:\n${output}")
    }

    // Make sure we don't have warnings.
    if (output.toString(Charset.defaultCharset()).contains("Warning:")) {
      throw new GradleException("JavaCC emitted warnings for ${javaccFile}, here is the compilation output:\n${output}")
    }

    // Apply any custom modifications.
    def generatedFiles = project.fileTree(tempDir)

    afterGenerate.each {closure ->
      closure.call(generatedFiles)
    }

    // Copy back to mainline sources.
    project.copy {
      from tempDir
      into targetDir

      // We don't need CharStream interface as we redirect to our own.
      exclude "CharStream.java"
    }
  }
}
