// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.uiDesigner;

import com.intellij.lang.properties.PropertiesReferenceManager;
import com.intellij.lang.properties.psi.PropertiesFile;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.util.Ref;
import com.intellij.psi.*;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.uiDesigner.compiler.AsmCodeGenerator;
import com.intellij.uiDesigner.componentTree.ComponentTreeBuilder;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.designSurface.ComponentDropLocation;
import com.intellij.uiDesigner.designSurface.DraggedComponentList;
import com.intellij.uiDesigner.designSurface.GuiEditor;
import com.intellij.uiDesigner.designSurface.Painter;
import com.intellij.uiDesigner.editor.UIFormEditor;
import com.intellij.uiDesigner.lw.*;
import com.intellij.uiDesigner.palette.ComponentItem;
import com.intellij.uiDesigner.palette.Palette;
import com.intellij.uiDesigner.propertyInspector.DesignerToolWindowManager;
import com.intellij.uiDesigner.propertyInspector.properties.BindingProperty;
import com.intellij.uiDesigner.propertyInspector.properties.IntroComponentProperty;
import com.intellij.uiDesigner.radComponents.RadAbstractGridLayoutManager;
import com.intellij.uiDesigner.radComponents.RadComponent;
import com.intellij.uiDesigner.radComponents.RadContainer;
import com.intellij.uiDesigner.radComponents.RadRootContainer;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.SlowOperations;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.*;

public final class FormEditingUtil {
  private FormEditingUtil() {
  }

  public static boolean canDeleteSelection(final GuiEditor editor) {
    final ArrayList<RadComponent> selection = getSelectedComponents(editor);
    if (selection.isEmpty()) return false;
    final RadRootContainer rootContainer = editor.getRootContainer();
    if (rootContainer.getComponentCount() > 0 && selection.contains(rootContainer.getComponent(0))) {
      return false;
    }
    return true;
  }

  /**
   * <b>This method must be executed in command</b>
   *
   * @param editor the editor in which the selection is deleted.
   */
  public static void deleteSelection(final GuiEditor editor) {
    final List<RadComponent> selection = getSelectedComponents(editor);
    deleteComponents(selection, true);
    editor.refreshAndSave(true);
  }

  public static void deleteComponents(final Collection<? extends RadComponent> selection, boolean deleteEmptyCells) {
    if (selection.isEmpty()) {
      return;
    }
    final RadRootContainer rootContainer = (RadRootContainer)getRoot(selection.iterator().next());
    final Set<String> deletedComponentIds = new HashSet<>();
    for (final RadComponent component : selection) {
      boolean wasSelected = component.isSelected();
      final RadContainer parent = component.getParent();

      boolean wasPackedHorz = false;
      boolean wasPackedVert = false;
      if (parent.getParent() != null && parent.getParent().isXY()) {
        final Dimension minSize = parent.getMinimumSize();
        wasPackedHorz = parent.getWidth() == minSize.width;
        wasPackedVert = parent.getHeight() == minSize.height;
      }

      iterate(component, new ComponentVisitor() {
        @Override
        public boolean visit(final IComponent c) {
          RadComponent rc = (RadComponent)c;
          BindingProperty.checkRemoveUnusedField(rootContainer, rc.getBinding(), null);
          deletedComponentIds.add(rc.getId());
          return true;
        }
      });


      GridConstraints delConstraints = parent.getLayoutManager().isGrid() ? component.getConstraints() : null;

      int index = parent.indexOfComponent(component);
      parent.removeComponent(component);
      if (wasSelected) {
        if (parent.getComponentCount() > index) {
          parent.getComponent(index).setSelected(true);
        }
        else if (index > 0 && parent.getComponentCount() == index) {
          parent.getComponent(index - 1).setSelected(true);
        }
        else {
          parent.setSelected(true);
        }
      }
      if (delConstraints != null && deleteEmptyCells) {
        deleteEmptyGridCells(parent, delConstraints);
      }

      if (wasPackedHorz || wasPackedVert) {
        final Dimension minSize = parent.getMinimumSize();
        Dimension newSize = new Dimension(parent.getWidth(), parent.getHeight());
        if (wasPackedHorz) {
          newSize.width = minSize.width;
        }
        if (wasPackedVert) {
          newSize.height = minSize.height;
        }
        parent.setSize(newSize);
      }
    }

    iterate(rootContainer, new ComponentVisitor() {
      @Override
      public boolean visit(final IComponent component) {
        RadComponent rc = (RadComponent)component;
        for (IProperty p : component.getModifiedProperties()) {
          if (p instanceof IntroComponentProperty icp) {
            final String value = icp.getValue(rc);
            if (deletedComponentIds.contains(value)) {
              try {
                icp.resetValue(rc);
              }
              catch (Exception e) {
                // ignore
              }
            }
          }
        }
        return true;
      }
    });
  }

