/*
 * Copyright (C) 2015 The Pennsylvania State University and the University of Wisconsin
 * Systems and Internet Infrastructure Security Laboratory
 *
 * Author: Damien Octeau
 *
 * 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 edu.psu.cse.siis.coal;

import java.io.File;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An abstract parser for command line options. Subclasses should implement
 * {@link #parseAnalysisSpecificArguments} and {@link #printHelp}.
 * 
 * @param <A> A {@link CommandLineArguments} parameter.
 */
public abstract class CommandLineParser<A extends CommandLineArguments> {
  private final Logger logger = LoggerFactory.getLogger(getClass());

  /**
   * Parses the command line options. This method parses default options that are common to all
   * analyses and it also causes analysis-specific options to be processed.
   * 
   * @param args The command line arguments.
   * @param clazz The class type of the {@link CommandLineArguments} that should be returned.
   * @return The parsed command line arguments.
   */
  public A parseCommandLine(String[] args, Class<A> clazz) {
    Options options = new Options();

    parseDefaultCommandLineArguments(options);
    parseAnalysisSpecificArguments(options);

    CommandLine commandLine = null;
    try {
      org.apache.commons.cli.CommandLineParser commandLineParser = new DefaultParser();
      commandLine = commandLineParser.parse(options, args);
    } catch (ParseException e) {
      printHelp(options);
      logger.error("Could not parse command line arguments", e);
      return null;
    }

    A commandLineArguments = null;
    try {
      commandLineArguments = clazz.newInstance();
    } catch (InstantiationException | IllegalAccessException e) {
      logger.error("Could not instantiate type " + clazz, e);
      return null;
    }
    commandLineArguments.setCommandLine(commandLine);
    commandLineArguments.setModel(commandLine.getOptionValue("model"));
    commandLineArguments.setCompiledModel(commandLine.getOptionValue("cmodel"));
    commandLineArguments.setInput(commandLine.getOptionValue("in"));
    commandLineArguments.setClasspath(commandLine.getOptionValue("cp") + File.pathSeparator + commandLineArguments.getInput() + File.pathSeparator);
    commandLineArguments.setOutput(commandLine.getOptionValue("out"));
    commandLineArguments.setTraverseModeled(commandLine.hasOption("traversemodeled"));
    AnalysisParameters.v().setInferNonModeledTypes(!commandLine.hasOption("modeledtypesonly"));

    int threadCount;
    try {
      threadCount =
          commandLineArguments.hasOption("threadcount") ? ((Number) commandLineArguments
              .getParsedOptionValue("threadcount")).intValue() : Runtime.getRuntime()
              .availableProcessors();
    } catch (ParseException exception) {
      logger.error(
          "Could not parse thread count: " + commandLineArguments.getOptionValue("threadcount"),
          exception);
      return null;
    }
    AnalysisParameters.v().setThreadCount(threadCount);

    return commandLineArguments;
  }

  /**
   * Populates the analysis-specific command line options.
   * 
   * @param options The options that should be populated.
   */
  protected abstract void parseAnalysisSpecificArguments(Options options);

  /**
   * Specifies how the help message should be printed.
   * 
   * @param options The options that should be used to print the help message.
   */
  protected abstract void printHelp(Options options);

  /**
   * Populates the default command line arguments that are common to all analyses.
   * 
   * @param options The command line options object that should be modified.
   */
  private void parseDefaultCommandLineArguments(Options options) {
    OptionGroup modelGroup = new OptionGroup();
    modelGroup.addOption(Option.builder("model").desc("Path to the model directory.").hasArg()
        .argName("model directory").build());
    modelGroup.addOption(Option.builder("cmodel").desc("Path to the compiled model.").hasArg()
        .argName("compiled model").build());
    modelGroup.setRequired(false);

    options.addOptionGroup(modelGroup);

    options.addOption(Option.builder("cp").desc("The classpath for the analysis.").hasArg()
        .argName("classpath").required().longOpt("classpath").build());
    options.addOption(Option.builder("in").desc("The input code for the analysis.").hasArg()
        .argName("input").required().longOpt("input").build());
    options.addOption(Option.builder("out").desc("The output directory or file.").hasArg()
        .argName("output").longOpt("output").build());
    options.addOption(Option.builder("traversemodeled").desc("Propagate through modeled classes.")
        .hasArg(false).build());
    options.addOption("modeledtypesonly", false, "Only infer modeled types.");
    options.addOption(Option.builder("threadcount")
        .desc("The maximum number of threads that should be used.").hasArg()
        .argName("thread count").type(Number.class).build());
  }
}