// 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.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.Couple;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.core.GridLayoutManager;
import com.intellij.uiDesigner.core.Util;
import com.intellij.uiDesigner.designSurface.GuiEditor;
import com.intellij.uiDesigner.palette.ComponentItem;
import com.intellij.uiDesigner.palette.Palette;
import com.intellij.uiDesigner.radComponents.*;
import com.intellij.uiDesigner.shared.XYLayoutManager;

import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;


public final class GridBuildUtil {
  private static final Logger LOG = Logger.getInstance(GridBuildUtil.class);

  private static final int HORIZONTAL_GRID = 1;
  private static final int VERTICAL_GRID = 2;
  private static final int GRID = 3;
  /**
   * TODO[anton,vova]: most likely should be equal to "xy grid step" when available
   */
  private static final int GRID_TREMOR = 5;

  private GridBuildUtil() {
  }

  public static void breakGrid(final GuiEditor editor) {
    final ArrayList<RadComponent> selection = FormEditingUtil.getSelectedComponents(editor);
    if (selection.size() != 1){
      return;
    }
    if (!(selection.get(0) instanceof RadContainer container)) {
      return;
    }
    if (
      container instanceof RadScrollPane ||
      container instanceof RadSplitPane ||
      container instanceof RadTabbedPane
    ){
      return;
    }

    final RadContainer parent = container.getParent();

    if (parent instanceof RadRootContainer) {
      editor.getRootContainer().setMainComponentBinding(container.getBinding());
    }

    // XY can be broken only if its parent is also XY.
    // In other words, breaking of XY is a deletion of unnecessary intermediate panel
    if (container.isXY() && !parent.isXY()) {
      return;
    }

    if (parent != null && parent.isXY()) {
      // parent is XY
      // put the contents of the container into 'parent' and remove 'container'

      final int dx = container.getX();
      final int dy = container.getY();

      while (container.getComponentCount() > 0) {
        final RadComponent component = container.getComponent(0);
        component.shift(dx, dy);
        parent.addComponent(component);
      }

      parent.removeComponent(container);
    }
    else {
      // container becomes XY
      final XYLayoutManager xyLayout = new XYLayoutManagerImpl();
      container.setLayout(xyLayout);
      xyLayout.setPreferredSize(container.getSize());
    }

    editor.refreshAndSave(true);
  }

  public static void convertToVerticalGrid(final GuiEditor editor){
    convertToGridImpl(editor, VERTICAL_GRID);
  }

  public static void convertToHorizontalGrid(final GuiEditor editor){
    convertToGridImpl(editor, HORIZONTAL_GRID);
  }

  public static void convertToGrid(final GuiEditor editor){
    convertToGridImpl(editor, GRID);
  }

