/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
 *
 *
 *      Copyright 2006-2010 Sun Microsystems, Inc.
 */

package org.opends.quicksetup.ui;

import org.opends.quicksetup.event.ButtonActionListener;
import org.opends.quicksetup.event.ProgressUpdateListener;
import org.opends.quicksetup.event.ButtonEvent;
import org.opends.quicksetup.event.ProgressUpdateEvent;
import org.opends.quicksetup.*;
import org.opends.quicksetup.util.ProgressMessageFormatter;
import org.opends.quicksetup.util.HtmlProgressMessageFormatter;
import org.opends.quicksetup.util.BackgroundTask;
import org.opends.server.util.SetupUtils;

import static org.opends.quicksetup.util.Utils.*;
import org.opends.messages.MessageBuilder;
import org.opends.messages.Message;
import static org.opends.messages.QuickSetupMessages.*;

import javax.swing.*;

import java.awt.Cursor;
import java.util.ArrayList;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.logging.Handler;
import java.util.Map;

/**
 * This class is responsible for doing the following:
 *
 * Check whether we are installing or uninstalling and which type of
 * installation we are running.
 *
 * Performs all the checks and validation of the data provided by the user
 * during the setup.
 *
 * It will launch also the installation once the user clicks on 'Finish' if we
 * are installing the product.
 *
 * If we are running a web start installation it will start the background
 * downloading of the jar files that are required to perform the installation
 * (OpenDS.jar, je.jar, etc.).  The global idea is to force the user to
 * download just one jar file (quicksetup.jar) to launch the Web Start
 * installer.  Until this class is not finished the WebStart Installer will be
 * on the ProgressStep.DOWNLOADING step.
 *
 */
public class QuickSetup implements ButtonActionListener, ProgressUpdateListener
{

  static private final Logger LOG =
          Logger.getLogger(QuickSetup.class.getName());

  private GuiApplication application;

  private CurrentInstallStatus installStatus;

  private WizardStep currentStep;

  private QuickSetupDialog dialog;

  private MessageBuilder progressDetails = new MessageBuilder();

  private ProgressDescriptor lastDescriptor;

  private ProgressDescriptor lastDisplayedDescriptor;

  private ProgressDescriptor descriptorToDisplay;

  // Update period of the dialogs.
  private static final int UPDATE_PERIOD = 500;

  // The full pathname of the MacOS X LaunchServices OPEN(1) helper.
  private static final String MAC_APPLICATIONS_OPENER = "/usr/bin/open";

  /**
   * This method creates the install/uninstall dialogs and to check the current
   * install status. This method must be called outside the event thread because
   * it can perform long operations which can make the user think that the UI is
   * blocked.
   *
   * @param args for the moment this parameter is not used but we keep it in
   * order to (in case of need) pass parameters through the command line.
   */
  public void initialize(String[] args)
  {
    ProgressMessageFormatter formatter = new HtmlProgressMessageFormatter();

    installStatus = new CurrentInstallStatus();

    application = Application.create();
    application.setProgressMessageFormatter(formatter);
    application.setCurrentInstallStatus(installStatus);
    if (args != null)
    {
      application.setUserArguments(args);
    }
    else
    {
      application.setUserArguments(new String[]{});
    }
    try
    {
      initLookAndFeel();
    }
    catch (Throwable t)
    {
      // This is likely a bug.
      t.printStackTrace();
    }

    /* In the calls to setCurrentStep the dialog will be created */
    setCurrentStep(application.getFirstWizardStep());
  }

  /**
   * This method displays the setup dialog. This method must be called from the
   * event thread.
   */
  public void display()
  {
    getDialog().packAndShow();
  }

