// 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.designSurface;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.uiDesigner.FormEditingUtil;
import com.intellij.uiDesigner.GridChangeUtil;
import com.intellij.uiDesigner.UIDesignerBundle;
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.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.*;


public class GridInsertLocation extends GridDropLocation {
  private static final Logger LOG = Logger.getInstance(GridInsertLocation.class);

  public static final int INSERT_ARROW_SIZE = 3;
  public static final int INSERT_RECT_MIN_SIZE = 15;  // should be larger than the insets increase on Shift

  private GridInsertMode myMode;

  private boolean mySpanInsertMode;

  public GridInsertLocation(final @NotNull RadContainer container,
                            final int row,
                            final int column,
                            final GridInsertMode mode) {
    super(container, row, column);
    myMode = mode;
    assert container.getLayoutManager().isGrid();
  }

  public GridInsertLocation normalize() {
    final RadAbstractGridLayoutManager gridManager = myContainer.getGridLayoutManager();

    if (myMode == GridInsertMode.RowBefore && myRow > 0) {
      myMode = GridInsertMode.RowAfter;
      myRow--;
    }
    else if (myMode == GridInsertMode.ColumnBefore && myColumn > 0) {
      myMode = GridInsertMode.ColumnAfter;
      myColumn--;
    }

    if (myMode == GridInsertMode.RowAfter && gridManager.isGapCell(myContainer, true, myRow)) {
      myRow--;
    }
    else if (myMode == GridInsertMode.RowBefore && gridManager.isGapCell(myContainer, true, myRow)) {
      myRow++;
    }
    else if (myMode == GridInsertMode.ColumnAfter && gridManager.isGapCell(myContainer, false, myColumn)) {
      myColumn--;
    }
    else if (myMode == GridInsertMode.ColumnBefore && gridManager.isGapCell(myContainer, false, myColumn)) {
      myColumn++;
    }

    return this;
  }

  public GridInsertMode getMode() {
    return myMode;
  }

  public void setSpanInsertMode(boolean spanInsertMode) {
    mySpanInsertMode = spanInsertMode;
  }

  private boolean isColumnInsert() {
    return myMode == GridInsertMode.ColumnAfter || myMode == GridInsertMode.ColumnBefore;
  }

  private boolean isRowInsert() {
    return myMode == GridInsertMode.RowAfter || myMode == GridInsertMode.RowBefore;
  }

  public boolean isInsert() {
    return isColumnInsert() || isRowInsert();
  }

  private int getInsertCell() {
    return isRowInsert() ? myRow : myColumn;
  }

  private int getOppositeCell() {
    return isRowInsert() ? myColumn : myRow;
  }

  private boolean isInsertAfter() {
    return myMode == GridInsertMode.ColumnAfter || myMode == GridInsertMode.RowAfter;
  }

  @Override public boolean canDrop(ComponentDragObject dragObject) {
    Rectangle rc = getDragObjectDimensions(dragObject, true);
    int size = isRowInsert() ? rc.width : rc.height;
    if (isInsertInsideComponent(size)) {
      LOG.debug("GridInsertLocation.canDrop()=false because insert inside component");
      return false;
    }

    // TODO[yole]: any other conditions to check here?
    LOG.debug("GridInsertLocation.canDrop()=true");
    return true;
  }

  private static boolean isSameCell(final ComponentDragObject dragObject, boolean isRow) {
    if (dragObject.getComponentCount() == 0) {
      return true;
    }
    int cell = isRow ? dragObject.getRelativeRow(0) : dragObject.getRelativeCol(0);
    for(int i=1; i<dragObject.getComponentCount(); i++) {
      int cell2 = isRow ? dragObject.getRelativeRow(i) : dragObject.getRelativeCol(i);
      if (cell2 != cell) {
        return false;
      }
    }
    return true;
  }

  private boolean isInsertInsideComponent(final int size) {
    int endColumn = getInsertCell();
    if (isInsertAfter()) endColumn++;
    int row = getOppositeCell();

    for(int r=row; r<row+size; r++) {
      for(int col = 0; col<endColumn; col++) {
        RadComponent component;
        if (isColumnInsert()) {
          component = RadAbstractGridLayoutManager.getComponentAtGrid(getContainer(), r, col);
        }
        else {
          component = RadAbstractGridLayoutManager.getComponentAtGrid(getContainer(), col, r);
        }

        if (component != null) {
          GridConstraints constraints = component.getConstraints();
          final boolean isRow = !isColumnInsert();
          if (constraints.getCell(isRow) + constraints.getSpan(isRow) > endColumn &&
              constraints.getSpan(isRow) > 1) {
            return true;
          }
        }
      }
    }
    return false;
  }