  private static void convertToGridImpl(final GuiEditor editor, final int gridType) {
    final boolean createNewContainer;

    final RadContainer parent;
    final RadComponent[] componentsToConvert;
    {
      final ArrayList<RadComponent> selection = FormEditingUtil.getSelectedComponents(editor);
      if (selection.isEmpty()) {
        // root container selected
        final RadRootContainer rootContainer = editor.getRootContainer();
        if (rootContainer.getComponentCount() < 2) {
          // nothing to convert
          return;
        }

        componentsToConvert = new RadComponent[rootContainer.getComponentCount()];
        for (int i = 0; i < componentsToConvert.length; i++) {
          componentsToConvert[i] = rootContainer.getComponent(i);
        }
        parent = rootContainer;
        createNewContainer = true;
      }
      else if (selection.size() == 1 && selection.get(0) instanceof RadContainer) {
        parent = (RadContainer)selection.get(0);
        componentsToConvert = new RadComponent[parent.getComponentCount()];
        for (int i = 0; i < componentsToConvert.length; i++) {
          componentsToConvert[i] = parent.getComponent(i);
        }
        createNewContainer = false;
      }
      else {
        componentsToConvert = selection.toArray(RadComponent.EMPTY_ARRAY);
        parent = selection.get(0).getParent();
        createNewContainer = true;
      }
    }

    if (!parent.isXY()) {
      // only components in XY can be layed out in grid
      return;
    }
    for (int i = 1; i < componentsToConvert.length; i++) {
      final RadComponent component = componentsToConvert[i];
      if (component.getParent() != parent) {
        return;
      }
    }

    final GridLayoutManager gridLayoutManager;
    if (componentsToConvert.length == 0) {
      // we convert empty XY panel to grid
      gridLayoutManager = new GridLayoutManager(1,1);
    }
    else {
      if (gridType == VERTICAL_GRID) {
        gridLayoutManager = createOneDimensionGrid(componentsToConvert, true);
      }
      else if (gridType == HORIZONTAL_GRID) {
        gridLayoutManager = createOneDimensionGrid(componentsToConvert, false);
      }
      else if (gridType == GRID) {
        gridLayoutManager = createTwoDimensionGrid(componentsToConvert);
      }
      else {
        throw new IllegalArgumentException("invalid grid type: " + gridType);
      }
    }

    for (final RadComponent component : componentsToConvert) {
      if (component instanceof RadContainer) {
        final LayoutManager layout = ((RadContainer)component).getLayout();
        if (layout instanceof XYLayoutManager) {
          ((XYLayoutManager)layout).setPreferredSize(component.getSize());
        }
      }
    }

    if (createNewContainer) {
      // we should create a new panel

      final Module module = editor.getModule();
      final ComponentItem panelItem = Palette.getInstance(editor.getProject()).getPanelItem();
      final RadContainer newContainer = new RadContainer(editor, FormEditingUtil.generateId(editor.getRootContainer()));
      newContainer.setLayout(gridLayoutManager);
      newContainer.init(editor, panelItem);

      for (RadComponent componentToConvert : componentsToConvert) {
        newContainer.addComponent(componentToConvert);
      }

      final Point topLeftPoint = getTopLeftPoint(componentsToConvert);
      newContainer.setLocation(topLeftPoint);

      final Point bottomRightPoint = getBottomRightPoint(componentsToConvert);
      final Dimension size = new Dimension(bottomRightPoint.x - topLeftPoint.x, bottomRightPoint.y - topLeftPoint.y);
      Util.adjustSize(newContainer.getDelegee(), newContainer.getConstraints(), size);
      newContainer.getDelegee().setSize(size);

      parent.addComponent(newContainer);

      FormEditingUtil.clearSelection(editor.getRootContainer());
      newContainer.setSelected(true);

      // restore binding of main component
      {
        final String mainComponentBinding = editor.getRootContainer().getMainComponentBinding();
        if (mainComponentBinding != null && parent instanceof RadRootContainer) {
          newContainer.setBinding(mainComponentBinding);
          editor.getRootContainer().setMainComponentBinding(null);
        }
      }
    }
    else {
      // convert entire 'parent' to grid

      parent.setLayout(gridLayoutManager);

      FormEditingUtil.clearSelection(editor.getRootContainer());
      parent.setSelected(true);
    }

    editor.refreshAndSave(true);
  }

  private static GridLayoutManager createOneDimensionGrid(final RadComponent[] selection, final boolean isVertical){
    Arrays.sort(
      selection,
      (o1, o2) -> {
        final Rectangle bounds1 = o1.getBounds();
        final Rectangle bounds2 = o2.getBounds();

        if (isVertical) {
          return (bounds1.y + bounds1.height / 2) - (bounds2.y + bounds2.height / 2);
        }
        else {
          return (bounds1.x + bounds1.width / 2) - (bounds2.x + bounds2.width / 2);
        }
      }
    );

    for (int i = 0; i < selection.length; i++) {
      final RadComponent component = selection[i];
      final GridConstraints constraints = component.getConstraints();
      if (isVertical) {
        constraints.setRow(i);
        constraints.setColumn(0);
      }
      else {
        constraints.setRow(0);
        constraints.setColumn(i);
      }
      constraints.setRowSpan(1);
      constraints.setColSpan(1);
    }

    final GridLayoutManager gridLayoutManager;
    if (isVertical) {
      gridLayoutManager = new GridLayoutManager(selection.length, 1);
    }
    else {
      gridLayoutManager = new GridLayoutManager(1, selection.length);
    }
    return gridLayoutManager;
  }