  /**
   * ButtonActionListener implementation. It assumes that we are called in the
   * event thread.
   *
   * @param ev the ButtonEvent we receive.
   */
  public void buttonActionPerformed(ButtonEvent ev)
  {
    switch (ev.getButtonName())
    {
    case NEXT:
      nextClicked();
      break;

    case CLOSE:
      closeClicked();
      break;

    case FINISH:
      finishClicked();
      break;

    case QUIT:
      quitClicked();
      break;

    case CONTINUE_INSTALL:
      continueInstallClicked();
      break;

    case PREVIOUS:
      previousClicked();
      break;

    case LAUNCH_STATUS_PANEL:
      launchStatusPanelClicked();
      break;

    case INPUT_PANEL_BUTTON:
      inputPanelButtonClicked();
      break;

    default:
      throw new IllegalArgumentException("Unknown button name: "
          + ev.getButtonName());
    }
  }

  /**
   * ProgressUpdateListener implementation. Here we take the
   * ProgressUpdateEvent and create a ProgressDescriptor that
   * will be used to update the progress dialog.
   *
   * @param ev the ProgressUpdateEvent we receive.
   *
   * @see #runDisplayUpdater()
   */
  public void progressUpdate(ProgressUpdateEvent ev)
  {
    synchronized (this)
    {
      ProgressDescriptor desc = createProgressDescriptor(ev);
      boolean isLastDescriptor = desc.getProgressStep().isLast();
      if (isLastDescriptor)
      {
        lastDescriptor = desc;
      }

      descriptorToDisplay = desc;
    }
  }

  /**
   * This method is used to update the progress dialog.
   *
   * We are receiving notifications from the installer and uninstaller (this
   * class is a ProgressListener). However if we lots of notifications updating
   * the progress panel every time we get a progress update can result of a lot
   * of flickering. So the idea here is to have a minimal time between 2 updates
   * of the progress dialog (specified by UPDATE_PERIOD).
   *
   * @see #progressUpdate(org.opends.quicksetup.event.ProgressUpdateEvent)
   */
  private void runDisplayUpdater()
  {
    boolean doPool = true;
    while (doPool)
    {
      try
      {
        Thread.sleep(UPDATE_PERIOD);
      } catch (Exception ex)
      {
      }
      synchronized (this) {
        final ProgressDescriptor desc = descriptorToDisplay;
        if (desc != null) {
          if (desc != lastDisplayedDescriptor) {
            lastDisplayedDescriptor = desc;

            SwingUtilities.invokeLater(new Runnable() {
              public void run() {
                if (application.isFinished() &&
                    !getCurrentStep().isFinishedStep())
                {
                  setCurrentStep(application.getFinishedStep());
                }
                getDialog().displayProgress(desc);
              }
            });
          }
          doPool = desc != lastDescriptor;
        }
      }
    }
  }

  /**
   * Method called when user clicks 'Next' button of the wizard.
   *
   */
  private void nextClicked()
  {
    final WizardStep cStep = getCurrentStep();
    application.nextClicked(cStep, this);
    BackgroundTask<?> worker = new NextClickedBackgroundTask(cStep);
    getDialog().workerStarted();
    worker.startBackgroundTask();
  }

  private void updateUserData(final WizardStep cStep) {
    BackgroundTask<?> worker = new BackgroundTask<Object>() {
      public Object processBackgroundTask() throws UserDataException {
        try {
          application.updateUserData(cStep, QuickSetup.this);
        }
        catch (UserDataException uide) {
          throw uide;
        }
        catch (Throwable t) {
          throw new UserDataException(cStep,
                  getThrowableMsg(INFO_BUG_MSG.get(), t));
        }
        return null;
      }

      public void backgroundTaskCompleted(Object returnValue,
                                          Throwable throwable) {
        getDialog().workerFinished();

        if (throwable != null) {
          UserDataException ude = (UserDataException) throwable;
          if (ude instanceof UserDataConfirmationException)
          {
            if (displayConfirmation(ude.getMessageObject(),
                INFO_CONFIRMATION_TITLE.get()))
            {
              try
              {
              setCurrentStep(application.getNextWizardStep(cStep));
              }
              catch (Throwable t)
              {
                t.printStackTrace();
              }
            }
          }
          else
          {
            displayError(ude.getMessageObject(), INFO_ERROR_TITLE.get());
          }
        } else {
          setCurrentStep(application.getNextWizardStep(cStep));
        }
        if (currentStep.isProgressStep()) {
          launch();
        }
      }
    };
    getDialog().workerStarted();
    worker.startBackgroundTask();
  }