  @Override public void placeFeedback(FeedbackLayer feedbackLayer, ComponentDragObject dragObject) {
    final int insertCol = getColumn();
    final int insertRow = getRow();

    Rectangle feedbackRect = getGridFeedbackRect(dragObject, isColumnInsert(), isRowInsert(), false);
    if (feedbackRect == null) {
      feedbackLayer.removeFeedback();
      return;
    }
    Rectangle cellRect = getGridFeedbackCellRect(dragObject, isColumnInsert(), isRowInsert(), false);
    assert cellRect != null;

    final RadAbstractGridLayoutManager layoutManager = getContainer().getGridLayoutManager();
    int[] vGridLines = layoutManager.getVerticalGridLines(getContainer());
    int[] hGridLines = layoutManager.getHorizontalGridLines(getContainer());

    FeedbackPainter painter = (myMode == GridInsertMode.ColumnBefore ||
                               myMode == GridInsertMode.ColumnAfter)
                              ? VertInsertFeedbackPainter.INSTANCE
                              : HorzInsertFeedbackPainter.INSTANCE;
    Rectangle rc;

    Rectangle rcFeedback = null;
    if (dragObject.getComponentCount() == 1) {
      int lastColIndex = insertCol + dragObject.getColSpan(0);
      if (lastColIndex > vGridLines.length - 1) {
        lastColIndex = insertCol + 1;
      }
      
      int lastRowIndex = insertRow + dragObject.getRowSpan(0);
      if (lastRowIndex > hGridLines.length - 1) {
        lastRowIndex = insertRow + 1;
      }
      
      int cellWidth = vGridLines [lastColIndex] - vGridLines [insertCol];
      int cellHeight = hGridLines [lastRowIndex] - hGridLines [insertRow];
      RadComponent component = RadAbstractGridLayoutManager.getComponentAtGrid(getContainer(), insertRow, insertCol);
      if (component != null && mySpanInsertMode) {
        Rectangle bounds = component.getBounds();
        bounds.translate(-vGridLines [insertCol], -hGridLines [insertRow]);

        int spaceToRight = vGridLines [lastColIndex] - vGridLines [insertCol] - (bounds.x + bounds.width);
        int spaceBelow = hGridLines [lastRowIndex] - hGridLines [insertRow] - (bounds.y + bounds.height);
        if (myMode == GridInsertMode.RowBefore && bounds.y > INSERT_RECT_MIN_SIZE) {
          rcFeedback = new Rectangle(0, 0, cellWidth, bounds.y);
        }
        else if (myMode == GridInsertMode.RowAfter && spaceBelow > INSERT_RECT_MIN_SIZE) {
          rcFeedback = new Rectangle(0, bounds.y + bounds.height, cellWidth, spaceBelow);
        }
        else if (myMode == GridInsertMode.ColumnBefore && bounds.x > INSERT_RECT_MIN_SIZE) {
          rcFeedback = new Rectangle(0, 0, bounds.x, cellHeight);
        }
        else if (myMode == GridInsertMode.ColumnAfter && spaceToRight > INSERT_RECT_MIN_SIZE) {
          rcFeedback = new Rectangle(bounds.x + bounds.width, 0, spaceToRight, cellHeight);
        }

        if (rcFeedback != null) {
          boolean spanInsertMode = false;
          
          if (isRowInsert()) {
            int columns = layoutManager.getGridColumnCount(getContainer());
            for (int i = 0; i < columns; i++) {
              if (i != insertCol && RadAbstractGridLayoutManager.getComponentAtGrid(getContainer(), insertRow, i) != null) {
                spanInsertMode = true;
                break;
              }
            }
          } else {
            int rows = layoutManager.getGridRowCount(getContainer());
            for (int i = 0; i < rows; i++) {
              if (i != insertRow && RadAbstractGridLayoutManager.getComponentAtGrid(getContainer(), i, insertCol) != null) {
                spanInsertMode = true;
                break;
              }
            }
          }

          if (!spanInsertMode) {
            rcFeedback = null;
          }
        }

        if (rcFeedback != null) {
          rcFeedback.translate(vGridLines [insertCol], hGridLines [insertRow]);
        }
      }

      if (rcFeedback == null) {
        if (insertCol == layoutManager.getGridColumnCount(getContainer())-1 && myMode == GridInsertMode.ColumnAfter) {
          final Dimension initialSize = dragObject.getInitialSize(getContainer());
          int feedbackX = vGridLines [vGridLines.length-1] + layoutManager.getGapCellSize(myContainer, false);
          int remainingSize = getContainer().getDelegee().getWidth() - feedbackX;
          if (!dragObject.isHGrow() && remainingSize > initialSize.width) {
           if (dragObject.isVGrow() || initialSize.height > cellHeight) {
              rcFeedback = new Rectangle(feedbackX, hGridLines [insertRow], initialSize.width, cellHeight);
            }
            else {
              rcFeedback = new Rectangle(feedbackX, hGridLines [insertRow] + (cellHeight - initialSize.height)/2,
                                         initialSize.width, initialSize.height);
            }
          }
          else if (remainingSize >= 4) {
            rcFeedback = new Rectangle(feedbackX, hGridLines [insertRow], remainingSize, cellHeight);
          }
        }
        else if (insertRow == layoutManager.getGridRowCount(getContainer())-1 && myMode == GridInsertMode.RowAfter) {
          final Dimension initialSize = dragObject.getInitialSize(getContainer());
          int feedbackY = hGridLines [hGridLines.length-1] + layoutManager.getGapCellSize(myContainer, true);
          int remainingSize = getContainer().getDelegee().getHeight() - feedbackY;
          if (!dragObject.isVGrow() && remainingSize > initialSize.height) {
            rcFeedback = new Rectangle(vGridLines [insertCol], feedbackY, cellWidth, initialSize.height);
          }
          else if (remainingSize >= 4) {
            rcFeedback = new Rectangle(vGridLines [insertCol], feedbackY, cellWidth, remainingSize);
          }
        }
      }
    }

    if (rcFeedback != null) {
      feedbackLayer.putFeedback(getContainer().getDelegee(), rcFeedback, getInsertFeedbackTooltip());
      return;
    }

    rc = getInsertFeedbackPosition(myMode, getContainer(), cellRect, feedbackRect);
    feedbackLayer.putFeedback(getContainer().getDelegee(), rc, painter, getInsertFeedbackTooltip());
  }

