/**
   * Displays an information bubble pointing to an operator to indicate a {@link UserError} was
   * thrown by that operator.
   *
   * @param error the error instance
   * @param i18nKey the i18n key which defines the title, text and button label for the bubble.
   *     Format is "gui.bubble.{i18nKey}.title", "gui.bubble.{i18nKey}.body" and
   *     "gui.bubble.{i18nKey}.button.label".
   * @return the {@link OperatorInfoBubble} instance, never {@code null}
   */
  private static OperatorInfoBubble displayGenericUserError(
      final UserError error, final String i18nKey) {
    final JButton ackButton =
        new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label"));
    ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip"));

    final BubbleDelegator bubbleDelegator = new BubbleDelegator();
    final String message = removeTerminationCharacters(error.getMessage());
    final JPanel linkPanel = new JPanel();
    LinkButton showDetailsButton =
        new LinkButton(
            new ResourceAction(i18nKey + ".button_show_details") {

              private static final long serialVersionUID = 1L;

              @Override
              public void actionPerformed(final ActionEvent e) {
                BubbleWindow bubble = bubbleDelegator.getBubble();
                if (bubble != null) {
                  String text =
                      I18N.getMessage(
                          I18N.getGUIBundle(),
                          "gui.bubble." + i18nKey + ".body",
                          message,
                          error.getDetails());
                  bubble.setMainText(text);

                  linkPanel.removeAll();
                  bubble.pack();
                }
              }
            });
    showDetailsButton.setToolTipText(
        I18N.getGUIMessage("gui.action." + i18nKey + ".button_show_details.tip"));
    linkPanel.add(showDetailsButton);

    OperatorBubbleBuilder builder =
        new OperatorBubbleBuilder(
            RapidMinerGUI.getMainFrame(), error.getOperator(), i18nKey, message, "");
    // if no operator or root operator, show in middle, otherwise below
    AlignedSide prefSide =
        error.getOperator() == null || error.getOperator() instanceof ProcessRootOperator
            ? AlignedSide.MIDDLE
            : AlignedSide.BOTTOM;
    final OperatorInfoBubble userErrorBubble =
        builder
            .setHideOnDisable(true)
            .setAlignment(prefSide)
            .setStyle(BubbleStyle.ERROR)
            .setEnsureVisible(true)
            .hideCloseButton()
            .setHideOnProcessRun(true)
            .setAdditionalComponents(new JComponent[] {ackButton, linkPanel})
            .build();
    ackButton.addActionListener(
        new ActionListener() {

          @Override
          public void actionPerformed(ActionEvent e) {
            userErrorBubble.killBubble(true);
          }
        });
    bubbleDelegator.setBubbleWindow(userErrorBubble);

    userErrorBubble.setVisible(true);
    return userErrorBubble;
  }
  /**
   * Displays a warning bubble that alerts the user that an input port of an operator expected input
   * but that there was a problem. The bubble is located at the port and the process view will
   * change to said port. execution.
   *
   * @param port the port for which to display the warning
   * @param i18nKey the i18n key which defines the title, text and button label for the bubble.
   *     Format is "gui.bubble.{i18nKey}.title", "gui.bubble.{i18nKey}.body" and
   *     "gui.bubble.{i18nKey}.button.label".
   * @param hideOnConnection if {@code true}, the bubble will be hidden once the port is connected
   * @param isError if {@code true}, an error bubble will be shown; otherwise a warning bubble is
   *     displayed
   * @param arguments optional i18n arguments
   * @return the {@link PortInfoBubble} instance, never {@code null}
   */
  private static PortInfoBubble displayPrecheckMissingInputPortWarning(
      final Port port,
      final boolean hideOnConnection,
      final boolean isError,
      final String i18nKey,
      final Object... arguments) {
    final BubbleDelegator bubbleDelegator = new BubbleDelegator();
    ResourceAction runAnywayAction =
        new ResourceAction(i18nKey + ".button_run_anyway", "F11") {

          private static final long serialVersionUID = 1L;

          @Override
          public void actionPerformed(final ActionEvent e) {
            BubbleWindow bubble = bubbleDelegator.getBubble();
            if (bubble != null) {
              bubble.killBubble(true);
            }

            // run process without checking for problems
            RapidMinerGUI.getMainFrame().runProcess(false);
          }
        };
    LinkButton runAnywayButton = new LinkButton(runAnywayAction);
    runAnywayButton.setToolTipText(
        I18N.getGUIMessage("gui.bubble." + i18nKey + ".button_run_anyway.tip"));
    runAnywayButton.registerKeyboardAction(
        runAnywayAction,
        KeyStroke.getKeyStroke(KeyEvent.VK_F11, 0),
        JComponent.WHEN_IN_FOCUSED_WINDOW);

    final JButton ackButton =
        new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label", arguments));
    ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip"));

    PortBubbleBuilder builder =
        new PortBubbleBuilder(RapidMinerGUI.getMainFrame(), port, i18nKey, arguments);
    final PortInfoBubble missingInputBubble =
        builder
            .setHideOnConnection(hideOnConnection)
            .setHideOnDisable(true)
            .setAlignment(AlignedSide.LEFT)
            .setStyle(isError ? BubbleStyle.ERROR : BubbleStyle.WARNING)
            .setEnsureVisible(true)
            .hideCloseButton()
            .setHideOnProcessRun(true)
            .setAdditionalComponents(new JComponent[] {ackButton, runAnywayButton})
            .build();
    ackButton.addActionListener(
        new ActionListener() {

          @Override
          public void actionPerformed(ActionEvent e) {
            missingInputBubble.killBubble(true);
          }
        });
    bubbleDelegator.setBubbleWindow(missingInputBubble);

    missingInputBubble.setVisible(true);
    return missingInputBubble;
  }
  /**
   * Displays an information bubble pointing to an ExecuteProcess operator to indicate a {@link
   * UserError} occurred inside the process executed by the operator.
   *
   * @param error the error instance
   * @param i18nKey the i18n key which defines the title, text and button label for the bubble.
   *     Format is "gui.bubble.{i18nKey}.title", "gui.bubble.{i18nKey}.body" and
   *     "gui.bubble.{i18nKey}.button.label".
   * @param arguments optional i18n arguments
   * @return the {@link OperatorInfoBubble} instance, never {@code null}
   */
  private static OperatorInfoBubble displayUserErrorInExecutedProcess(
      final ProcessExecutionUserErrorError error, final String i18nKey, final Object... arguments) {
    final JButton ackButton =
        new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label", arguments));
    ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip"));

    final BubbleDelegator bubbleDelegator = new BubbleDelegator();
    LinkButton showDetailsButton =
        new LinkButton(
            new ResourceAction(i18nKey + ".button_show_details") {

              private static final long serialVersionUID = 1L;

              @Override
              public void actionPerformed(final ActionEvent e) {
                if (RapidMinerGUI.getMainFrame().close(true)) {
                  // kill bubble
                  BubbleWindow bubble = bubbleDelegator.getBubble();
                  if (bubble != null) {
                    bubble.killBubble(true);
                  }

                  // open process which caused the error
                  Operator op = error.getUserError().getOperator();
                  final Process causingProcess = op.getProcess();
                  RapidMinerGUI.getMainFrame()
                      .setOpenedProcess(
                          causingProcess, false, causingProcess.getProcessLocation().toString());

                  // show new error bubble in the newly opened process
                  displayBubbleForUserError(error.getUserError());
                }
              }
            });
    showDetailsButton.setToolTipText(
        I18N.getGUIMessage("gui.action." + i18nKey + ".button_show_details.tip"));

    OperatorBubbleBuilder builder =
        new OperatorBubbleBuilder(
            RapidMinerGUI.getMainFrame(), error.getOperator(), i18nKey, arguments);
    final OperatorInfoBubble userErrorBubble =
        builder
            .setHideOnDisable(true)
            .setHideOnProcessRun(true)
            .setAlignment(AlignedSide.BOTTOM)
            .setStyle(BubbleStyle.ERROR)
            .setEnsureVisible(true)
            .hideCloseButton()
            .setHideOnProcessRun(true)
            .setAdditionalComponents(new JComponent[] {ackButton, showDetailsButton})
            .build();
    ackButton.addActionListener(
        new ActionListener() {

          @Override
          public void actionPerformed(ActionEvent e) {
            userErrorBubble.killBubble(true);
          }
        });
    bubbleDelegator.setBubbleWindow(userErrorBubble);

    userErrorBubble.setVisible(true);
    return userErrorBubble;
  }
  /**
   * Displays a warning bubble that alerts the user that he failed to connect any of the process
   * result ports.
   *
   * <p>The bubble is located at the first result port of the outermost process and the process view
   * will change to said port. The {@link ResultWarningPreventionRegistry} contains a list with
   * Operators which can suppress this warning.
   *
   * @param process the process in question
   * @return the {@link PortInfoBubble} instance, never {@code null}
   */
  public static PortInfoBubble displayPrecheckNoResultPortInformation(final Process process) {
    if (process == null) {
      throw new IllegalArgumentException("port must not be null!");
    }

    Port firstResultPort =
        process.getRootOperator().getSubprocess(0).getInnerSinks().getPortByIndex(0);
    JButton ackButton =
        new JButton(I18N.getGUIMessage("gui.bubble.process_unconnected_result_port.button.label"));
    ackButton.setToolTipText(
        I18N.getGUIMessage("gui.bubble.process_unconnected_result_port.button.tip"));

    final BubbleDelegator bubbleDelegator = new BubbleDelegator();
    ResourceAction runAnywayAction =
        new ResourceAction("process_unconnected_result_port.button_run_anyway", "F11") {

          private static final long serialVersionUID = 1L;

          @Override
          public void actionPerformed(final ActionEvent e) {
            BubbleWindow bubble = bubbleDelegator.getBubble();
            if (bubble != null) {
              bubble.killBubble(true);
            }

            // run process without checking for problems
            RapidMinerGUI.getMainFrame().runProcess(false);
          }
        };
    LinkButton runAnywayButton = new LinkButton(runAnywayAction);
    runAnywayButton.setToolTipText(
        I18N.getGUIMessage("gui.bubble.process_unconnected_result_port.button_run_anyway.tip"));
    runAnywayButton.registerKeyboardAction(
        runAnywayAction,
        KeyStroke.getKeyStroke(KeyEvent.VK_F11, 0),
        JComponent.WHEN_IN_FOCUSED_WINDOW);

    PortBubbleBuilder builder =
        new PortBubbleBuilder(
            RapidMinerGUI.getMainFrame(), firstResultPort, "process_unconnected_result_port");
    final PortInfoBubble noResultConnectionBubble =
        builder
            .setHideOnConnection(true)
            .setAlignment(AlignedSide.LEFT)
            .setStyle(BubbleStyle.WARNING)
            .setHideOnProcessRun(false)
            .setEnsureVisible(true)
            .hideCloseButton()
            .setAdditionalComponents(new JComponent[] {ackButton, runAnywayButton})
            .build();
    ackButton.addActionListener(
        new ActionListener() {

          @Override
          public void actionPerformed(ActionEvent e) {
            noResultConnectionBubble.killBubble(true);
          }
        });
    bubbleDelegator.setBubbleWindow(noResultConnectionBubble);

    noResultConnectionBubble.setVisible(true);
    return noResultConnectionBubble;
  }
  /**
   * Displays an information bubble pointing to a port to indicate a {@link PortUserError} was
   * thrown.
   *
   * @param error the error instance
   * @param i18nKey the i18n key which defines the title, text and button label for the bubble.
   *     Format is "gui.bubble.{i18nKey}.title", "gui.bubble.{i18nKey}.body" and
   *     "gui.bubble.{i18nKey}.button.label".
   * @return the {@link PortInfoBubble} instance, never {@code null}
   */
  private static PortInfoBubble displayGenericPortError(
      final PortUserError error, final String i18nKey) {
    final JButton ackButton =
        new JButton(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.label"));
    ackButton.setToolTipText(I18N.getGUIMessage("gui.bubble." + i18nKey + ".button.tip"));

    final BubbleDelegator bubbleDelegator = new BubbleDelegator();
    final String message = removeTerminationCharacters(error.getMessage());
    final JPanel linkPanel = new JPanel();
    LinkButton showDetailsButton =
        new LinkButton(
            new ResourceAction(i18nKey + ".button_show_details") {

              private static final long serialVersionUID = 1L;

              @Override
              public void actionPerformed(final ActionEvent e) {
                BubbleWindow bubble = bubbleDelegator.getBubble();
                if (bubble != null) {
                  String text =
                      I18N.getMessage(
                          I18N.getGUIBundle(),
                          "gui.bubble." + i18nKey + ".body",
                          message,
                          error.getDetails());
                  bubble.setMainText(text);

                  linkPanel.removeAll();
                  bubble.pack();
                }
              }
            });
    showDetailsButton.setToolTipText(
        I18N.getGUIMessage("gui.action." + i18nKey + ".button_show_details.tip"));
    linkPanel.add(showDetailsButton);

    // input ports (located left) show the "hook" of the bubble on the left and vice versa
    AlignedSide prefSide =
        error.getPort() instanceof InputPort ? AlignedSide.LEFT : AlignedSide.RIGHT;
    PortBubbleBuilder builder =
        new PortBubbleBuilder(RapidMinerGUI.getMainFrame(), error.getPort(), i18nKey, message, "");
    final PortInfoBubble portErrorBubble =
        builder
            .setHideOnDisable(true)
            .setAlignment(prefSide)
            .setStyle(BubbleStyle.ERROR)
            .setEnsureVisible(true)
            .hideCloseButton()
            .setHideOnProcessRun(true)
            .setAdditionalComponents(new JComponent[] {ackButton, linkPanel})
            .build();
    ackButton.addActionListener(
        new ActionListener() {

          @Override
          public void actionPerformed(ActionEvent e) {
            portErrorBubble.killBubble(true);
          }
        });
    bubbleDelegator.setBubbleWindow(portErrorBubble);

    portErrorBubble.setVisible(true);
    return portErrorBubble;
  }