  /**
   * Method called when user clicks 'Finish' button of the wizard.
   *
   */
  private void finishClicked()
  {
    final WizardStep cStep = getCurrentStep();
    if (application.finishClicked(cStep, this)) {
      updateUserData(cStep);
    }
  }

  /**
   * Method called when user clicks 'Previous' button of the wizard.
   *
   */
  private void previousClicked()
  {
    WizardStep cStep = getCurrentStep();
    application.previousClicked(cStep, this);
    setCurrentStep(application.getPreviousWizardStep(cStep));
  }

  /**
   * Method called when user clicks 'Quit' button of the wizard.
   *
   */
  private void quitClicked()
  {
    WizardStep cStep = getCurrentStep();
    application.quitClicked(cStep, this);
  }

  /**
   * Method called when user clicks 'Continue' button in the case where there
   * is something installed.
   */
  private void continueInstallClicked()
  {
    // TODO:  move this stuff to Installer?
    application.forceToDisplay();
    getDialog().forceToDisplay();
    setCurrentStep(Step.WELCOME);
  }

  /**
   * Method called when user clicks 'Close' button of the wizard.
   *
   */
  private void closeClicked()
  {
    WizardStep cStep = getCurrentStep();
    application.closeClicked(cStep, this);
  }

  private void launchStatusPanelClicked()
  {
    BackgroundTask<Object> worker = new BackgroundTask<Object>()
    {
      public Object processBackgroundTask() throws UserDataException {
        try
        {
          Installation installation;
          if (isWebStart()) {
            String installDir = application.getUserData().getServerLocation();
            installation =
              new Installation(installDir, installDir);
          } else {
            installation = Installation.getLocal();
          }
          ProcessBuilder pb;
          if (isMacOS()) {
            ArrayList<String> cmd = new ArrayList<String>();
            cmd.add(MAC_APPLICATIONS_OPENER);
            cmd.add(getScriptPath(
                getPath(installation.getControlPanelCommandFile())));
            pb = new ProcessBuilder(cmd);
          } else {
            String cmd = getScriptPath(
                getPath(installation.getControlPanelCommandFile()));
            pb = new ProcessBuilder(cmd);
          }
          Map<String, String> env = pb.environment();
          env.put(SetupUtils.OPENDS_JAVA_HOME, System.getProperty("java.home"));
          Process process = pb.start();
          /**
           * Wait for 3 seconds.  Assume that if the process has not exited
           * everything went fine.
           */
          int returnValue = 0;
          try
          {
            Thread.sleep(3000);
          }
          catch (Throwable t)
          {
          }
          try
          {
            returnValue = process.exitValue();
          }
          catch (IllegalThreadStateException ithse)
          {
            // The process has not exited: assume that the status panel could
            // be launched successfully.
          }

          if (returnValue != 0)
          {
            throw new Error(
                    INFO_COULD_NOT_LAUNCH_CONTROL_PANEL_MSG.get().toString());
          }
        }
        catch (Throwable t)
        {
          // This looks like a bug
          t.printStackTrace();
          throw new Error(
              INFO_COULD_NOT_LAUNCH_CONTROL_PANEL_MSG.get().toString());
        }
        return null;
      }

      public void backgroundTaskCompleted(Object returnValue,
          Throwable throwable)
      {
        getDialog().getFrame().setCursor(
            Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));

        if (throwable != null)
        {
          displayError(Message.raw(throwable.getMessage()),
                  INFO_ERROR_TITLE.get());
        }
      }
    };
    getDialog().getFrame().setCursor(
        Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    worker.startBackgroundTask();
  }