  private String getInsertFeedbackTooltip() {
    int displayRow = myRow + getContainer().getGridLayoutManager().getCellIndexBase();
    int displayColumn = myColumn + getContainer().getGridLayoutManager().getCellIndexBase();
    String displayName = getContainer().getDisplayName();
    return switch (myMode) {
      case ColumnBefore -> UIDesignerBundle.message("insert.feedback.before.col", displayName, displayRow, displayColumn);
      case ColumnAfter -> UIDesignerBundle.message("insert.feedback.after.col", displayName, displayRow, displayColumn);
      case RowBefore -> UIDesignerBundle.message("insert.feedback.before.row", displayName, displayColumn, displayRow);
      case RowAfter -> UIDesignerBundle.message("insert.feedback.after.row", displayName, displayColumn, displayRow);
    };
  }

  public static Rectangle getInsertFeedbackPosition(final GridInsertMode mode, final RadContainer container, final Rectangle cellRect,
                                                    final Rectangle feedbackRect) {
    final RadAbstractGridLayoutManager manager = container.getGridLayoutManager();
    int[] vGridLines = manager.getVerticalGridLines(container);
    int[] hGridLines = manager.getHorizontalGridLines(container);

    Rectangle rc = feedbackRect;
    int w=4;
    switch (mode) {
      case ColumnBefore -> {
        rc = new Rectangle(vGridLines[cellRect.x] - w, feedbackRect.y - INSERT_ARROW_SIZE,
                           2 * w, feedbackRect.height + 2 * INSERT_ARROW_SIZE);
        if (cellRect.x > 0 && manager.isGapCell(container, false, cellRect.x - 1)) {
          rc.translate(-(vGridLines[cellRect.x] - vGridLines[cellRect.x - 1]) / 2, 0);
        }
      }
      case ColumnAfter -> {
        rc = new Rectangle(vGridLines[cellRect.x + cellRect.width + 1] - w, feedbackRect.y - INSERT_ARROW_SIZE,
                           2 * w, feedbackRect.height + 2 * INSERT_ARROW_SIZE);
        if (cellRect.x < manager.getGridColumnCount(container) - 1 && manager.isGapCell(container, false, cellRect.x + 1)) {
          rc.translate((vGridLines[cellRect.x + 2] - vGridLines[cellRect.x + 1]) / 2, 0);
        }
      }
      case RowBefore -> {
        rc = new Rectangle(feedbackRect.x - INSERT_ARROW_SIZE, hGridLines[cellRect.y] - w,
                           feedbackRect.width + 2 * INSERT_ARROW_SIZE, 2 * w);
        if (cellRect.y > 0 && manager.isGapCell(container, true, cellRect.y - 1)) {
          rc.translate(0, -(hGridLines[cellRect.y] - hGridLines[cellRect.y - 1]) / 2);
        }
      }
      case RowAfter -> {
        rc = new Rectangle(feedbackRect.x - INSERT_ARROW_SIZE, hGridLines[cellRect.y + cellRect.height + 1] - w,
                           feedbackRect.width + 2 * INSERT_ARROW_SIZE, 2 * w);
        if (cellRect.y < manager.getGridRowCount(container) - 1 && manager.isGapCell(container, true, cellRect.y + 1)) {
          rc.translate(0, (hGridLines[cellRect.y + 2] - hGridLines[cellRect.y + 1]) / 2);
        }
      }
    }

    return rc;
  }