  /**
   * @param x array of {@code X} coordinates of components that should be layed out in a grid.
   * This is input/output parameter.
   *
   * @param y array of {@code Y} coordinates of components that should be layed out in a grid.
   * This is input/output parameter.
   *
   * @param rowSpans output parameter.
   *
   * @param colSpans output parameter.
   *
   * @return pair that says how many (rows, columns) are in the composed grid.
   */
  public static Couple<Integer> layoutInGrid(
    final int[] x,
    final int[] y,
    final int[] rowSpans,
    final int[] colSpans
  ){
    LOG.assertTrue(x.length == y.length);
    LOG.assertTrue(y.length == colSpans.length);
    LOG.assertTrue(colSpans.length == rowSpans.length);

    for (int i = 0; i < x.length; i++) {
      colSpans[i] = Math.max(colSpans[i], 1);
      rowSpans[i] = Math.max(rowSpans[i], 1);

      if (colSpans[i] > GRID_TREMOR * 4) {
        colSpans[i] -= GRID_TREMOR * 2;
        x[i] += GRID_TREMOR;
      }
      if (rowSpans[i] > GRID_TREMOR * 4) {
        rowSpans[i] -= GRID_TREMOR * 2;
        y[i] += GRID_TREMOR;
      }
    }


    return Couple.of(
      Integer.valueOf(Util.eliminate(y, rowSpans, null)),
      Integer.valueOf(Util.eliminate(x, colSpans, null))
    );
  }

  private static GridLayoutManager createTwoDimensionGrid(final RadComponent[] selection){
    final int[] x = new int[selection.length];
    final int[] y = new int[selection.length];
    final int[] colSpans = new int[selection.length];
    final int[] rowSpans = new int[selection.length];

    for (int i = selection.length - 1; i >= 0; i--) {
      x[i] = selection[i].getX();
      y[i] = selection[i].getY();
      rowSpans[i] = selection[i].getHeight();
      colSpans[i] = selection[i].getWidth();
    }

    final Couple<Integer> pair = layoutInGrid(x, y, rowSpans, colSpans);
    for (int i = 0; i < selection.length; i++) {
      final RadComponent component = selection[i];
      final GridConstraints constraints = component.getConstraints();

      constraints.setRow(y[i]);
      constraints.setRowSpan(rowSpans[i]);

      constraints.setColumn(x[i]);
      constraints.setColSpan(colSpans[i]);
    }

    return new GridLayoutManager(pair.first.intValue(), pair.second.intValue());
  }

  /**
   * Find top left point of component group, i.e. (minimum X of all components; minimum Y of all components)
   * @param components array should contain at least one element
   */
  private static Point getTopLeftPoint(final RadComponent[] components){
    LOG.assertTrue(components.length > 0);

    final Point point = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
    for (final RadComponent component : components) {
      point.x = Math.min(component.getX(), point.x);
      point.y = Math.min(component.getY(), point.y);
    }

    return point;
  }

  /**
   * Find bottom right point of component group, i.e. (maximum (x + width) of all components; maximum (y + height) of all components)
   * @param components array should contain at least one element
   */
  private static Point getBottomRightPoint(final RadComponent[] components){
    LOG.assertTrue(components.length > 0);

    final Point point = new Point(Integer.MIN_VALUE, Integer.MIN_VALUE);
    for (final RadComponent component : components) {
      point.x = Math.max(component.getX() + component.getWidth(), point.x);
      point.y = Math.max(component.getY() + component.getHeight(), point.y);
    }

    return point;
  }
}