  /**
   * This method tries to update the visibility of the steps panel.  The
   * contents are updated because the user clicked in one of the buttons
   * that could make the steps panel to change.
   *
   */
  private void inputPanelButtonClicked()
  {
    getDialog().getStepsPanel().updateStepVisibility(this);
  }

  /**
   * Method called when we want to quit the setup (for instance when the user
   * clicks on 'Close' or 'Quit' buttons and has confirmed that (s)he wants to
   * quit the program.
   *
   */
  public void quit()
  {
    LOG.log(Level.INFO, "quitting application");
    flushLogs();
    System.exit(0);
  }

  private void flushLogs() {
    Handler[] handlers = LOG.getHandlers();
    if (handlers != null) {
      for (Handler h : handlers) {
        h.flush();
      }
    }
  }

  /**
   * Launch the QuickSetup application Open DS.
   */
  public void launch()
  {
    application.addProgressUpdateListener(this);
    new Thread(application, "Application Thread").start();
    Thread t = new Thread(new Runnable()
    {
      public void run()
      {
        runDisplayUpdater();
        WizardStep ws = application.getCurrentWizardStep();
        getDialog().getButtonsPanel().updateButtons(ws);
      }
    });
    t.start();
  }

  /**
   * Get the current step.
   *
   * @return the currently displayed Step of the wizard.
   */
  private WizardStep getCurrentStep()
  {
    return currentStep;
  }

  /**
   * Set the current step. This will basically make the required calls in the
   * dialog to display the panel that corresponds to the step passed as
   * argument.
   *
   * @param step The step to be displayed.
   */
  public void setCurrentStep(WizardStep step)
  {
    if (step == null)
    {
      throw new NullPointerException("step is null");
    }
    currentStep = step;
    application.setDisplayedWizardStep(step, application.getUserData(),
        getDialog());
  }

  /**
   * Get the dialog that is displayed.
   *
   * @return the dialog.
   */
  public QuickSetupDialog getDialog()
  {
    if (dialog == null)
    {
      dialog = new QuickSetupDialog(application,
              installStatus, this);
      dialog.addButtonActionListener(this);
      application.setQuickSetupDialog(dialog);
    }
    return dialog;
  }

  /**
   * Displays an error message dialog.
   *
   * @param msg
   *          the error message.
   * @param title
   *          the title for the dialog.
   */
  public void displayError(Message msg, Message title)
  {
    if (isCli()) {
      System.err.println(msg);
    } else {
      getDialog().displayError(msg, title);
    }
  }

  /**
   * Displays a confirmation message dialog.
   *
   * @param msg
   *          the confirmation message.
   * @param title
   *          the title of the dialog.
   * @return <CODE>true</CODE> if the user confirms the message, or
   * <CODE>false</CODE> if not.
   */
  public boolean displayConfirmation(Message msg, Message title)
  {
    return getDialog().displayConfirmation(msg, title);
  }

  /**
   * Gets the string value for a given field name.
   *
   * @param fieldName
   *          the field name object.
   * @return the string value for the field name.
   */
  public String getFieldStringValue(FieldName fieldName)
  {
    String sValue = null;

    Object value = getFieldValue(fieldName);
    if (value != null)
    {
      if (value instanceof String)
      {
        sValue = (String) value;
      } else
      {
        sValue = String.valueOf(value);
      }
    }
    return sValue;
  }

  /**
   * Gets the value for a given field name.
   *
   * @param fieldName
   *          the field name object.
   * @return the value for the field name.
   */
  public Object getFieldValue(FieldName fieldName)
  {
    return getDialog().getFieldValue(fieldName);
  }