  @Override
  public void processDrop(final GuiEditor editor,
                          final RadComponent[] components,
                          final GridConstraints @Nullable [] constraintsToAdjust,
                          final ComponentDragObject dragObject) {
    int row = getRow();
    int col = getColumn();
    RadContainer container = getContainer();
    boolean canGrow = isRowInsert() ? dragObject.isVGrow() : dragObject.isHGrow();
    int cell = isRowInsert() ? getRow() : getColumn();

    int cellsToInsert = 1;
    if (components.length > 0) {
      int cellSize = container.getGridCellCount(isRowInsert());
        Rectangle rc = getDragObjectDimensions(dragObject, cell < cellSize - 1);
        int size = isRowInsert() ? rc.height : rc.width;
        if (size > 0) {
          cellsToInsert = size;
      }
    }

    GridSpanInsertProcessor spanInsertProcessor =
      mySpanInsertMode && dragObject.getComponentCount() == 1 ? new GridSpanInsertProcessor(container, getRow(), getColumn(), myMode,
                                                                                            dragObject) : null;

    int newCell = insertGridCells(container, cell, cellsToInsert, canGrow, isRowInsert(), !isInsertAfter(), constraintsToAdjust);
    if (isRowInsert()) {
      row = newCell;
    }
    else {
      col = newCell;
    }

    if (components.length > 0) {
      if (spanInsertProcessor != null) {
        spanInsertProcessor.doBefore(newCell);
      }

      dropIntoGrid(container, components, row, col, dragObject);

      if (spanInsertProcessor != null) {
        spanInsertProcessor.doAfter(newCell);
      }
    }
  }

  private static int insertGridCells(RadContainer container, int cell, int cellsToInsert, boolean canGrow, boolean isRow, boolean isBefore,
                                     GridConstraints[] constraintsToAdjust) {
    int insertedCells = 1;
    for(int i=0; i<cellsToInsert; i++) {
      insertedCells = container.getGridLayoutManager().insertGridCells(container, cell, isRow, isBefore, canGrow);
    }
    // for insert after, shift only by one cell + possibly one gap cell, not by entire number of insertions
    if (!isBefore) {
      cell += insertedCells;
    }
    checkAdjustConstraints(constraintsToAdjust, isRow, cell, insertedCells);
    return cell;
  }

  private static void checkAdjustConstraints(final GridConstraints @Nullable [] constraintsToAdjust,
                                             final boolean isRow,
                                             final int index, final int count) {
    if (constraintsToAdjust != null) {
      for(GridConstraints constraints: constraintsToAdjust) {
        GridChangeUtil.adjustConstraintsOnInsert(constraints, isRow, index, count);
      }
    }
  }


