/*
 * Copyright 2009 Google Inc.
 *
 * 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.template.soy;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.template.soy.SoyFileSet.Builder;
import com.google.template.soy.base.internal.SoyFileKind;
import com.google.template.soy.error.SoyCompilationException;

import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.OptionDef;
import org.kohsuke.args4j.spi.OptionHandler;
import org.kohsuke.args4j.spi.Parameters;
import org.kohsuke.args4j.spi.Setter;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

/**
 * Utilities for classes with a {@code main()} method.
 *
 */
final class MainClassUtils {

  /**
   * Represents a top-level entry point into the Soy codebase.
   * Used by {@link #run} to catch unexpected exceptions and print errors.
   */
  interface Main {
    void main() throws IOException, SoyCompilationException;
  }

  private MainClassUtils() {}

  // NOTE: all the OptionHandler types need to be public with public constructors so args4j can use
  // them.

  /**
   * OptionHandler for args4j that handles a boolean.
   *
   * <p> The difference between this handler and the default boolean option handler supplied by
   * args4j is that the default one doesn't take any param, so can only be used to turn on boolean
   * flags, but never to turn them off. This implementation allows an optional param value
   * true/false/1/0 so that the user can turn on or off the flag.
   */
  public static final class BooleanOptionHandler extends OptionHandler<Boolean> {

    /** {@link OptionHandler#OptionHandler(CmdLineParser,OptionDef,Setter)} */
    public BooleanOptionHandler(
        CmdLineParser parser, OptionDef option, Setter<? super Boolean> setter) {
      super(parser, option, setter);
    }

    @Override public int parseArguments(Parameters params) throws CmdLineException {

      boolean value;
      boolean hasParam;
      try {
        String nextArg = params.getParameter(0);
        if (nextArg.equalsIgnoreCase("true") || nextArg.equals("1")) {
          value = true;
          hasParam = true;
        } else if (nextArg.equalsIgnoreCase("false") || nextArg.equals("0")) {
          value = false;
          hasParam = true;
        } else {
          // Next arg is not a param for this flag. No param means set flag to true.
          value = true;
          hasParam = false;
        }
      } catch (CmdLineException e) {
        // No additional args on command line. No param means set flag to true.
        value = true;
        hasParam = false;
      }

      setter.addValue(value);
      return hasParam ? 1 : 0;
    }

    @Override public String getDefaultMetaVariable() {
      return null;
    }
  }


  /**
   * OptionHandler for args4j that handles a comma-delimited list.
   */
  abstract static class ListOptionHandler<T> extends OptionHandler<T> {

    /** {@link OptionHandler#OptionHandler(CmdLineParser,OptionDef,Setter)} */
    ListOptionHandler(CmdLineParser parser, OptionDef option, Setter<? super T> setter) {
      super(parser, option, setter);
    }

    /**
     * Parses one item from the list into the appropriate type.
     * @param item One item from the list.
     * @return The object representation of the item.
     */
    abstract T parseItem(String item);

    @Override public int parseArguments(Parameters params) throws CmdLineException {
      String parameter = params.getParameter(0);
      // An empty string should be an empty list, not a list containing the empty item
      if (!parameter.isEmpty()) {
        for (String item : parameter.split(",")) {
          setter.addValue(parseItem(item));
        }
      }
      return 1;
    }

    @Override public String getDefaultMetaVariable() {
      return "ITEM,ITEM,...";
    }
  }


  /**
   * OptionHandler for args4j that handles a comma-delimited list of strings.
   */
  public static final class StringListOptionHandler extends ListOptionHandler<String> {

    /** {@link ListOptionHandler#ListOptionHandler(CmdLineParser,OptionDef,Setter)} */
    public StringListOptionHandler(
        CmdLineParser parser, OptionDef option, Setter<? super String> setter) {
      super(parser, option, setter);
    }

    @Override String parseItem(String item) {
      return item;
    }
  }

  /** OptionHandler for args4j that handles a comma-delimited list of guice modules. */
  public static final class ModuleListOptionHandler extends ListOptionHandler<Module> {

    /** {@link ListOptionHandler#ListOptionHandler(CmdLineParser,OptionDef,Setter)} */
    public ModuleListOptionHandler(
        CmdLineParser parser, OptionDef option, Setter<? super Module> setter) {
      super(parser, option, setter);
    }

    @Override
    Module parseItem(String item) {
      return instantiatePluginModule(item);
    }
  }
  /** OptionHandler for args4j that handles a comma-delimited list of files. */
  public static final class FileListOptionHandler extends ListOptionHandler<File> {

