/**
 * Layout strategies for positioning nodes in the workflow
 * Contains the core algorithms for node placement
 */

import { Node, Edge } from '../store/store';
import {
  LayoutPosition,
  BoundingBox,
  GraphData,
  LAYOUT_CONFIG,
  isPositionOccupied,
  findNonCollidingPosition,
  calculateIdealXPosition,
} from './layoutHelpers';

// ============ Flow-based Layout Strategy ============
/**
 * Process nodes using depth-first search from start nodes
 */
export function processFlowLayout(
  startNodes: Node[],
  nodes: Node[],
  graphData: GraphData
): Map<string, LayoutPosition> {
  const positioned = new Set<string>();
  const nodePositions = new Map<string, LayoutPosition>();
  const rowColumns = new Map<number, number>(); // row -> next available column
  let maxRow = 0;
  
  // Process each event/start node and its complete flow
  startNodes.forEach((startNode, eventIndex) => {
    if (positioned.has(startNode.id)) return;
    
    // Use DFS to process the complete flow from this event
    const stack = [{
      nodeId: startNode.id,
      row: 0,
      column: eventIndex * 2, // Space out different event flows
      parent: null as string | null
    }];
    
    while (stack.length > 0) {
      const { nodeId, row, column, parent: _parent } = stack.pop()!;
      
      if (positioned.has(nodeId)) {
        // Node already positioned, check if we need to update its position
        const existingPos = nodePositions.get(nodeId);
        if (existingPos && existingPos.row < row) {
          // Move to deeper row to maintain flow
          nodePositions.set(nodeId, { row, column: existingPos.column });
        }
        continue;
      }
      
      // Get next available column for this row
      let assignedColumn = column;
      if (rowColumns.has(row)) {
        const nextCol = rowColumns.get(row)!;
        assignedColumn = Math.max(column, nextCol);
        rowColumns.set(row, assignedColumn + 1);
      } else {
        rowColumns.set(row, assignedColumn + 1);
      }
      
      // Position the node
      nodePositions.set(nodeId, { row, column: assignedColumn });
      positioned.add(nodeId);
      maxRow = Math.max(maxRow, row);
      
      // Get children in the order they appear in edges
      const children = graphData.adjacencyMap.get(nodeId) || [];
      
      // Check if current node is a gateway
      const currentNode = nodes.find(n => n.id === nodeId);
      const isGateway = currentNode && (
        currentNode.type === 'gateway' || 
        currentNode.data?.nodeType === 'gateway'
      );
      
      // Process children in reverse order (for stack-based DFS)
      // to maintain the original order when popped
      [...children].reverse().forEach((childId, index) => {
        if (!positioned.has(childId)) {
          let childColumn: number;
          let childRow: number;
          
          if (isGateway) {
            // Gateway successors: shift right and go slightly deeper for visual clarity
            childColumn = children.length > 1 
              ? assignedColumn + 1 + index - Math.floor(children.length / 2)
              : assignedColumn + 1; // Always shift right for gateway successors
            childRow = row + LAYOUT_CONFIG.GATEWAY_VERTICAL_OFFSET;
          } else {
            // Normal successors
            childColumn = children.length > 1 
              ? assignedColumn + index - Math.floor(children.length / 2)
              : assignedColumn;
            childRow = row + 1;
          }
          
          stack.push({
            nodeId: childId,
            row: childRow,
            column: childColumn,
            parent: nodeId
          });
        }
      });
    }
  });
  
  // Handle any remaining unpositioned nodes
  nodes.forEach(node => {
    if (!positioned.has(node.id)) {
      const row = maxRow + 1;
      const column = rowColumns.get(row) || 0;
      nodePositions.set(node.id, { row, column });
      rowColumns.set(row, column + 1);
      positioned.add(node.id);
    }
  });
  
  return nodePositions;
}

// ============ Position Conversion ============
/**
 * Convert row/column positions to actual x/y coordinates
 */
