package saros.intellij.ui.util;

import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.InputValidator;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.TextRange;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import saros.SarosPluginContext;
import saros.exceptions.IllegalAWTContextException;
import saros.intellij.runtime.EDTExecutor;

/**
 * Dialog helper used to show messages in safe manner by starting it on the AWT event dispatcher
 * thread.
 *
 * <p><b>NOTE:</b> Synchronous dialogs must not be triggered while inside a write safe context. This
 * applies to all input dialogs as they need to be executed synchronously to return the input value.
 * Such dialogs should check whether they are executed in a write safe context with {@link
 * Application#isWriteAccessAllowed()} and then throw an {@link
 * saros.exceptions.IllegalAWTContextException}.
 *
 * <p>Asynchronous dialogs can still be safely executed from any context with {@link
 * Application#invokeLater(Runnable,ModalityState)}.
 */
public class SafeDialogUtils {
  private static final Logger log = Logger.getLogger(SafeDialogUtils.class);

  private static final Application application;

  static {
    application = ApplicationManager.getApplication();

    SarosPluginContext.initComponent(new SafeDialogUtils());
  }

  private SafeDialogUtils() {}

  /**
   * Synchronously shows an input dialog. This method must not be called from a write safe context
   * as it needs to be executed synchronously and AWT actions are not allowed from a write safe
   * context.
   *
   * @param project the project used as a reference to generate and position the dialog
   * @param message the text displayed as the message of the dialog
   * @param initialValue the initial value contained in the text field of the input dialog
   * @param title the text displayed as the title of the dialog
   * @param selection the input range that is selected by default
   * @return the <code>String</code> entered by the user or <code>null</code> if the dialog did not
   *     finish with the exit code 0 (it was not closed by pressing the "OK" button)
   * @throws IllegalAWTContextException if the calling thread is currently inside a write safe
   *     context
   * @see Messages.InputDialog#getInputString()
   * @see com.intellij.openapi.ui.DialogWrapper#OK_EXIT_CODE
   */
  public static String showInputDialog(
      Project project,
      final String message,
      final String initialValue,
      final String title,
      InputValidator inputValidator,
      TextRange selection)
      throws IllegalAWTContextException {

    if (application.isWriteAccessAllowed()) {
      throw new IllegalAWTContextException("AWT events are not allowed " + "inside write actions.");
    }

    log.info("Showing input dialog: " + title + " - " + message + " - " + initialValue);

    return EDTExecutor.invokeAndWait(
        (Computable<String>)
            () ->
                Messages.showInputDialog(
                    project,
                    message,
                    title,
                    Messages.getQuestionIcon(),
                    initialValue,
                    inputValidator,
                    selection),
        ModalityState.defaultModalityState());
  }

  /**
   * Calls {@link #showInputDialog(Project, String, String, String, InputValidator, TextRange)} with
   * <code>inputValidator=null</code>.
   *
   * @see #showInputDialog(Project, String, String, String, TextRange)
   */
  public static String showInputDialog(
      Project project,
      final String message,
      final String initialValue,
      final String title,
      TextRange selection)
      throws IllegalAWTContextException {

    return showInputDialog(project, message, initialValue, title, null, selection);
  }

  /**
   * Calls {@link #showInputDialog(Project, String, String, String, InputValidator, TextRange)} with
   * <code>inputValidator=null</code> and <code>selection=null</code>.
   *
   * @see #showInputDialog(Project, String, String, String, TextRange)
   */
  public static String showInputDialog(
      Project project, final String message, final String initialValue, final String title)
      throws IllegalAWTContextException {

    return showInputDialog(project, message, initialValue, title, null);
  }

  /**
   * Asynchronously shows an info dialog.
   *
   * @param project the project used as a reference to generate and position the dialog
   * @param message the text displayed as the message of the dialog
   * @param title the text displayed as the title of the dialog
   */
  public static void showInfo(Project project, final String message, final String title) {
    log.info("Showing info dialog: " + title + " - " + message);

    EDTExecutor.invokeLater(
        () -> Messages.showInfoMessage(project, message, title),
        ModalityState.defaultModalityState());
  }