    /** {@link ListOptionHandler#ListOptionHandler(CmdLineParser,OptionDef,Setter)} */
    public FileListOptionHandler(
        CmdLineParser parser, OptionDef option, Setter<? super File> setter) {
      super(parser, option, setter);
    }

    @Override
    File parseItem(String item) {
      return new File(item);
    }
  }
  /** OptionHandler for args4j that handles a comma-delimited list of strings. */
  public static final class ModuleOptionHandler extends OptionHandler<Module> {
    /** {@link ListOptionHandler#ListOptionHandler(CmdLineParser,OptionDef,Setter)} */
    public ModuleOptionHandler(
        CmdLineParser parser, OptionDef option, Setter<? super Module> setter) {
      super(parser, option, setter);
    }

    @Override
    public int parseArguments(Parameters params) throws CmdLineException {
      String parameter = params.getParameter(0);
      // An empty string should be null
      if (parameter.isEmpty()) {
        setter.addValue(null);
      } else {
        setter.addValue(instantiatePluginModule(parameter));
      }
      return 1;
    }

    @Override
    public String getDefaultMetaVariable() {
      return "com.foo.bar.BazModule";
    }
  }

  /**
   * Parses command line flags written with args4j.
   *
   * @param objWithFlags An instance of a class containing args4j flag definitions.
   * @param args The args string to parse.
   * @param usagePrefix The string to prepend to the usage message (when reporting an error).
   * @return The CmdLineParser that was created and used to parse the args (can be used to print
   *     usage text for flags when reporting errors).
   */
  static CmdLineParser parseFlags(Object objWithFlags, String[] args, String usagePrefix) {
    CmdLineParser.registerHandler(Module.class, ModuleOptionHandler.class);
    // overwrite the built in boolean handler
    CmdLineParser.registerHandler(Boolean.class, BooleanOptionHandler.class);
    CmdLineParser.registerHandler(boolean.class, BooleanOptionHandler.class);

    CmdLineParser cmdLineParser = new CmdLineParser(objWithFlags);
    cmdLineParser.setUsageWidth(100);

    try {
      cmdLineParser.parseArgument(args);

    } catch (CmdLineException cle) {
      exitWithError(cle.getMessage(), cmdLineParser, usagePrefix);
    }

    return cmdLineParser;
  }

  static void run(Main method) {
    int status = runInternal(method);
    System.exit(status);
  }

  @VisibleForTesting
  static int runInternal(Main method) {
    try {
      method.main();
      return 0;
    } catch (SoyCompilationException compilationException) {
      System.err.println(compilationException.getMessage());
      return 1;
    } catch (Exception e) {
      System.err.println("INTERNAL SOY ERROR.\n"
          + "Please open an issue at "
          + "https://github.com/google/closure-templates/issues"
          + " with this stack trace and repro steps"
      );
      e.printStackTrace(System.err);
      return 1;
    }
  }


  /**
   * Prints an error message and the usage string, and then exits.
   *
   * @param errorMsg The error message to print.
   * @param cmdLineParser The CmdLineParser used to print usage text for flags.
   * @param usagePrefix The string to prepend to the usage message (when reporting an error).
   */
  static RuntimeException exitWithError(
      String errorMsg, CmdLineParser cmdLineParser, String usagePrefix) {

    System.err.println("\nError: " + errorMsg + "\n\n");
    System.err.println(usagePrefix);
    cmdLineParser.printUsage(System.err);

    System.exit(1);
    throw new AssertionError(); // dead code
  }

  /**
   * Returns a Guice injector that includes the SoyModule, a message plugin module, and maybe
   * additional plugin modules, and maybe additional modules.
   *
   * @param msgPluginModuleName The full class name of the message plugin module. Required.
   * @param pluginModuleNames Comma-delimited list of full class names of additional plugin modules
   *     to include. Optional.
   * @return A Guice injector that includes the SoyModule, the given message plugin module, and the
   *     given additional plugin modules (if any).
   */
  static Injector createInjector(String msgPluginModuleName, String pluginModuleNames) {
    checkArgument(!msgPluginModuleName.isEmpty());
    return doCreateInjector(msgPluginModuleName, pluginModuleNames);
  }

  /**
   * Returns a Guice injector that includes the SoyModule, and maybe additional plugin modules.
   *
   * @param pluginModuleNames Comma-delimited list of full class names of additional plugin modules
   *     to include. Optional.
   * @return A Guice injector that includes the SoyModule, the given message plugin module, and the
   *     given additional plugin modules (if any).
   */
  static Injector createInjectorForPlugins(String pluginModuleNames) {
    return doCreateInjector("", pluginModuleNames);
  }

