/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2013-2015 ForgeRock AS.
 */
package org.opends.server.tools.upgrade;

import static com.forgerock.opendj.cli.ArgumentConstants.*;
import static com.forgerock.opendj.cli.Utils.*;
import static javax.security.auth.callback.TextOutputCallback.*;

import static org.opends.messages.ToolMessages.*;
import static org.opends.server.tools.upgrade.FormattedNotificationCallback.*;
import static org.opends.server.tools.upgrade.Upgrade.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.ConfirmationCallback;
import javax.security.auth.callback.TextOutputCallback;
import javax.security.auth.callback.UnsupportedCallbackException;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.server.core.DirectoryServer.DirectoryServerVersionHandler;
import org.opends.server.extensions.ConfigFileHandler;
import org.opends.server.util.StaticUtils;

import com.forgerock.opendj.cli.ArgumentException;
import com.forgerock.opendj.cli.BooleanArgument;
import com.forgerock.opendj.cli.ClientException;
import com.forgerock.opendj.cli.CommonArguments;
import com.forgerock.opendj.cli.ConsoleApplication;
import com.forgerock.opendj.cli.StringArgument;
import com.forgerock.opendj.cli.SubCommandArgumentParser;

/**
 * This class provides the CLI used for upgrading the OpenDJ product.
 */
