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

package org.apache.geode.modules.session.installer.args;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

/**
 * This class is used to process command line arguments for Java programs in a flexible and powerful
 * manner.
 */
public class ArgumentProcessor {
  /**
   * Logger.
   */
  private static final Logger LOG = Logger.getLogger(ArgumentProcessor.class.getName());

  /**
   * Description line length.
   */
  private static final int LINE_LENGTH = 60;

  /**
   * Map containing all arguments defined, indexed by their unique IDs.
   */
  private final List<Argument> args = new ArrayList<>();

  /**
   * Unknown argument handler.
   */
  private UnknownArgumentHandler handler;

  /**
   * Program name to display in usage.
   */
  private final String programName;

  ///////////////////////////////////////////////////////////////////////////
  // Classes:

  /**
   * Structure used to represent an argument match.
   */
  private static class Match {
    /**
     * The argument which matched.
     */
    private final Argument arg;

    /**
     * The specific form which matched.
     */
    private final String form;

    /**
     * The parameters to the argument form.
     */
    private final String[] params;

    /**
     * Constructor.
     *
     * @param theArgument the argument which matched
     * @param theForm the form used
     * @param theParams the parameters supplied
     */
    public Match(final Argument theArgument, final String theForm, final String[] theParams) {
      arg = theArgument;
      form = theForm;
      params = theParams;
    }

    /**
     * Accessor.
     *
     * @return argument which matched
     */
    public Argument getArgument() {
      return arg;
    }

    /**
     * Accessor.
     *
     * @return form which was used
     */
    public String getForm() {
      return form;
    }

    /**
     * Accessor.
     *
     * @return parameters supplied
     */
    public String[] getParams() {
      return params;
    }
  }

  ///////////////////////////////////////////////////////////////////////////
  // Constructors:

  /**
   * Creates a new Argument processor instance for te program name given.
   *
   * @param progName program name used in usage
   */
  public ArgumentProcessor(final String progName) {
    programName = progName;
  }


  ///////////////////////////////////////////////////////////////////////////
  // Public methods:

  /**
   * Adds a new argument.
   *
   * @param arg argument to add
   */
  public void addArgument(final Argument arg) {
    args.add(arg);
  }

  /**
   * Sets the handler to call when an unknown argument is encountered.
   *
   * @param aHandler unknown arg handler, or null to unset
   */
  public void setUnknownArgumentHandler(final UnknownArgumentHandler aHandler) {
    handler = aHandler;
  }

  /**
   * Process the command line arguments provided.
   *
   * @param programArgs command line arguments supplied to program
   * @return argument values parsed out of command line
   * @throws UsageException when usge sucked
   */
  public ArgumentValues process(final String[] programArgs) throws UsageException {
    ArgumentHandler argHandler;
    final ArgumentValues result = new ArgumentValues();
    List<Argument> unmatched;
    List<Match> matches;

    // Find all argument matches and set postArgs
    matches = checkMatches(programArgs, result);

    // Find arguments which didnt match
    unmatched = new ArrayList<>();
    unmatched.addAll(args);
    for (Match match : matches) {
      unmatched.remove(match.getArgument());
    }

    // Error on unmatched yet required args
    for (Argument arg : unmatched) {
      if (arg.isRequired() && !arg.isDefinedInEnv()) {
        final UsageException usageException =
            new UsageException("Required argument not provided: " + arg);
        usageException.setUsage(getUsage());
        throw usageException;
      }
    }

    // Handle the arguments
    for (Match match : matches) {
      final Argument arg = match.getArgument();
      argHandler = arg.getArgumentHandler();
      if (argHandler != null) {
        argHandler.handleArgument(arg, match.getForm(), match.getParams());
      }
      result.addResult(arg, match.getParams());
    }

    return result;
  }


