
 * (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 org.apache.hop.ui.i18n;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSelectInfo;
import org.apache.commons.vfs2.FileSelector;
import org.apache.hop.core.Const;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.logging.ILogChannel;
import org.apache.hop.core.vfs.HopVfs;
import org.apache.hop.i18n.BaseMessages;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class takes care of crawling through the source code
 *
 * @author matt
 */
public class MessagesSourceCrawler {

  /** The Hop source code */
  private String rootFolder;

  /** The source directories to crawl through */
  private List<String> sourceDirectories;

  /** The bundle store */
  private BundlesStore bundlesStore;

  /** Source folder - package name - all the key occurrences in there */
  private Map<String, Map<String, List<KeyOccurrence>>> sourcePackageOccurrences;

  /** The file names to avoid (base names) */
  private List<String> filesToAvoid;

  private Pattern packagePattern;
  private Pattern importPattern;
  private Pattern stringPkgPattern;
  private List<Pattern> classPkgPatterns;
  private Pattern scanPhrasePattern;
  private Pattern doubleQuotePattern;
  private Pattern i18nStringPattern;

  private ILogChannel log;

  public MessagesSourceCrawler() {
    this.sourceDirectories = new ArrayList<>();
    this.filesToAvoid = new ArrayList<>();
    this.sourcePackageOccurrences = new HashMap<>();

    packagePattern =
        Pattern.compile("^package [a-z\\.0-9A-Z]*;$", Pattern.DOTALL | Pattern.MULTILINE);
    importPattern =
        Pattern.compile("^import [a-z\\._0-9A-Z]*;$", Pattern.DOTALL | Pattern.MULTILINE);
    stringPkgPattern =
        Pattern.compile("private static String PKG.*=.*$", Pattern.DOTALL | Pattern.MULTILINE);
    classPkgPatterns =
        Arrays.asList(
            Pattern.compile(
                "private static Class.*\\sPKG\\s*=.*\\.class;", Pattern.DOTALL | Pattern.MULTILINE),
            Pattern.compile(
                "private static final Class.*\\sPKG\\s*=.*\\.class;",
                Pattern.DOTALL | Pattern.MULTILINE),
            Pattern.compile(
                "public static Class.*\\sPKG\\s*=.*\\.class;", Pattern.DOTALL | Pattern.MULTILINE),
            Pattern.compile(
                "public static final Class.*\\sPKG\\s*=.*\\.class;",
                Pattern.DOTALL | Pattern.MULTILINE));

    scanPhrasePattern =
        Pattern.compile(
            "BaseMessages\\s*.getString\\(\\s*PKG,.*\\);", Pattern.DOTALL | Pattern.MULTILINE);
    doubleQuotePattern = Pattern.compile("\"", Pattern.MULTILINE);
    i18nStringPattern =
        Pattern.compile(
            "\"" + Const.I18N_PREFIX + "[a-z\\.0-9]*:[a-zA-Z0-9\\.]*\"",
            Pattern.DOTALL | Pattern.MULTILINE);
  }

  public MessagesSourceCrawler(ILogChannel log, String rootFolder, BundlesStore bundlesStore)
      throws HopException {
    this();
    this.log = log;
    this.rootFolder = rootFolder;
    this.bundlesStore = bundlesStore;
    this.filesToAvoid = new ArrayList<>();

    // Let's look for all the src/main/java folders in the root folder.
    //
    try {
      Files.walk(Paths.get(rootFolder))
         .filter(
              path ->
                  Files.isDirectory(path)
                      && path.endsWith("src/main/java")
                      &&!path.toString().contains("archive")
                      &&!path.toString().contains("/impl/"))
         .forEach(path -> sourceDirectories.add(path.toAbsolutePath().toFile().getPath()));
      ;
    } catch (IOException e) {
      throw new HopException(
          "Error scanning root folder '" + rootFolder + "' for Java source files (*.java)", e);
    }
  }

  /**
   * Add a key occurrence to the list of occurrences. The list is kept sorted on key and message
   * package. If the key already exists, we increment the number of occurrences.
   *
   * @param occ The key occurrence to add
   */
  public void addKeyOccurrence(KeyOccurrence occ) {

    String sourceFolder = occ.getSourceFolder();
    if (sourceFolder == null) {
      throw new RuntimeException(
          "No source folder found for key: "
              + occ.getKey()
              + " in package "
              + occ.getMessagesPackage());
    }
    String messagesPackage = occ.getMessagesPackage();

    // Do we have a map for the source folders?
    // If not, add one...
    //
    Map<String, List<KeyOccurrence>> packageOccurrences =
        sourcePackageOccurrences.computeIfAbsent(sourceFolder, k -> new HashMap<>());

    // Do we have a map entry for the occurrences list in the source folder?
    // If not, add a list for the messages package
    //
    List<KeyOccurrence> occurrences = packageOccurrences.get(messagesPackage);
    if (occurrences == null) {
      occurrences = new ArrayList<>();
      occurrences.add(occ);
      packageOccurrences.put(messagesPackage, occurrences);
    } else {
      int index = Collections.binarySearch(occurrences, occ);
      if (index < 0) {
        // Add it to the list, keep it sorted...
        //
        occurrences.add(-index - 1, occ);
      }
    }
  }

  public void crawl() throws Exception {

    for (final String sourceDirectory : sourceDirectories) {
      FileObject folder = HopVfs.getFileObject(sourceDirectory);
      FileObject[] javaFiles =
          folder.findFiles(
              new FileSelector() {
                @Override
                public boolean traverseDescendents(FileSelectInfo info) {
                  return true;
                }

                @Override
                public boolean includeFile(FileSelectInfo info) {
                  return info.getFile().getName().getExtension().equals("java");
                }
              });

      for (FileObject javaFile : javaFiles) {

        /** We don't want certain files, there is nothing in there for us. */
        boolean skip = false;
        for (String filename : filesToAvoid) {
          if (javaFile.getName().getBaseName().equals(filename)) {
            skip = true;
          }
        }
        if (skip) {
          continue; // don't process this file.
        }

        // For each of these files we look for keys...
        //
        lookForOccurrencesInFile(sourceDirectory, javaFile);
      }
    }
  }

  /**
   * Look for additional occurrences of keys in the specified file.
   *
   * @param sourceFolder The folder the java file and messages files live in
   * @param javaFile The java source file to examine
   * @