/*
 * Copyright 2016 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 com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.ForOverride;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.protobuf.Descriptors.DescriptorValidationException;
import com.google.template.soy.MainClassUtils.Main;
import com.google.template.soy.error.SoyCompilationException;
import com.google.template.soy.msgs.SoyMsgPlugin;
import com.google.template.soy.types.SoyTypeProvider;
import com.google.template.soy.types.SoyTypeRegistry;
import com.google.template.soy.types.proto.SoyProtoTypeProvider;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.CheckReturnValue;

/**
 * Base class for the Soy Compilers.
 *
 * <p>Defines common flags and performs shared initialization routines.
 *
 * <p>TODO(lukes): make all compilers subclass this type and move all MainClassUtils logic into this
 * class.
 */
abstract class AbstractSoyCompiler {
  /** The string to prepend to the usage message. */
  private final String usagePrefix =
      "Usage:\n"
          + "java "
          + getClass().getName()
          + " \\\n"
          + "     [<flag1> <flag2> ...] --jar <jarName>  \\\n"
          + "     --srcs <soyFilePath>,... [--deps <soyFilePath>,...]\n";

  private final Function<String, Void> exitWithErrorFn =
      new Function<String, Void>() {
        @Override
        public Void apply(String errorMsg) {
          exitWithError(errorMsg);
          return null;
        }
      };

  @Option(
    name = "--inputPrefix",
    usage =
        "If provided, this path prefix will be prepended to each input file path"
            + " listed on the command line. This is a literal string prefix, so you'll need"
            + " to include a trailing slash if necessary."
  )
  protected String inputPrefix = "";

  @Option(
    name = "--srcs",
    usage =
        "The list of source Soy files. Extra arguments are treated as srcs. Sources"
            + " are required from either this flag or as extra arguments.",
    handler = MainClassUtils.StringListOptionHandler.class
  )
  private List<String> srcs = new ArrayList<>();

  @Option(
    name = "--deps",
    usage =
        "The list of dependency Soy files (if applicable). The compiler needs deps for"
            + " analysis/checking, but will not generate code for dep files.",
    handler = MainClassUtils.StringListOptionHandler.class
  )
  private List<String> deps = new ArrayList<>();

  @Option(
    name = "--indirectDeps",
    usage = "Soy files required by deps, but which may not be used by srcs.",
    handler = MainClassUtils.StringListOptionHandler.class
  )
  private List<String> indirectDeps = new ArrayList<>();

  @Option(
    name = "--compileTimeGlobalsFile",
    usage =
        "The path to a file containing the mappings for global names to be substituted"
            + " at compile time. Each line of the file should have the format"
            + " \"<global_name> = <primitive_data>\" where primitive_data is a valid Soy"
            + " expression literal for a primitive type (null, boolean, integer, float, or"
            + " string). Empty lines and lines beginning with \"//\" are ignored. The file"
            + " should be encoded in UTF-8. If you need to generate a file in this format"
            + " from Java, consider using the utility"
            + " SoyUtils.generateCompileTimeGlobalsFile()."
  )
  private File globalsFile = null;

  @Option(
    name = "--pluginModules",
    usage =
        "Specifies the full class names of Guice modules for function plugins and"
            + " print directive plugins (comma-delimited list).",
    handler = MainClassUtils.ModuleListOptionHandler.class
  )
  private List<Module> pluginModules = new ArrayList<>();

  @Option(name = "--protoFileDescriptors",
      usage = "Location of protocol buffer definitions in the form of a file descriptor set."
            + "The compiler needs defs for parameter type checking and generating direct "
            + "access support for proto types.",
      handler = MainClassUtils.FileListOptionHandler.class)
  private static final List<File> protoFileDescriptors = new ArrayList<>();

  /** The remaining arguments after parsing command-line flags. */
  @Argument private final List<String> arguments = new ArrayList<>();

  private CmdLineParser cmdLineParser;

  final void runMain(String ...args) {
    int status = run(args);
    System.exit(status);
  }