  /**
   * Marks the fieldName as valid or invalid depending on the value of the
   * invalid parameter. With the current implementation this implies basically
   * using a red color in the label associated with the fieldName object. The
   * color/style used to mark the label invalid is specified in UIFactory.
   *
   * @param fieldName
   *          the field name object.
   * @param invalid
   *          whether to mark the field valid or invalid.
   */
  public void displayFieldInvalid(FieldName fieldName, boolean invalid)
  {
    getDialog().displayFieldInvalid(fieldName, invalid);
  }

  /**
   * A method to initialize the look and feel.
   *
   */
  private void initLookAndFeel() throws Throwable
  {
    UIFactory.initialize();
  }

  /**
   * A methods that creates an ProgressDescriptor based on the value of a
   * ProgressUpdateEvent.
   *
   * @param ev
   *          the ProgressUpdateEvent used to generate the
   *          ProgressDescriptor.
   * @return the ProgressDescriptor.
   */
  private ProgressDescriptor createProgressDescriptor(ProgressUpdateEvent ev)
  {
    ProgressStep status = ev.getProgressStep();
    Message newProgressLabel = ev.getCurrentPhaseSummary();
    Message additionalDetails = ev.getNewLogs();
    Integer ratio = ev.getProgressRatio();

    if (additionalDetails != null)
    {
      progressDetails.append(additionalDetails);
    }
    // Note: progressDetails might have a certain number of characters that
    // break Message Formatter (for instance percentages).  When fix for
    // issue 2142 was committed it broke this code.  So here
    // we use Message.raw instead of calling directly progressDetails.toMessage
    return new ProgressDescriptor(status, ratio, newProgressLabel,
        Message.raw(progressDetails.toString()));
  }

  /**
   * This is a class used when the user clicks on next and that extends
   * BackgroundTask.
   */
  private class NextClickedBackgroundTask extends BackgroundTask<Object>
  {
    private WizardStep cStep;
    public NextClickedBackgroundTask(WizardStep cStep)
    {
      this.cStep = cStep;
    }

    public Object processBackgroundTask() throws UserDataException {
      try {
        application.updateUserData(cStep, QuickSetup.this);
      }
      catch (UserDataException uide) {
        throw uide;
      }
      catch (Throwable t) {
        throw new UserDataException(cStep,
                getThrowableMsg(INFO_BUG_MSG.get(), t));
      }
      return null;
    }

    public void backgroundTaskCompleted(Object returnValue,
                                        Throwable throwable) {
      getDialog().workerFinished();

      if (throwable != null)
      {
        if (!(throwable instanceof UserDataException))
        {
          LOG.log(Level.WARNING, "Unhandled exception.", throwable);
        }
        else
        {
          UserDataException ude = (UserDataException) throwable;
          if (ude instanceof UserDataConfirmationException)
          {
            if (displayConfirmation(ude.getMessageObject(),
                INFO_CONFIRMATION_TITLE.get()))
            {
              setCurrentStep(application.getNextWizardStep(cStep));
            }
          }
          else if (ude instanceof UserDataCertificateException)
          {
            final UserDataCertificateException ce =
              (UserDataCertificateException)ude;
            CertificateDialog dlg =
              new CertificateDialog(getDialog().getFrame(), ce);
            dlg.pack();
            dlg.setVisible(true);
            CertificateDialog.ReturnType answer = dlg.getUserAnswer();
            if (answer != CertificateDialog.ReturnType.NOT_ACCEPTED)
            {
              /*
               * Retry the click but now with the certificate accepted.
               */
              application.acceptCertificateForException(ce,
                  answer == CertificateDialog.ReturnType.ACCEPTED_PERMANENTLY);
              application.nextClicked(cStep, QuickSetup.this);
              BackgroundTask<Object> worker =
                new NextClickedBackgroundTask(cStep);
              getDialog().workerStarted();
              worker.startBackgroundTask();
            }
          }
          else
          {
            displayError(ude.getMessageObject(), INFO_ERROR_TITLE.get());
          }
        }
      } else {
        setCurrentStep(application.getNextWizardStep(cStep));
      }
    }
  }
}