public final class UpgradeCli extends ConsoleApplication implements
    CallbackHandler
{
  /** Upgrade's logger. */
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /** The command-line argument parser. */
  private final SubCommandArgumentParser parser;

  /** The argument which should be used to specify the config class. */
  private StringArgument configClass;
  /** The argument which should be used to specify the config file. */
  private StringArgument configFile;

  /** The argument which should be used to specify non interactive mode. */
  private BooleanArgument noPrompt;
  private BooleanArgument ignoreErrors;
  private BooleanArgument force;
  private BooleanArgument quietMode;
  private BooleanArgument verbose;
  private BooleanArgument acceptLicense;


  /** The argument which should be used to request usage information. */
  private BooleanArgument showUsageArgument;

  /**
   * Flag indicating whether or not the global arguments have
   * already been initialized.
   */
  private boolean globalArgumentsInitialized;

  private UpgradeCli(InputStream in, OutputStream out, OutputStream err)
  {
    super(new PrintStream(out), new PrintStream(err));
    this.parser =
        new SubCommandArgumentParser(getClass().getName(),
            INFO_UPGRADE_DESCRIPTION_CLI.get(), false);
    this.parser.setVersionHandler(new DirectoryServerVersionHandler());
    this.parser.setShortToolDescription(REF_SHORT_DESC_UPGRADE.get());
    this.parser.setDocToolDescriptionSupplement(SUPPLEMENT_DESCRIPTION_UPGRADE_CLI.get());
  }

  /**
   * Provides the command-line arguments to the main application for processing.
   *
   * @param args
   *          The set of command-line arguments provided to this program.
   */
  public static void main(String[] args)
  {
    final int exitCode = main(args, true, System.out, System.err);
    if (exitCode != 0)
    {
      System.exit(filterExitCode(exitCode));
    }
  }

  /**
   * Provides the command-line arguments to the main application for processing
   * and returns the exit code as an integer.
   *
   * @param args
   *          The set of command-line arguments provided to this program.
   * @param initializeServer
   *          Indicates whether to perform basic initialization (which should
   *          not be done if the tool is running in the same JVM as the server).
   * @param outStream
   *          The output stream for standard output.
   * @param errStream
   *          The output stream for standard error.
   * @return Zero to indicate that the program completed successfully, or
   *         non-zero to indicate that an error occurred.
   */
  public static int main(String[] args, boolean initializeServer,
      OutputStream outStream, OutputStream errStream)
  {
    final UpgradeCli app = new UpgradeCli(System.in, outStream, errStream);

    // Run the application.
    return app.run(args, initializeServer);
  }

  /** {@inheritDoc} */
  @Override
  public boolean isAdvancedMode()
  {
    return false;
  }

  /** {@inheritDoc} */
  @Override
  public boolean isInteractive()
  {
    return !noPrompt.isPresent();
  }

  /** {@inheritDoc} */
  @Override
  public boolean isMenuDrivenMode()
  {
    return false;
  }

  /** {@inheritDoc} */
  @Override
  public boolean isQuiet()
  {
    return quietMode.isPresent();
  }

  /** {@inheritDoc} */
  @Override
  public boolean isScriptFriendly()
  {
    return false;
  }

  /** {@inheritDoc} */
  @Override
  public boolean isVerbose()
  {
    return verbose.isPresent();
  }

  /**
   * Force the upgrade. All critical questions will be forced to 'yes'.
   *
   * @return {@code true} if the upgrade process is forced.
   */
  private boolean isForceUpgrade()
  {
    return force.isPresent();
  }

  /**
   * Force to ignore the errors during the upgrade process.
   * Continues rather than fails.
   *
   * @return {@code true} if the errors are forced to be ignored.
   */
  private boolean isIgnoreErrors()
  {
    return ignoreErrors.isPresent();
  }

  /**
   * Automatically accepts the license if it's present.
   *
   * @return {@code true} if license is accepted by default.
   */
  private boolean isAcceptLicense()
  {
    return acceptLicense.isPresent();
  }

  /** Initialize arguments provided by the command line. */
  private void initializeGlobalArguments() throws ArgumentException
  {
    if (!globalArgumentsInitialized)
    {
      configClass = CommonArguments.getConfigClass(ConfigFileHandler.class.getName());
      configFile = CommonArguments.getConfigFile();
      noPrompt = CommonArguments.getNoPrompt();
      verbose = CommonArguments.getVerbose();
      quietMode = CommonArguments.getQuiet();

      ignoreErrors =
          new BooleanArgument(OPTION_LONG_IGNORE_ERRORS, null,
              OPTION_LONG_IGNORE_ERRORS, INFO_UPGRADE_OPTION_IGNORE_ERRORS
                  .get());

      force = new BooleanArgument(OPTION_LONG_FORCE_UPGRADE, null,
          OPTION_LONG_FORCE_UPGRADE,
          INFO_UPGRADE_OPTION_FORCE.get(OPTION_LONG_NO_PROMPT));

      acceptLicense = CommonArguments.getAcceptLicense();
      showUsageArgument = CommonArguments.getShowUsage();


      // Register the global arguments.
      parser.addGlobalArgument(showUsageArgument);
      parser.setUsageArgument(showUsageArgument, getOutputStream());
      parser.addGlobalArgument(configClass);
      parser.addGlobalArgument(configFile);
      parser.addGlobalArgument(noPrompt);
      parser.addGlobalArgument(verbose);
      parser.addGlobalArgument(quietMode);
      parser.addGlobalArgument(force);
      parser.addGlobalArgument(ignoreErrors);
      parser.addGlobalArgument(acceptLicense);

      globalArgumentsInitialized = true;
    }
  }

  private int run(String[] args, boolean initializeServer)
  {
    // Initialize the arguments
    try
    {
      initializeGlobalArguments();
    }
    catch (ArgumentException e)
    {
      final LocalizableMessage message = ERR_CANNOT_INITIALIZE_ARGS.get(e.getMessage());
      getOutputStream().print(message);
      return EXIT_CODE_ERROR;
    }

    // Parse the command-line arguments provided to this program.
    try
    {
      parser.parseArguments(args);
      if (isInteractive() && isQuiet())
      {
        final LocalizableMessage message =
            ERR_UPGRADE_INCOMPATIBLE_ARGS.get(OPTION_LONG_QUIET,
                "interactive mode");
        getOutputStream().println(message);
        return EXIT_CODE_ERROR;
      }
      if (isInteractive() && isForceUpgrade())
      {
        final LocalizableMessage message =
            ERR_UPGRADE_INCOMPATIBLE_ARGS.get(OPTION_LONG_FORCE_UPGRADE,
                "interactive mode");
        getOutputStream().println(message);
        return EXIT_CODE_ERROR;
      }
      if (isQuiet() && isVerbose())
      {
        final LocalizableMessage message =
            ERR_UPGRADE_INCOMPATIBLE_ARGS.get(OPTION_LONG_QUIET,
                OPTION_LONG_VERBOSE);
        getOutputStream().println(message);
        return EXIT_CODE_ERROR;
      }
    }
    catch (ArgumentException ae)
    {
      parser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
      return EXIT_CODE_ERROR;
    }

    // If the usage/version argument was provided, then we don't need
    // to do anything else.
    if (parser.usageOrVersionDisplayed())
    {
      return EXIT_CODE_SUCCESS;
    }

    // Main process
    try
    {
      // Creates the log file.
      UpgradeLog.initLogFileHandler();

      // Upgrade's context.
      UpgradeContext context = new UpgradeContext(this)
          .setIgnoreErrorsMode(isIgnoreErrors())
          .setAcceptLicenseMode(isAcceptLicense())
          .setInteractiveMode(isInteractive())
          .setForceUpgradeMode(isForceUpgrade());

      // Starts upgrade.
      Upgrade.upgrade(context);
    }
    catch (ClientException ex)
    {
      return ex.getReturnCode();
    }
    catch (Exception ex)
    {
      println(Style.ERROR, ERR_UPGRADE_MAIN_UPGRADE_PROCESS.get(ex
          .getMessage()), 0);

      return EXIT_CODE_ERROR;
    }
    return EXIT_CODE_SUCCESS;
  }

  /** {@inheritDoc} */
  @Override
  public void handle(Callback[] callbacks) throws IOException,
      UnsupportedCallbackException
  {
    for (final Callback c : callbacks)
    {
      // Displays progress eg. for a task.
      if (c instanceof ProgressNotificationCallback)
      {
        final ProgressNotificationCallback pnc =
            (ProgressNotificationCallback) c;
        printProgressBar(pnc.getMessage(), pnc.getProgress(), 2);
      }
      else if (c instanceof FormattedNotificationCallback)
      {
        // Displays formatted notifications.
        final FormattedNotificationCallback fnc =
            (FormattedNotificationCallback) c;
        switch (fnc.getMessageSubType())
        {
        case TITLE_CALLBACK:
          println(Style.TITLE, LocalizableMessage.raw(fnc.getMessage()), 0);
          logger.info(LocalizableMessage.raw(fnc.getMessage()));
          break;
        case SUBTITLE_CALLBACK:
          println(Style.SUBTITLE, LocalizableMessage.raw(fnc.getMessage()),
              4);
          logger.info(LocalizableMessage.raw(fnc.getMessage()));
          break;
        case NOTICE_CALLBACK:
          println(Style.NOTICE, LocalizableMessage.raw(fnc.getMessage()), 1);
          logger.info(LocalizableMessage.raw(fnc.getMessage()));
          break;
        case ERROR_CALLBACK:
          println(Style.ERROR, LocalizableMessage.raw(fnc.getMessage()), 1);
          logger.error(LocalizableMessage.raw(fnc.getMessage()));
          break;
        case WARNING:
          println(Style.WARNING, LocalizableMessage.raw(fnc.getMessage()), 2);
          logger.warn(LocalizableMessage.raw(fnc.getMessage()));
          break;
        default:
          logger.error(LocalizableMessage.raw("Unsupported message type: "
            + fnc.getMessage()));
          throw new IOException("Unsupported message type: ");
        }
      }
      else if (c instanceof TextOutputCallback)
      {
        // Usual output text.
        final TextOutputCallback toc = (TextOutputCallback) c;
        if(toc.getMessageType() == TextOutputCallback.INFORMATION) {
          logger.info(LocalizableMessage.raw(toc.getMessage()));
          println(LocalizableMessage.raw(toc.getMessage()));
        } else {
          logger.error(LocalizableMessage.raw("Unsupported message type: "
            + toc.getMessage()));
          throw new IOException("Unsupported message type: ");
        }
      }
      else if (c instanceof ConfirmationCallback)
      {
        final ConfirmationCallback cc = (ConfirmationCallback) c;
        List<String> choices = new ArrayList<>();

        final String defaultOption = getDefaultOption(cc.getDefaultOption());
        StringBuilder prompt =
            new StringBuilder(wrapText(cc.getPrompt(), MAX_LINE_WIDTH, 2));

        // Default answers.
        final List<String> yesNoDefaultResponses =
            StaticUtils.arrayToList(
              INFO_PROMPT_YES_COMPLETE_ANSWER.get().toString(),
              INFO_PROMPT_YES_FIRST_LETTER_ANSWER.get().toString(),
              INFO_PROMPT_NO_COMPLETE_ANSWER.get().toString(),
              INFO_PROMPT_NO_FIRST_LETTER_ANSWER.get().toString());

        // Generating prompt and possible answers list.
        prompt.append(" ").append("(");
        if (cc.getOptionType() == ConfirmationCallback.YES_NO_OPTION)
        {
          choices.addAll(yesNoDefaultResponses);
          prompt.append(INFO_PROMPT_YES_COMPLETE_ANSWER.get())
              .append("/")
              .append(INFO_PROMPT_NO_COMPLETE_ANSWER.get());
        }
        else if (cc.getOptionType()
            == ConfirmationCallback.YES_NO_CANCEL_OPTION)
        {
          choices.addAll(yesNoDefaultResponses);
          choices.addAll(StaticUtils.arrayToList(
              INFO_TASKINFO_CMD_CANCEL_CHAR.get().toString()));

          prompt.append(" ")
                .append("(").append(INFO_PROMPT_YES_COMPLETE_ANSWER.get())
                .append("/").append(INFO_PROMPT_NO_COMPLETE_ANSWER.get())
                .append("/").append(INFO_TASKINFO_CMD_CANCEL_CHAR.get());
        }
        prompt.append(")");

        logger.info(LocalizableMessage.raw(cc.getPrompt()));

        // Displays the output and
        // while it hasn't a valid response, question is repeated.
        if (isInteractive())
        {
          while (true)
          {
            String value = null;
            try
            {
              value =
                  readInput(LocalizableMessage.raw(prompt), defaultOption,
                      Style.SUBTITLE);
            }
            catch (ClientException e)
            {
              logger.error(LocalizableMessage.raw(e.getMessage()));
              break;
            }

            String valueLC = value.toLowerCase();
            if ((valueLC.equals(INFO_PROMPT_YES_FIRST_LETTER_ANSWER.get().toString())
                || valueLC.equals(INFO_PROMPT_YES_COMPLETE_ANSWER.get().toString()))
                && choices.contains(value))
            {
              cc.setSelectedIndex(ConfirmationCallback.YES);
              break;
            }
            else if ((valueLC.equals(INFO_PROMPT_NO_FIRST_LETTER_ANSWER.get().toString())
                || valueLC.equals(INFO_PROMPT_NO_COMPLETE_ANSWER.get().toString()))
                && choices.contains(value))
            {
              cc.setSelectedIndex(ConfirmationCallback.NO);
              break;
            }
            else if (valueLC.equals(INFO_TASKINFO_CMD_CANCEL_CHAR.get().toString())
                && choices.contains(value))
            {
              cc.setSelectedIndex(ConfirmationCallback.CANCEL);
              break;
            }
            logger.info(LocalizableMessage.raw(value));
          }
        }
        else // Non interactive mode :
        {
          // Force mode.
          if (isForceUpgrade())
          {
            cc.setSelectedIndex(ConfirmationCallback.YES);
          }
          else // Default non interactive mode.
          {
            cc.setSelectedIndex(cc.getDefaultOption());
          }
          // Displays the prompt
          prompt.append(" ").append(getDefaultOption(cc.getSelectedIndex()));
          println(Style.SUBTITLE, LocalizableMessage.raw(prompt), 0);
          logger.info(LocalizableMessage.raw(getDefaultOption(cc.getSelectedIndex())));
        }
      }
      else
      {
        logger.error(LocalizableMessage.raw("Unrecognized Callback"));
        throw new UnsupportedCallbackException(c, "Unrecognized Callback");
      }
    }
  }



  private static String getDefaultOption(final int defaultOption)
  {
    if (defaultOption == ConfirmationCallback.YES)
    {
      return INFO_PROMPT_YES_COMPLETE_ANSWER.get().toString();
    }
    else if (defaultOption == ConfirmationCallback.NO)
    {
      return INFO_PROMPT_NO_COMPLETE_ANSWER.get().toString();
    }
    else if (defaultOption == ConfirmationCallback.CANCEL)
    {
      return INFO_TASKINFO_CMD_CANCEL_CHAR.get().toString();
    }
    return null;
  }
}