export function convertPositionsToCoordinates(
  nodes: Node[],
  nodePositions: Map<string, LayoutPosition>,
  startPos: { x: number; y: number },
  lockedNodeBounds: BoundingBox[]
): Node[] {
  const layoutNodes = [...nodes];
  const updatedBounds = [...lockedNodeBounds];
  
  layoutNodes.forEach(node => {
    const pos = nodePositions.get(node.id);
    if (pos && node.data?.locked !== true) { // Only adjust unlocked nodes
      const targetX = startPos.x + (pos.column * LAYOUT_CONFIG.HORIZONTAL_SPACING);
      const targetY = startPos.y + (pos.row * LAYOUT_CONFIG.VERTICAL_SPACING);
      
      // Find non-colliding position
      const finalPosition = findNonCollidingPosition(
        targetX,
        targetY,
        updatedBounds,
        startPos
      );
      
      node.position = finalPosition;
      
      // Add this positioned node to collision bounds to prevent overlaps with subsequent nodes
      updatedBounds.push({
        id: node.id,
        x1: finalPosition.x - LAYOUT_CONFIG.NODE_WIDTH / 2,
        y1: finalPosition.y - LAYOUT_CONFIG.NODE_HEIGHT / 2,
        x2: finalPosition.x + LAYOUT_CONFIG.NODE_WIDTH / 2,
        y2: finalPosition.y + LAYOUT_CONFIG.NODE_HEIGHT / 2
      });
    }
  });
  
  return layoutNodes;
}

// ============ Layout Optimization ============
/**
 * Group nodes by row for alignment optimization
 */
export function groupNodesByRow(
  nodePositions: Map<string, LayoutPosition>,
  nodes: Node[]
): Map<number, string[]> {
  const rowNodes = new Map<number, string[]>();
  
  nodePositions.forEach((pos, nodeId) => {
    const node = nodes.find(n => n.id === nodeId);
    if (node && node.data?.locked !== true) { // Only include unlocked nodes
      if (!rowNodes.has(pos.row)) {
        rowNodes.set(pos.row, []);
      }
      rowNodes.get(pos.row)!.push(nodeId);
    }
  });
  
  return rowNodes;
}

/**
 * Align nodes within rows to minimize edge crossings
 */
export function optimizeRowAlignment(
  nodes: Node[],
  edges: Edge[],
  rowNodes: Map<number, string[]>,
  lockedNodeBounds: BoundingBox[]
): void {
  rowNodes.forEach((nodeIds, _row) => {
    if (nodeIds.length <= 1) return;
    
    // Calculate ideal positions based on connected nodes
    nodeIds.forEach(nodeId => {
      const node = nodes.find(n => n.id === nodeId);
      if (!node || !node.position) return;
      
      const idealX = calculateIdealXPosition(nodeId, edges, nodes);
      
      // Apply gentle adjustment toward ideal position, but check for collisions
      if (idealX !== null) {
        const currentX = node.position.x;
        const adjustedX = currentX + (idealX - currentX) * LAYOUT_CONFIG.IDEAL_POSITION_WEIGHT;
        
        // Check if the adjusted position would collide with locked nodes
        if (!isPositionOccupied(adjustedX, node.position.y, lockedNodeBounds)) {
          node.position.x = adjustedX;
        }
        // If collision would occur, keep the current position
      }
    });
    
    // Ensure minimum spacing between nodes in the same row
    enforceMinimumSpacing(nodeIds, nodes, lockedNodeBounds);
  });
}

/**
 * Ensure minimum spacing between nodes in the same row
 */
function enforceMinimumSpacing(
  nodeIds: string[],
  nodes: Node[],
  lockedNodeBounds: BoundingBox[]
): void {
  const sortedNodes = nodeIds
    .map(id => nodes.find(n => n.id === id))
    .filter((n): n is Node => n !== undefined && n.position !== undefined)
    .sort((a, b) => a.position!.x - b.position!.x);
  
  for (let i = 1; i < sortedNodes.length; i++) {
    const prevNode = sortedNodes[i - 1];
    const currNode = sortedNodes[i];
    
    if (!prevNode.position || !currNode.position) continue;
    
    const minDistance = LAYOUT_CONFIG.MIN_NODE_DISTANCE;
    
    if (currNode.position.x - prevNode.position.x < minDistance) {
      let newX = prevNode.position.x + minDistance;
      
      // Check if the new position would collide with locked nodes
      let attempts = 0;
      const maxAttempts = 10;
      while (isPositionOccupied(newX, currNode.position.y, lockedNodeBounds) && attempts < maxAttempts) {
        attempts++;
        newX += LAYOUT_CONFIG.HORIZONTAL_SPACING / 4; // Move further right
      }
      
      currNode.position.x = newX;
    }
  }
}