// 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.uiDesigner.core.GridConstraints;
import com.intellij.uiDesigner.radComponents.RadAbstractGridLayoutManager;
import com.intellij.uiDesigner.radComponents.RadComponent;
import com.intellij.uiDesigner.radComponents.RadContainer;
import org.jetbrains.annotations.NotNull;

import java.awt.*;

public final class GridChangeUtil {
  private GridChangeUtil() {
  }

  public static void splitColumn(final RadContainer grid, final int columnIndex) {
    splitCell(grid, columnIndex, false);
  }

  public static void splitRow(final RadContainer grid, final int rowIndex) {
    splitCell(grid, rowIndex, true);
  }

  public static boolean isColumnEmpty(final RadContainer grid, final int columnIndex) {
    return canDeleteCell(grid, columnIndex, false) == CellStatus.Empty;
  }

  public static boolean isRowEmpty(final RadContainer grid, final int rowIndex) {
    return canDeleteCell(grid, rowIndex, true) == CellStatus.Empty;
  }

  /**
   * @param cellIndex column or row index, depending on isRow parameter; must be in the range 0..grid.get{Row|Column}Count()-1
   * @param isRow if true, row inserted, otherwise column
   * @param isBefore if true, row/column will be inserted before row/column with given index, otherwise after
   */
  public static void insertRowOrColumn(final RadContainer grid, final int cellIndex, final boolean isRow, final boolean isBefore) {
    check(grid, isRow, cellIndex);

    final RadAbstractGridLayoutManager oldLayout = grid.getGridLayoutManager();

    int beforeIndex = cellIndex;
    if (!isBefore) {
      // beforeIndex can actually be equal to get{Row|Column}Count an case we add row after the last row/column
      beforeIndex++;
    }

    final LayoutManager newLayout = oldLayout.copyLayout(grid.getLayout(), isRow ? 1 : 0, isRow ? 0 : 1);
    GridConstraints[] oldConstraints = copyConstraints(grid);

    for (int i=grid.getComponentCount() - 1; i >= 0; i--){
      final GridConstraints constraints = grid.getComponent(i).getConstraints();
      adjustConstraintsOnInsert(constraints, isRow, beforeIndex, 1);
    }

    grid.setLayout(newLayout);
    fireAllConstraintsChanged(grid, oldConstraints);
  }

  private static GridConstraints[] copyConstraints(RadContainer grid) {
    final GridConstraints[] gridConstraints = new GridConstraints[grid.getComponentCount()];
    for (int i = 0; i < grid.getComponentCount(); i++) {
      gridConstraints [i] = (GridConstraints) grid.getComponent(i).getConstraints().clone();
    }
    return gridConstraints;
  }

  private static void fireAllConstraintsChanged(RadContainer grid, GridConstraints[] oldConstraints) {
    for(int i=grid.getComponentCount()-1; i >= 0; i--) {
      grid.getComponent(i).fireConstraintsChanged(oldConstraints [i]);
    }
  }

  public static void adjustConstraintsOnInsert(final GridConstraints constraints, final boolean isRow, final int beforeIndex,
                                               final int count) {
    if (constraints.getCell(isRow) >= beforeIndex) {
      addToCell(constraints, isRow, count);
    }
    else if (isCellInsideComponent(constraints, isRow, beforeIndex)) {
      // component belongs to the cell being resized - increment component's span
      addToSpan(constraints, isRow, count);
    }
  }

  /**
   * @param cellIndex column or row index, depending on isRow parameter; must be in the range 0..grid.get{Row|Column}Count()-1
   * @param isRow if true, row is splitted, otherwise column
   */
  public static void splitCell(final RadContainer grid, final int cellIndex, final boolean isRow) {
    check(grid, isRow, cellIndex);

    int insertedCells = grid.getGridLayoutManager().insertGridCells(grid, cellIndex, isRow, false, false);

    for (int i=grid.getComponentCount() - 1; i >= 0; i--){
      final RadComponent component = grid.getComponent(i);
      final GridConstraints constraints = component.getConstraints();

      if (constraints.getCell(isRow) + constraints.getSpan(isRow) - 1 == cellIndex) {
        // component belongs to the cell being resized - increment component's span
        GridConstraints oldConstraints = (GridConstraints)constraints.clone();
        constraints.setSpan(isRow, constraints.getSpan(isRow) + insertedCells);
        component.fireConstraintsChanged(oldConstraints);
      }
    }
  }

  public enum CellStatus {
    Empty, Redundant, CanShift, Required
  }

  /**
   * @param cellIndex column or row index, depending on isRow parameter; must be in the range 0..grid.get{Row|Column}Count()-1
   * @param isRow if true, row is deleted, otherwise column
   * @return whether the specified column can be deleted
   */
 public static CellStatus canDeleteCell(final @NotNull RadContainer grid, final int cellIndex, final boolean isRow) {
    check(grid, isRow, cellIndex);

    // Do not allow to delete the single row/column
    if(isRow && grid.getGridRowCount() <= grid.getGridLayoutManager().getMinCellCount()) {
      return CellStatus.Required;
    }
    else if(!isRow && grid.getGridColumnCount() <= grid.getGridLayoutManager().getMinCellCount()) {
      return CellStatus.Required;
    }

    boolean haveComponents = false;
    boolean haveOrigins = false;
    boolean haveSingleSpan = false;
    for (int i = 0; i < grid.getComponentCount(); i++) {
      final GridConstraints constraints = grid.getComponent(i).getConstraints();
      final int cell = constraints.getCell(isRow);
      final int span = constraints.getSpan(isRow);

      if (cellIndex >= cell && cellIndex < cell+span) {
        haveComponents = true;
        if (cellIndex == cell) {
          haveOrigins = true;
          if (span == 1) {
            haveSingleSpan = true;
          }
        }
      }
    }
    if (haveSingleSpan)
      return CellStatus.Required;
    if (haveOrigins)
      return CellStatus.CanShift;
    if (haveComponents)
      return CellStatus.Redundant;
    return CellStatus.Empty;
  }