  @VisibleForTesting
  @CheckReturnValue
  int run(final String ...args) {
    // TODO(lukes): inline this method once all mains have been migrated to this base class.
    return MainClassUtils.runInternal(new Main() {
      @Override
      public void main() throws IOException, SoyCompilationException {
        doMain(args);
      }
    });
  }

  private void doMain(String[] args) throws IOException {
    this.cmdLineParser = MainClassUtils.parseFlags(this, args, usagePrefix);

    validateFlags();
    if (!arguments.isEmpty() && !acceptsSourcesAsArguments()) {
      exitWithError("Found old style sources passed on the command line, use --srcs=... instead");
    }

    List<Module> modules = new ArrayList<>();
    modules.addAll(pluginModules);
    modules.addAll(msgPluginModule().asSet());
    Injector injector = MainClassUtils.createInjector(modules);
    SoyFileSet.Builder sfsBuilder = injector.getInstance(SoyFileSet.Builder.class);
    // TODO(lukes): Only call setLocalTypeRegistry if we were actually passed protos, we should
    // change this to always call setLocalTypeRegistry (and rename the method) once all uses of the
    // legacy proto configuration are gone.
    if (!protoFileDescriptors.isEmpty()) {
      SoyProtoTypeProvider.Builder protoTypeProviderBuilder = new SoyProtoTypeProvider.Builder();
      for (File descriptor : protoFileDescriptors) {
        protoTypeProviderBuilder.addFileDescriptorSetFromFile(descriptor);
      }
      try {
        sfsBuilder.setLocalTypeRegistry(new SoyTypeRegistry(
            ImmutableSet.<SoyTypeProvider>of(protoTypeProviderBuilder.build())));
      } catch (DescriptorValidationException ex) {
        throw new IOException("Malformed descriptor set", ex);
      }
    }
    MainClassUtils.addSoyFilesToBuilder(
        sfsBuilder, inputPrefix, srcs, arguments, deps, indirectDeps, exitWithErrorFn);
    if (globalsFile != null) {
      sfsBuilder.setCompileTimeGlobals(globalsFile);
    }
    compile(sfsBuilder, injector);
  }

  /**
   * Returns {@code true} if old style sources should be supported. {@code true} is the default.
   *
   * <p>Old style srcs are for when source files are passed as arguments directly, rather than
   * passed to the {@code --srcs} flag.
   *
   * <p>TODO(lukes): eliminate support for old-style srcs
   */
  boolean acceptsSourcesAsArguments() {
    return true;
  }

  /**
   * Returns an additional plugin module to support the {@link SoyMsgPlugin}. This is only neccesary
   * if the compiler needs to perform msg extraction.
   */
  @ForOverride
  Optional<Module> msgPluginModule() {
    return Optional.absent();
  }

  /**
   * Extension point for subtypes to perform additional logic to validate compiler specific flags.
   */
  @ForOverride
  void validateFlags() {}

  /**
   * Performs the actual compilation.
   *
   * @param sfsBuilder The builder, already populated with sources, globals (if set) and plugins.
   *     subclasses may set additional compilation options on the builder.
   * @param injector The injector
   * @throws IOException
   */
  @ForOverride
  void compile(SoyFileSet.Builder sfsBuilder, Injector injector) throws IOException {
    compile(sfsBuilder);
  }

  /**
   * Performs the actual compilation.
   *
   * @param sfsBuilder The builder, already populated with sources, globals (if set) and plugins.
   *     subclasses may set additional compilation options on the builder.
   * @throws IOException
   */
  @ForOverride
  void compile(SoyFileSet.Builder sfsBuilder) throws IOException {
    throw new AbstractMethodError("must override at least one overload of compile()");
  }

  /**
   * Prints an error message and the usage string, and then exits.
   *
   * @param errorMsg The error message to print.
   */
  final RuntimeException exitWithError(String errorMsg) {
    return MainClassUtils.exitWithError(errorMsg, cmdLineParser, usagePrefix);
  }
}