  /**
   * Asynchronously shows an error dialog.
   *
   * @param project the project used as a reference to generate and position the dialog
   * @param message the text displayed as the message of the dialog
   * @param title the text displayed as the title of the dialog
   */
  public static void showError(Project project, final String message, final String title) {
    log.info("Showing error dialog: " + title + " - " + message);

    EDTExecutor.invokeLater(
        () -> Messages.showErrorDialog(project, message, title),
        ModalityState.defaultModalityState());
  }

  /**
   * Synchronously shows a password dialog. This method must not be called from a write safe context
   * as it needs to be executed synchronously and AWT actions are not allowed from a write safe
   * context.
   *
   * @param project the project used as a reference to generate and position the dialog
   * @param message the text displayed as the message of the dialog
   * @param title the text displayed as the title of the dialog
   * @return the <code>String</code> entered by the user or <code>null</code> if the dialog did not
   *     finish with the exit code 0 (it was not closed by pressing the "OK" button)
   * @throws IllegalAWTContextException if the calling thread is currently inside a write safe
   *     context
   * @see Messages.InputDialog#getInputString()
   * @see com.intellij.openapi.ui.DialogWrapper#OK_EXIT_CODE
   */
  public static String showPasswordDialog(Project project, final String message, final String title)
      throws IllegalAWTContextException {

    if (application.isWriteAccessAllowed()) {
      throw new IllegalAWTContextException("AWT events are not allowed " + "inside write actions.");
    }

    log.info("Showing password dialog: " + title + " - " + message);

    return EDTExecutor.invokeAndWait(
        (Computable<String>)
            () -> Messages.showPasswordDialog(project, message, title, Messages.getQuestionIcon()),
        ModalityState.defaultModalityState());
  }

  /**
   * Synchronously shows a yes/no dialog. This method must not be called from a write safe context
   * as it needs to be executed synchronously and AWT actions are not allowed from a write safe
   * context.
   *
   * @param project the project used as a reference to generate and position the dialog
   * @param message the text displayed as the message of the dialog
   * @param title the text displayed as the title of the dialog
   * @return <code>true</code> if {@link Messages#YES} is chosen or <code>false</code> if {@link
   *     Messages#NO} is chosen or the dialog is closed
   * @throws IllegalAWTContextException if the calling thread is currently inside a write safe
   *     context
   * @throws IllegalStateException if no response value was received from the dialog or the response
   *     was not {@link Messages#YES} or {@link Messages#NO}.
   */
  public static boolean showYesNoDialog(Project project, final String message, final String title)
      throws IllegalAWTContextException {

    if (application.isWriteAccessAllowed()) {
      throw new IllegalAWTContextException("AWT events are not allowed inside write actions.");
    }

    log.info("Showing yes/no dialog: " + title + " - " + message);

    Integer result =
        EDTExecutor.invokeAndWait(
            (Computable<Integer>)
                () -> Messages.showYesNoDialog(project, message, title, Messages.getQuestionIcon()),
            ModalityState.defaultModalityState());

    switch (result) {
      case Messages.YES:
        return true;
      case Messages.NO:
        return false;
      default:
        throw new IllegalStateException("Encountered unknown dialog answer " + result);
    }
  }

  /**
   * Shows a non-blocking yes/no dialog. The passed <code>runnable</code> can be used to run code
   * after the dialog is finished. However, it is <b>only</b> run if the user finishes the dialog
   * with the option {@link Messages#YES}.
   *
   * @param project the project used as a reference to generate and position the dialog
   * @param message the text displayed as the message of the dialog
   * @param title the text displayed as the title of the dialog
   * @param runAfter the runnable to execute if the user chooses {@link Messages#YES}
   * @throws IllegalStateException if the response from the dialog was not {@link Messages#YES} or
   *     {@link Messages#NO}.
   */
  public static void showYesNoDialog(
      @NotNull Project project,
      @NotNull String message,
      @NotNull String title,
      @NotNull Runnable runAfter) {

    log.info("Showing non-blocking yes/no dialog: " + title + " - " + message);

    EDTExecutor.invokeLater(
        () -> {
          int option =
              Messages.showYesNoDialog(project, message, title, Messages.getQuestionIcon());

          if (option == Messages.YES) {
            runAfter.run();

          } else if (option != Messages.NO) {
            throw new IllegalStateException("Encountered unknown dialog answer " + option);
          }
        },
        ModalityState.defaultModalityState());
  }
}