  public static void deleteEmptyGridCells(final RadContainer parent, final GridConstraints delConstraints) {
    deleteEmptyGridCells(parent, delConstraints, true);
    deleteEmptyGridCells(parent, delConstraints, false);
  }

  private static void deleteEmptyGridCells(final RadContainer parent, final GridConstraints delConstraints, final boolean isRow) {
    final RadAbstractGridLayoutManager layoutManager = parent.getGridLayoutManager();
    for (int cell = delConstraints.getCell(isRow) + delConstraints.getSpan(isRow) - 1; cell >= delConstraints.getCell(isRow); cell--) {
      if (cell < parent.getGridCellCount(isRow) && GridChangeUtil.canDeleteCell(parent, cell, isRow) == GridChangeUtil.CellStatus.Empty &&
          !layoutManager.isGapCell(parent, isRow, cell)) {
        layoutManager.deleteGridCells(parent, cell, isRow);
      }
    }
  }

  public static final int EMPTY_COMPONENT_SIZE = 5;

  private static Component getDeepestEmptyComponentAt(JComponent parent, Point location) {
    int size = parent.getComponentCount();

    for (int i = 0; i < size; i++) {
      Component child = parent.getComponent(i);

      if (child.isShowing()) {
        if (child.getWidth() < EMPTY_COMPONENT_SIZE || child.getHeight() < EMPTY_COMPONENT_SIZE) {
          Point childLocation = child.getLocationOnScreen();
          Rectangle bounds = new Rectangle();

          bounds.x = childLocation.x;
          bounds.y = childLocation.y;
          bounds.width = child.getWidth();
          bounds.height = child.getHeight();
          bounds.grow(child.getWidth() < EMPTY_COMPONENT_SIZE ? EMPTY_COMPONENT_SIZE : 0,
                      child.getHeight() < EMPTY_COMPONENT_SIZE ? EMPTY_COMPONENT_SIZE : 0);

          if (bounds.contains(location)) {
            return child;
          }
        }

        if (child instanceof JComponent) {
          Component result = getDeepestEmptyComponentAt((JComponent)child, location);

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

    return null;
  }

  /**
   * @param x in editor pane coordinates
   * @param y in editor pane coordinates
   */
  public static RadComponent getRadComponentAt(final RadRootContainer rootContainer, final int x, final int y) {
    Point location = new Point(x, y);
    SwingUtilities.convertPointToScreen(location, rootContainer.getDelegee());
    Component c = getDeepestEmptyComponentAt(rootContainer.getDelegee(), location);

    if (c == null) {
      c = SwingUtilities.getDeepestComponentAt(rootContainer.getDelegee(), x, y);
    }

    RadComponent result = null;

    while (c != null) {
      if (c instanceof JComponent) {
        final RadComponent component = (RadComponent)((JComponent)c).getClientProperty(RadComponent.CLIENT_PROP_RAD_COMPONENT);
        if (component != null) {

          if (result == null) {
            result = component;
          }
          else {
            final Point p = SwingUtilities.convertPoint(rootContainer.getDelegee(), x, y, c);
            if (Painter.getResizeMask(component, p.x, p.y) != 0) {
              result = component;
            }
          }
        }
      }
      c = c.getParent();
    }

    return result;
  }

  /**
   * @return component which has dragger. There is only one component with the dargger
   *         at a time.
   */
  public static @Nullable RadComponent getDraggerHost(final @NotNull GuiEditor editor) {
    final Ref<RadComponent> result = new Ref<>();
    iterate(
      editor.getRootContainer(),
      new ComponentVisitor<RadComponent>() {
        @Override
        public boolean visit(final RadComponent component) {
          if (component.hasDragger()) {
            result.set(component);
            return false;
          }
          return true;
        }
      }
    );

    return result.get();
  }


  public static Cursor getMoveDropCursor() {
    try {
      return Cursor.getSystemCustomCursor("MoveDrop.32x32");
    }
    catch (Exception ex) {
      return Cursor.getDefaultCursor();
    }
  }

  public static Cursor getMoveNoDropCursor() {
    try {
      return Cursor.getSystemCustomCursor("MoveNoDrop.32x32");
    }
    catch (Exception ex) {
      return Cursor.getDefaultCursor();
    }
  }

  public static Cursor getCopyDropCursor() {
    try {
      return Cursor.getSystemCustomCursor("CopyDrop.32x32");
    }
    catch (Exception ex) {
      return Cursor.getDefaultCursor();
    }
  }

  /**
   * @return currently selected components. Method returns the minimal amount of
   *         selected component which contains all selected components. It means that if the
   *         selected container contains some selected children then only this container
   *         will be added to the returned array.
   */
  public static @NotNull ArrayList<RadComponent> getSelectedComponents(final @NotNull GuiEditor editor) {
    final ArrayList<RadComponent> result = new ArrayList<>();
    calcSelectedComponentsImpl(result, editor.getRootContainer());
    return result;
  }

  private static void calcSelectedComponentsImpl(final ArrayList<? super RadComponent> result, final RadContainer container) {
    if (container.isSelected()) {
      if (container.getParent() != null) { // ignore RadRootContainer
        result.add(container);
        return;
      }
    }

    for (int i = 0; i < container.getComponentCount(); i++) {
      final RadComponent component = container.getComponent(i);
      if (component instanceof RadContainer) {
        calcSelectedComponentsImpl(result, (RadContainer)component);
      }
      else {
        if (component.isSelected()) {
          result.add(component);
        }
      }
    }
  }

  /**
   * @return all selected component inside the {@code editor}
   */
  public static @NotNull ArrayList<RadComponent> getAllSelectedComponents(final @NotNull GuiEditor editor) {
    final ArrayList<RadComponent> result = new ArrayList<>();
    iterate(
      editor.getRootContainer(),
      new ComponentVisitor<RadComponent>() {
        @Override
        public boolean visit(final RadComponent component) {
          if (component.isSelected()) {
            result.add(component);
          }
          return true;
        }
      }
    );
    return result;
  }

  public static String getExceptionMessage(Throwable ex) {
    if (ex instanceof RuntimeException) {
      final Throwable cause = ex.getCause();
      if (cause != null && cause != ex) {
        return getExceptionMessage(cause);
      }
    }
    else if (ex instanceof InvocationTargetException) {
      final Throwable target = ((InvocationTargetException)ex).getTargetException();
      if (target != null && target != ex) {
        return getExceptionMessage(target);
      }
    }
    String message = ex.getMessage();
    if (ex instanceof ClassNotFoundException) {
      message =
        message != null ? UIDesignerBundle.message("error.class.not.found.N", message) : UIDesignerBundle.message("error.class.not.found");
    }
    else if (ex instanceof NoClassDefFoundError) {
      message = message != null
                ? UIDesignerBundle.message("error.required.class.not.found.N", message)
                : UIDesignerBundle.message("error.required.class.not.found");
    }
    return message;
  }

  public static IComponent findComponentWithBinding(IComponent component, final String binding) {
    return findComponentWithBinding(component, binding, null);
  }

  public static IComponent findComponentWithBinding(IComponent component,
                                                    final String binding,
                                                    final @Nullable IComponent exceptComponent) {
    // Check that binding is unique
    final Ref<IComponent> boundComponent = new Ref<>();
    iterate(
      component,
      new ComponentVisitor() {
        @Override
        public boolean visit(final IComponent component) {
          if (component != exceptComponent && binding.equals(component.getBinding())) {
            boundComponent.set(component);
            return false;
          }
          return true;
        }
      }
    );

    return boundComponent.get();
  }

  public static @Nullable RadContainer getRadContainerAt(final RadRootContainer rootContainer, final int x, final int y,
                                                         int epsilon) {
    RadComponent component = getRadComponentAt(rootContainer, x, y);
    if (isNullOrRoot(component) && epsilon > 0) {
      // try to find component near specified location
      component = getRadComponentAt(rootContainer, x - epsilon, y - epsilon);
      if (isNullOrRoot(component)) component = getRadComponentAt(rootContainer, x - epsilon, y + epsilon);
      if (isNullOrRoot(component)) component = getRadComponentAt(rootContainer, x + epsilon, y - epsilon);
      if (isNullOrRoot(component)) component = getRadComponentAt(rootContainer, x + epsilon, y + epsilon);
    }

    if (component != null) {
      return component instanceof RadContainer
             ? (RadContainer)component
             : component.getParent();
    }
    return null;
  }

  private static boolean isNullOrRoot(final RadComponent component) {
    return component == null || component instanceof RadRootContainer;
  }

  public static GridConstraints getDefaultConstraints(final RadComponent component) {
    final Palette palette = Palette.getInstance(component.getProject());
    final ComponentItem item = palette.getItem(component.getComponentClassName());
    if (item != null) {
      return item.getDefaultConstraints();
    }
    return new GridConstraints();
  }

  public static IRootContainer getRoot(IComponent component) {
    while (component != null) {
      if (component.getParentContainer() instanceof IRootContainer) {
        return (IRootContainer)component.getParentContainer();
      }
      component = component.getParentContainer();
    }
    return null;
  }

  /**
   * Iterates component and its children (if any)
   */
  public static void iterate(final IComponent component, final ComponentVisitor visitor) {
    iterateImpl(component, visitor);
  }

  private static boolean iterateImpl(final IComponent component, final ComponentVisitor visitor) {
    final boolean shouldContinue;
    try {
      shouldContinue = visitor.visit(component);
    }
    catch (ProcessCanceledException ex) {
      return false;
    }
    if (!shouldContinue) {
      return false;
    }

    if (!(component instanceof IContainer container)) {
      return true;
    }

    for (int i = 0; i < container.getComponentCount(); i++) {
      final IComponent c = container.getComponent(i);
      if (!iterateImpl(c, visitor)) {
        return false;
      }
    }

    return true;
  }

  public static Set<String> collectUsedBundleNames(final IRootContainer rootContainer) {
    final Set<String> bundleNames = new HashSet<>();
    iterateStringDescriptors(rootContainer, new StringDescriptorVisitor<>() {
      @Override
      public boolean visit(final IComponent component, final StringDescriptor descriptor) {
        if (descriptor.getBundleName() != null) {
          bundleNames.add(descriptor.getBundleName());
        }
        return true;
      }
    });
    return bundleNames;
  }

  public static Locale[] collectUsedLocales(final Module module, final IRootContainer rootContainer) {
    final Set<Locale> locales = new HashSet<>();
    final PropertiesReferenceManager propManager = PropertiesReferenceManager.getInstance(module.getProject());
    for (String bundleName : collectUsedBundleNames(rootContainer)) {
      List<PropertiesFile> propFiles = propManager.findPropertiesFiles(module, bundleName.replace('/', '.'));
      for (PropertiesFile propFile : propFiles) {
        locales.add(propFile.getLocale());
      }
    }
    return locales.toArray(new Locale[0]);
  }

  public static void deleteRowOrColumn(final GuiEditor editor, final RadContainer container,
                                       final int[] cellsToDelete, final boolean isRow) {
    Arrays.sort(cellsToDelete);
    final int[] cells = ArrayUtil.reverseArray(cellsToDelete);
    if (!editor.ensureEditable()) {
      return;
    }

    Runnable runnable = () -> {
      if (!GridChangeUtil.canDeleteCells(container, cells, isRow)) {
        Set<RadComponent> componentsInColumn = new HashSet<>();
        for (RadComponent component : container.getComponents()) {
          GridConstraints c = component.getConstraints();
          for (int cell : cells) {
            if (c.contains(isRow, cell)) {
              componentsInColumn.add(component);
              break;
            }
          }
        }

        if (!componentsInColumn.isEmpty()) {
          String message = isRow
                           ? UIDesignerBundle.message("delete.row.nonempty", componentsInColumn.size(), cells.length)
                           : UIDesignerBundle.message("delete.column.nonempty", componentsInColumn.size(), cells.length);

          final int rc = Messages.showYesNoDialog(editor, message,
                                                  isRow ? UIDesignerBundle.message("delete.row.title")
                                                        : UIDesignerBundle.message("delete.column.title"), Messages.getQuestionIcon());
          if (rc != Messages.YES) {
            return;
          }

          deleteComponents(componentsInColumn, false);
        }
      }

      for (int cell : cells) {
        container.getGridLayoutManager().deleteGridCells(container, cell, isRow);
      }
      editor.refreshAndSave(true);
    };
    CommandProcessor.getInstance().executeCommand(editor.getProject(), runnable,
                                                  isRow ? UIDesignerBundle.message("command.delete.row")
                                                        : UIDesignerBundle.message("command.delete.column"), null);
  }

  /**
   * @return id
   */
  public static String generateId(final RadRootContainer rootContainer) {
    while (true) {
      final String id = Integer.toString((int)(Math.random() * 1024 * 1024), 16);
      if (findComponent(rootContainer, id) == null) {
        return id;
      }
    }
  }

  /**
   * @return {@link GuiEditor} from the context. Can be {@code null}.
   */
  public static @Nullable GuiEditor getEditorFromContext(final @NotNull DataContext context) {
    final FileEditor editor = PlatformCoreDataKeys.FILE_EDITOR.getData(context);
    if (editor instanceof UIFormEditor) {
      return ((UIFormEditor)editor).getEditor();
    }
    else {
      return GuiEditor.DATA_KEY.getData(context);
    }
  }

  public static @Nullable GuiEditor getActiveEditor(@NotNull DataContext context) {
    FileEditor fileEditor = PlatformDataKeys.LAST_ACTIVE_FILE_EDITOR.getData(context);
    return fileEditor instanceof UIFormEditor ? ((UIFormEditor)fileEditor).getEditor() : null;
  }

  /**
   * @param component                topmost container where to find duplicate binding. In most cases
   *                                 it should be {@link GuiEditor#getRootContainer()}
   */
  public static boolean isBindingUnique(
    final IComponent componentToAssignBinding,
    final String binding,
    final IComponent component
  ) {
    return findComponentWithBinding(component, binding, componentToAssignBinding) == null;
  }

  public static @Nullable String buildResourceName(final PsiFile file) {
    PsiDirectory directory = file.getContainingDirectory();
    if (directory != null) {
      PsiPackage pkg = JavaDirectoryService.getInstance().getPackage(directory);
      String packageName = pkg != null ? pkg.getQualifiedName() : "";
      if (packageName.isEmpty()) {
        return file.getName();
      }
      return packageName.replace('.', '/') + '/' + file.getName();
    }
    return null;
  }

  public static @Nullable RadContainer getSelectionParent(final List<? extends RadComponent> selection) {
    RadContainer parent = null;
    for (RadComponent c : selection) {
      if (parent == null) {
        parent = c.getParent();
      }
      else if (parent != c.getParent()) {
        parent = null;
        break;
      }
    }
    return parent;
  }

  public static Rectangle getSelectionBounds(List<? extends RadComponent> selection) {
    int minRow = Integer.MAX_VALUE;
    int minCol = Integer.MAX_VALUE;
    int maxRow = 0;
    int maxCol = 0;

    for (RadComponent c : selection) {
      minRow = Math.min(minRow, c.getConstraints().getRow());
      minCol = Math.min(minCol, c.getConstraints().getColumn());
      maxRow = Math.max(maxRow, c.getConstraints().getRow() + c.getConstraints().getRowSpan());
      maxCol = Math.max(maxCol, c.getConstraints().getColumn() + c.getConstraints().getColSpan());
    }
    return new Rectangle(minCol, minRow, maxCol - minCol, maxRow - minRow);
  }

  public static boolean isComponentSwitchedInView(@NotNull RadComponent component) {
    while (component.getParent() != null) {
      if (!component.getParent().getLayoutManager().isSwitchedToChild(component.getParent(), component)) {
        return false;
      }
      component = component.getParent();
    }
    return true;
  }

  /**
   * Selects the component and ensures that the tabbed panes containing the component are
   * switched to the correct tab.
   *
   * @param component the component to select. @return true if the component is enclosed in at least one tabbed pane, false otherwise.
   */
  public static boolean selectComponent(final GuiEditor editor, final @NotNull RadComponent component) {
    boolean hasTab = false;
    RadComponent parent = component;
    while (parent.getParent() != null) {
      if (parent.getParent().getLayoutManager().switchContainerToChild(parent.getParent(), parent)) {
        hasTab = true;
      }
      parent = parent.getParent();
    }
    component.setSelected(true);
    editor.setSelectionLead(component);
    return hasTab;
  }

  public static void selectSingleComponent(final GuiEditor editor, final RadComponent component) {
    final RadContainer root = (RadContainer)getRoot(component);
    if (root == null) return;

    ComponentTreeBuilder builder = DesignerToolWindowManager.getInstance(editor).getComponentTreeBuilder();
    // this can return null if the click to select the control also requested to grab the focus -
    // the component tree will be instantiated after the event has been processed completely
    if (builder != null) {
      builder.beginUpdateSelection();
    }
    try {
      clearSelection(root);
      selectComponent(editor, component);
      editor.setSelectionAnchor(component);
      editor.scrollComponentInView(component);
    }
    finally {
      if (builder != null) {
        builder.endUpdateSelection();
      }
    }
  }

  public static void selectComponents(final GuiEditor editor, List<? extends RadComponent> components) {
    if (!components.isEmpty()) {
      RadComponent component = components.get(0);
      ComponentTreeBuilder builder = DesignerToolWindowManager.getInstance(editor).getComponentTreeBuilder();
      if (builder == null) {
        // race condition when handling event?
        return;
      }
      builder.beginUpdateSelection();
      try {
        clearSelection((RadContainer)getRoot(component));
        for (RadComponent aComponent : components) {
          selectComponent(editor, aComponent);
        }
      }
      finally {
        builder.endUpdateSelection();
      }
    }
  }

  public static boolean isDropOnChild(final DraggedComponentList draggedComponentList,
                                      final ComponentDropLocation location) {
    if (location.getContainer() == null) {
      return false;
    }

    for (RadComponent component : draggedComponentList.getComponents()) {
      if (isChild(location.getContainer(), component)) {
        return true;
      }
    }
    return false;
  }

  private static boolean isChild(RadContainer maybeChild, RadComponent maybeParent) {
    while (maybeChild != null) {
      if (maybeParent == maybeChild) {
        return true;
      }
      maybeChild = maybeChild.getParent();
    }
    return false;
  }

  public static PsiMethod findCreateComponentsMethod(final PsiClass aClass) {
    PsiElementFactory factory = JavaPsiFacade.getInstance(aClass.getProject()).getElementFactory();
    PsiMethod method;
    try {
      method = factory.createMethodFromText("void " + AsmCodeGenerator.CREATE_COMPONENTS_METHOD_NAME + "() {}",
                                            aClass);
    }
    catch (IncorrectOperationException e) {
      throw new RuntimeException(e);
    }
    return aClass.findMethodBySignature(method, true);
  }

  public static Object getNextSaveUndoGroupId(final Project project) {
    final GuiEditor guiEditor = DesignerToolWindowManager.getInstance(project).getActiveFormEditor();
    return guiEditor == null ? null : guiEditor.getNextSaveGroupId();
  }

  public static int adjustForGap(final RadContainer container, final int cellIndex, final boolean isRow, final int delta) {
    if (container.getGridLayoutManager().isGapCell(container, isRow, cellIndex)) {
      return cellIndex + delta;
    }
    return cellIndex;
  }

  public static int prevRow(final RadContainer container, final int row) {
    return adjustForGap(container, row - 1, true, -1);
  }

  public static int nextRow(final RadContainer container, final int row) {
    return adjustForGap(container, row + 1, true, 1);
  }

  public static int prevCol(final RadContainer container, final int col) {
    return adjustForGap(container, col - 1, false, -1);
  }

  public static int nextCol(final RadContainer container, final int col) {
    return adjustForGap(container, col + 1, false, 1);
  }

  public static @Nullable IButtonGroup findGroupForComponent(final IRootContainer radRootContainer, final @NotNull IComponent component) {
    for (IButtonGroup group : radRootContainer.getButtonGroups()) {
      if (ArrayUtil.contains(component.getId(), group.getComponentIds())) {
          return group;
      }
    }
    return null;
  }

  public static List<RadComponent> remapToActionTargets(final List<? extends RadComponent> selection) {
    ArrayList<RadComponent> result = new ArrayList<>(selection.size());
    for (int i = 0; i < selection.size(); i++) {
      final RadComponent c = selection.get(i);
      if (c.getParent() != null) {
        result.add(c.getParent().getActionTargetComponent(c));
      }
    }
    return result;
  }

  public static void showPopupUnderComponent(final JBPopup popup, final RadComponent selectedComponent) {
    // popup.showUnderneathOf() doesn't work on invisible components
    Rectangle rc = selectedComponent.getBounds();
    Point pnt = new Point(rc.x, rc.y + rc.height);
    popup.show(new RelativePoint(selectedComponent.getDelegee().getParent(), pnt));
  }

  public interface StringDescriptorVisitor<T extends IComponent> {
    boolean visit(T component, StringDescriptor descriptor);
  }


  public static void iterateStringDescriptors(final IComponent component,
                                              final StringDescriptorVisitor<? super IComponent> visitor) {
    iterate(component, new ComponentVisitor<>() {

      @Override
      public boolean visit(final IComponent component) {
        for (IProperty prop : component.getModifiedProperties()) {
          Object value = prop.getPropertyValue(component);
          if (value instanceof StringDescriptor) {
            if (!visitor.visit(component, (StringDescriptor)value)) {
              return false;
            }
          }
        }
        if (component.getParentContainer() instanceof ITabbedPane) {
          StringDescriptor tabTitle =
            ((ITabbedPane)component.getParentContainer()).getTabProperty(component, ITabbedPane.TAB_TITLE_PROPERTY);
          if (tabTitle != null && !visitor.visit(component, tabTitle)) {
            return false;
          }
          StringDescriptor tabToolTip =
            ((ITabbedPane)component.getParentContainer()).getTabProperty(component, ITabbedPane.TAB_TOOLTIP_PROPERTY);
          if (tabToolTip != null && !visitor.visit(component, tabToolTip)) {
            return false;
          }
        }
        if (component instanceof IContainer) {
          final StringDescriptor borderTitle = ((IContainer)component).getBorderTitle();
          if (borderTitle != null && !visitor.visit(component, borderTitle)) {
            return false;
          }
        }
        return true;
      }
    });
  }

  public static void clearSelection(final @NotNull RadContainer container) {
    container.setSelected(false);

    for (int i = 0; i < container.getComponentCount(); i++) {
      final RadComponent c = container.getComponent(i);
      if (c instanceof RadContainer) {
        clearSelection((RadContainer)c);
      }
      else {
        c.setSelected(false);
      }
    }
  }

  /**
   * Finds component with the specified {@code id} starting from the
   * {@code container}. The method goes recursively through the hierarchy
   * of components. Note, that if {@code container} itself has {@code id}
   * then the method immediately retuns it.
   *
   * @return the found component.
   */
  public static @Nullable IComponent findComponent(final @NotNull IComponent component, final @NotNull String id) {
    if (id.equals(component.getId())) {
      return component;
    }
    if (!(component instanceof IContainer uiContainer)) {
      return null;
    }

    for (int i = 0; i < uiContainer.getComponentCount(); i++) {
      final IComponent found = findComponent(uiContainer.getComponent(i), id);
      if (found != null) {
        return found;
      }
    }
    return null;
  }

  public static @Nullable PsiClass findClassToBind(final @NotNull Module module, final @NotNull String classToBindName) {
    try (AccessToken ignore = SlowOperations.knownIssue("IDEA-307701, EA-266779")) {
      return JavaPsiFacade.getInstance(module.getProject())
        .findClass(classToBindName.replace('$', '.'), module.getModuleWithDependenciesScope());
    }
  }

  public interface ComponentVisitor<Type extends IComponent> {
    /**
     * @return true if iteration should continue
     */
    boolean visit(Type component);
  }
}