  public static boolean canDeleteCells(final RadContainer grid, final int[] cells, final boolean row) {
    // for multiple cells, we can't determine if deleting all cells will have a correct result
    for(int cell: cells) {
      CellStatus status = canDeleteCell(grid, cell, row);
      if (status != CellStatus.Empty) {
        if (cells.length == 1 && status == CellStatus.Redundant) {
          return true;
        }
        return false;
      }
    }
    return true;    
  }

  /**
   * @param cellIndex column or row index, depending on isRow parameter; must be in the range 0..grid.get{Row|Column}Count()-1
   * @param isRow if true, row is deleted, otherwise column
   */
  public static void deleteCell(final RadContainer grid, final int cellIndex, final boolean isRow) {
    check(grid, isRow, cellIndex);
    if (canDeleteCell(grid, cellIndex, isRow) == CellStatus.Required) {
      throw new IllegalArgumentException("cell cannot be deleted");
    }

    final RadAbstractGridLayoutManager oldLayout = grid.getGridLayoutManager();

    final LayoutManager newLayout = oldLayout.copyLayout(grid.getLayout(), isRow ? -1 : 0, isRow ? 0 : -1);
    GridConstraints[] oldConstraints = copyConstraints(grid);

    for (int i=grid.getComponentCount() - 1; i >= 0; i--){
      final GridConstraints constraints = grid.getComponent(i).getConstraints();

      if (constraints.getCell(isRow) > cellIndex) {
        // component starts after the cell being deleted - move it
        addToCell(constraints, isRow, -1);
      }
      else if (isCellInsideComponent(constraints, isRow, cellIndex)) {
        // component belongs to the cell being deleted - decrement component's span
        addToSpan(constraints, isRow, -1);
      }
    }

    grid.setLayout(newLayout);
    fireAllConstraintsChanged(grid, oldConstraints);
  }


  private static boolean isCellInsideComponent(final GridConstraints constraints, final boolean isRow, final int cellIndex) {
    final int cell = constraints.getCell(isRow);
    final int span = constraints.getSpan(isRow);
    return cell <= cellIndex && cellIndex <= cell + span - 1;
  }

  /**
   * check whether passed container is grid and cellIndex is in proper range
   */
  private static void check(@NotNull RadContainer grid, final boolean isRow, final int cellIndex){
    if (!grid.getLayoutManager().isGrid()){
      throw new IllegalArgumentException("container must be grid");
    }

    final int cellCount = isRow ? grid.getGridRowCount() : grid.getGridColumnCount();
    if (cellIndex == 0 && cellCount == 0) return;
    if (cellIndex < 0 || cellIndex >= cellCount) {
      throw new IllegalArgumentException("invalid index: " + cellIndex);
    }
  }

  private static void addToCell(final GridConstraints constraints, final boolean isRow, final int delta){
    if (isRow) {
      constraints.setRow(constraints.getRow() + delta);
    }
    else {
      constraints.setColumn(constraints.getColumn() + delta);
    }
  }

  private static void addToSpan(final GridConstraints constraints, final boolean isRow, final int delta){
    if (isRow) {
      constraints.setRowSpan(constraints.getRowSpan() + delta);
    }
    else {
      constraints.setColSpan(constraints.getColSpan() + delta);
    }
  }

  public static void moveCells(final RadContainer container, final boolean isRow, final int[] cellsToMove, int targetCell) {
    for(int i=0; i<cellsToMove.length; i++) {
      final int sourceCell = cellsToMove[i];
      moveCell(container, isRow, sourceCell, targetCell);
      if (sourceCell < targetCell) {
        for(int j=i+1; j<cellsToMove.length; j++) {
          cellsToMove [j]--;
        }
      }
      else {
        targetCell++;
      }
    }
  }

  public static void moveCell(final RadContainer container, final boolean isRow, final int sourceCell, int targetCell) {
    if (targetCell == sourceCell || targetCell == sourceCell+1) return;
    // if column moved to left - components inbetween move to right, and vice versa
    int delta = (sourceCell > targetCell) ? 1 : -1;
    int startCell = Math.min(sourceCell, targetCell);
    int endCell = Math.max(sourceCell, targetCell);
    if (sourceCell < targetCell) targetCell--;
    for(RadComponent c: container.getComponents()) {
      GridConstraints constraints = c.getConstraints();
      GridConstraints oldConstraints = (GridConstraints) constraints.clone();
      final int aCell = constraints.getCell(isRow);
      if (aCell == sourceCell) {
        constraints.setCell(isRow, targetCell);
      }
      else if (aCell >= startCell && aCell < endCell) {
        constraints.setCell(isRow, aCell + delta);
      }
      c.fireConstraintsChanged(oldConstraints);
    }
  }
}