  /**
   * Returns a Guice injector that includes the SoyModule, and maybe additional plugin modules.
   *
   * @param msgPluginModuleName The full class name of the message plugin module. Required.
   */
  static Injector createInjectorForMsgPlugin(String msgPluginModuleName) {
    return doCreateInjector(msgPluginModuleName, "");
  }

  /** Returns a Guice injector that includes the SoyModule, and the given modules. */
  static Injector createInjector(List<Module> modules) {
    modules = new ArrayList<>(modules); // make a copy that we know is mutable
    modules.add(new SoyModule());
    return Guice.createInjector(modules); // TODO(lukes): Stage.PRODUCTION?
  }

  /**
   * Returns an injector configured with the given plugins
   * @param msgPluginModuleName The name of a guice module binding a msgplugin, may be empty
   * @param pluginModuleNames A comma delimited list of plugin modules name, may be empty
   */
  private static Injector doCreateInjector(String msgPluginModuleName, String pluginModuleNames) {
    List<Module> guiceModules = new ArrayList<>();
    guiceModules.add(new SoyModule());

    if (!msgPluginModuleName.isEmpty()) {
      guiceModules.add(instantiatePluginModule(msgPluginModuleName));
    }

    for (String pluginModuleName : Splitter.on(',').omitEmptyStrings().split(pluginModuleNames)) {
      guiceModules.add(instantiatePluginModule(pluginModuleName));
    }

    return Guice.createInjector(guiceModules);
  }

  /**
   * Private helper for createInjector().
   *
   * @param moduleName The name of the plugin module to instantiate.
   * @return A new instance of the specified plugin module.
   */
  private static Module instantiatePluginModule(String moduleName) {

    try {
      return (Module) Class.forName(moduleName).newInstance();

    } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
      throw new RuntimeException("Cannot instantiate plugin module \"" + moduleName + "\".", e);
    }
  }


  /**
   * Helper to add srcs and deps Soy files to a SoyFileSet builder. Also does sanity checks.
   * @param sfsBuilder The SoyFileSet builder to add to.
   * @param inputPrefix The input path prefix to prepend to all the file paths.
   * @param srcs The srcs from the --srcs flag. Exactly one of 'srcs' and 'args' must be nonempty.
   * @param args The old-style srcs from the command line (that's how they were specified before we
   *     added the --srcs flag). Exactly one of 'srcs' and 'args' must be nonempty.
   * @param deps The deps from the --deps flag, or empty list if not applicable.
   * @param exitWithErrorFn A function that exits with an error message followed by a usage message.
   */
  static void addSoyFilesToBuilder(
      Builder sfsBuilder, String inputPrefix, Collection<String> srcs, Collection<String> args,
      Collection<String> deps, Collection<String> indirectDeps,
      Function<String, Void> exitWithErrorFn) {
    if (srcs.isEmpty() && args.isEmpty()) {
      exitWithErrorFn.apply("Must provide list of source Soy files (--srcs).");
    }
    if (!srcs.isEmpty() && !args.isEmpty()) {
      exitWithErrorFn.apply(
          "Found source Soy files from --srcs and from args (please use --srcs only).");
    }

    // Create Set versions of each of the arguments, and de-dupe. If something is included as
    // multiple file kinds, we'll keep the strongest one; a file in both srcs and deps will be a
    // src, and one in both deps and indirect_deps will be a dep.
    // TODO(gboyer): Maybe stop supporting old style (srcs from command line args) at some point.
    Set<String> srcsSet = ImmutableSet.<String>builder().addAll(srcs).addAll(args).build();
    Set<String> depsSet = Sets.difference(ImmutableSet.copyOf(deps), srcsSet);
    Set<String> indirectDepsSet = Sets.difference(ImmutableSet.copyOf(indirectDeps),
        Sets.union(srcsSet, depsSet));

    for (String src : srcsSet) {
      sfsBuilder.addWithKind(new File(inputPrefix + src), SoyFileKind.SRC);
    }
    for (String dep : depsSet) {
      sfsBuilder.addWithKind(new File(inputPrefix + dep), SoyFileKind.DEP);
    }
    for (String dep : indirectDepsSet) {
      sfsBuilder.addWithKind(new File(inputPrefix + dep), SoyFileKind.INDIRECT_DEP);
    }
  }

}