  @Override
  public @NonNls String toString() {
    return "GridInsertLocation(" + myMode.toString() + ", row=" + getRow() + ", col=" + getColumn() + ")";
  }

  @Override
  public @Nullable ComponentDropLocation getAdjacentLocation(Direction direction) {
    if (isRowInsert()) {
      if (direction == Direction.RIGHT) {
        if (getColumn() < myContainer.getGridColumnCount()-1) {
          return new GridInsertLocation(myContainer, getRow(), FormEditingUtil.adjustForGap(myContainer, getColumn()+1, false, 1), getMode());
        }
        return new GridInsertLocation(myContainer, getRow(), getColumn(), GridInsertMode.ColumnAfter);
      }
      if (direction == Direction.LEFT) {
        if (getColumn() > 0) {
          return new GridInsertLocation(myContainer, getRow(), FormEditingUtil.adjustForGap(myContainer, getColumn()-1, false, -1), getMode());
        }
        return new GridInsertLocation(myContainer, getRow(), getColumn(), GridInsertMode.ColumnBefore);
      }
      if (direction == Direction.DOWN || direction == Direction.UP) {
        int adjRow = (myMode == GridInsertMode.RowAfter) ? getRow() : getRow()-1;
        if (direction == Direction.DOWN && adjRow+1 < myContainer.getGridRowCount()) {
          return new GridDropLocation(myContainer, FormEditingUtil.adjustForGap(myContainer, adjRow+1, true, 1), getColumn());
        }
        if (direction == Direction.UP && adjRow >= 0) {
          return new GridDropLocation(myContainer, FormEditingUtil.adjustForGap(myContainer, adjRow, true, -1), getColumn());
        }
        return getLocationAtParent(direction);
      }
    }
    else {
      if (direction == Direction.DOWN) {
        if (getRow() < myContainer.getGridRowCount()-1) {
          return new GridInsertLocation(myContainer, FormEditingUtil.adjustForGap(myContainer, getRow()+1, true, 1), getColumn(), getMode());
        }
        return new GridInsertLocation(myContainer, getRow(), getColumn(), GridInsertMode.RowAfter);
      }
      if (direction == Direction.UP) {
        if (getRow() > 0) {
          return new GridInsertLocation(myContainer, FormEditingUtil.adjustForGap(myContainer, getRow()-1, true, -1), getColumn(), getMode());
        }
        return new GridInsertLocation(myContainer, getRow(), getColumn(), GridInsertMode.RowBefore);
      }
      if (direction == Direction.LEFT || direction == Direction.RIGHT) {
        int adjCol = (myMode == GridInsertMode.ColumnAfter) ? getColumn() : getColumn()-1;
        if (direction == Direction.RIGHT && adjCol+1 < myContainer.getGridColumnCount()) {
          return new GridDropLocation(myContainer, getRow(), FormEditingUtil.adjustForGap(myContainer, adjCol+1, false, 1));
        }
        if (direction == Direction.LEFT && adjCol >= 0) {
          return new GridDropLocation(myContainer, getRow(), FormEditingUtil.adjustForGap(myContainer, adjCol, false, -1));
        }
        return getLocationAtParent(direction);
      }
    }
    return null;
  }

  private ComponentDropLocation getLocationAtParent(final Direction direction) {
    final RadContainer parent = myContainer.getParent();
    if (parent.getLayoutManager().isGrid()) {
      final GridConstraints c = myContainer.getConstraints();
      return switch (direction) {
        case LEFT -> new GridInsertLocation(parent, c.getRow(), c.getColumn(), GridInsertMode.ColumnBefore);
        case RIGHT -> new GridInsertLocation(parent, c.getRow(), c.getColumn() + c.getColSpan() - 1, GridInsertMode.ColumnAfter);
        case UP -> new GridInsertLocation(parent, c.getRow(), c.getColumn(), GridInsertMode.RowBefore);
        case DOWN -> new GridInsertLocation(parent, c.getRow() + c.getRowSpan() - 1, c.getColumn(), GridInsertMode.RowAfter);
      };
    }
    return null;
  }
}