  /**
   * Generates command line usage text for display to user.
   *
   * @return usage to dusplay to user
   */
  public String getUsage() {
    final StringBuilder builder = new StringBuilder();
    List<String> descriptionLines;
    final String blank20 = "                    ";

    builder.append("\nUSAGE: ");
    if (programName == null) {
      builder.append("<program>");
    } else {
      builder.append(programName);
    }
    if (args.isEmpty()) {
      builder.append("\nNo arguments supported.\n");
    } else {
      builder.append(" <args>\nWHERE <args>:\n\n");
      for (Argument arg : args) {
        for (String form : arg.getForms()) {
          builder.append("    ");
          builder.append(form);

          for (int i = 0; i < arg.getParameterCount(); i++) {
            builder.append(" <");
            builder.append(arg.getParameterName(i));
            builder.append(">");
          }
          builder.append("\n");
        }

        descriptionLines = breakupString(arg.getDescription(), LINE_LENGTH);
        if (descriptionLines.isEmpty()) {
          builder.append(blank20);
          builder.append("No argument description provided.");
          builder.append("\n\n");
        } else {
          for (String line : descriptionLines) {
            builder.append(blank20);
            builder.append(line.trim());
            builder.append("\n");
          }
          builder.append("\n");
        }
      }
    }
    builder.append("\n");

    return builder.toString();
  }


  ///////////////////////////////////////////////////////////////////////////
  // Private methods:

  /**
   * Builds a listof all argument matches and sets the postArgs array.
   *
   * @param programArgs command line arguments to search through
   * @param values values object in which to store results
   * @return list of matches
   * @throws UsageException when there is EBKAC
   */
  private List<Match> checkMatches(final String[] programArgs, final ArgumentValues values)
      throws UsageException {
    final List<Match> result = new ArrayList<>();
    Match match;
    String[] params;
    String[] postArgs;
    int idx = 0;
    int idx2;

    while (idx < programArgs.length) {
      // Check for end-of-parameters arg
      if ("--".equals(programArgs[idx])) {
        if (++idx < programArgs.length) {
          postArgs = new String[programArgs.length - idx];
          System.arraycopy(programArgs, idx, postArgs, 0, postArgs.length);
          values.setPostArgs(postArgs);
        }
        // We're done processing args'
        break;
      }

      // Determine parameter count
      idx2 = idx;
      while ((idx2 + 1) < programArgs.length && programArgs[idx2 + 1].charAt(0) != '-') {
        idx2++;
      }

      // Generate parameter array
      params = new String[idx2 - idx];
      System.arraycopy(programArgs, idx + 1, params, 0, params.length);

      LOG.fine("Arg: " + programArgs[idx]);
      LOG.fine("Params: " + params.length);

      // Find first argument matches
      match = null;
      for (Argument arg : args) {
        match = checkMatch(programArgs[idx], arg, params);
        if (match != null) {
          result.add(match);
          LOG.fine("Match found: ");
          LOG.fine("     ID: " + arg);
          LOG.fine("   Form: " + match.getForm());
          break;
        }
      }
      if (match == null) {
        if (handler == null) {
          final UsageException usageException = new UsageException(
              "Unknown argument: " + programArgs[idx] + " with " + params.length + " parameters.");
          usageException.setUsage(getUsage());
          throw (usageException);
        } else {
          handler.handleUnknownArgument(programArgs[idx], params);
        }
      }

      idx += params.length + 1;
    }

    return result;
  }

  /**
   * Checks to see if an rgument form matches the suppies parameter list.
   *
   * @param argName argument name
   * @param arg argument
   * @param params parameters supplied
   * @return match object on match, null otherwise
   */
  private Match checkMatch(final String argName, final Argument arg, final String[] params) {
    // Look for a matching form
    for (String form : arg.getForms()) {
      if (form.equals(argName) && arg.getParameterCount() == params.length) {
        return new Match(arg, form, params);
      }
    }

    return null;
  }

  /**
   * Breaks up a string into sub-strings, each with a length equal to or less than the max length
   * specified.
   *
   * @param str string to break up
   * @param maxLength maximum line length to use
   * @return broken up string
   */
  private List<String> breakupString(final String str, final int maxLength) {
    final List<String> result = new ArrayList<>();
    int startIdx = -1;
    int lastIdx;
    int idx;

    if (str == null) {
      return result;
    }

    do {
      idx = startIdx;
      do {
        lastIdx = idx;
        idx = str.indexOf(' ', lastIdx + 1);
        LOG.fine("startIdx=" + startIdx + "  lastIdx=" + lastIdx + "  idx=" + idx);
        if (idx < 0) {
          // Canot break line up any further
          result.add(str.substring(startIdx + 1));
          return result;
        }
      } while ((idx - startIdx) <= maxLength);

      result.add(str.substring(startIdx + 1, lastIdx));
      startIdx = lastIdx;
    } while (true);
  }

}
