/**
 * Layout helper functions for auto-layout algorithm
 * Extracted from the monolithic autoLayout function for better maintainability
 */

import { Node, Edge } from '../store/store';
import { LAYOUT, NODE_DIMENSIONS } from '../constants/dimensions';

// ============ Type Definitions ============
export interface LayoutPosition {
  row: number;
  column: number;
}

export interface BoundingBox {
  x1: number;
  y1: number;
  x2: number;
  y2: number;
  id?: string;
}

interface LayoutBounds {
  x: number;
  y: number;
  width: number;
  height: number;
}

export interface GraphData {
  adjacencyMap: Map<string, string[]>;
  inDegree: Map<string, number>;
  outDegree: Map<string, number>;
}

// ============ Constants ============
export const LAYOUT_CONFIG = {
  NODE_WIDTH: NODE_DIMENSIONS.DEFAULT_WIDTH,
  NODE_HEIGHT: NODE_DIMENSIONS.DEFAULT_HEIGHT,
  HORIZONTAL_SPACING: 300,
  VERTICAL_SPACING: 200,
  COLLISION_PADDING: 25,
  MIN_NODE_DISTANCE: NODE_DIMENSIONS.DEFAULT_WIDTH + 50,
  GATEWAY_VERTICAL_OFFSET: 0.75, // 75% of normal distance for gateway children
  IDEAL_POSITION_WEIGHT: 0.3, // Weight for adjusting nodes toward ideal position
} as const;

// ============ Node Classification ============
/**
 * Separate nodes into locked and unlocked groups
 */
export function classifyNodes(nodes: Node[]): {
  lockedNodes: Node[];
  unlockedNodes: Node[];
} {
  const lockedNodes = nodes.filter(node => node.data?.locked === true);
  const unlockedNodes = nodes.filter(node => node.data?.locked !== true);
  return { lockedNodes, unlockedNodes };
}

/**
 * Find event/start nodes that should be positioned first
 */
export function findStartNodes(nodes: Node[], inDegree: Map<string, number>): Node[] {
  const eventNodes = nodes.filter(
    node => node.type === 'start' || 
            inDegree.get(node.id) === 0 ||
            (node.data?.plugin && node.data.plugin.includes('event'))
  ).sort((a, b) => {
    // Prioritize 'start' type nodes
    if (a.type === 'start' && b.type !== 'start') return -1;
    if (b.type === 'start' && a.type !== 'start') return 1;
    return 0;
  });
  
  // If no event nodes found, use nodes with no incoming edges
  const startNodes = eventNodes.length > 0 ? eventNodes : 
    nodes.filter(node => inDegree.get(node.id) === 0);
  
  return startNodes.length > 0 ? startNodes : (nodes.length > 0 ? [nodes[0]] : []);
}

// ============ Bounds Calculation ============
/**
 * Calculate bounding box for a set of nodes
 */
function calculateNodesBounds(nodes: Node[]): LayoutBounds | null {
  if (!nodes || nodes.length === 0) {
    return null;
  }

  let minX = Infinity, minY = Infinity;
  let maxX = -Infinity, maxY = -Infinity;

  nodes.forEach(node => {
    if (node.position) {
      minX = Math.min(minX, node.position.x);
      minY = Math.min(minY, node.position.y);
      maxX = Math.max(maxX, node.position.x);
      maxY = Math.max(maxY, node.position.y);
    }
  });

  if (minX === Infinity) {
    return null;
  }

  return {
    x: minX,
    y: minY,
    width: maxX - minX,
    height: maxY - minY
  };
}

/**
 * Calculate padded bounding box for locked nodes
 */
export function calculateLockedBounds(lockedNodes: Node[]): BoundingBox | null {
  if (lockedNodes.length === 0) return null;
  
  const bounds = calculateNodesBounds(lockedNodes);
  if (!bounds) return null;
  
  const padding = LAYOUT.LOCKED_ELEMENT_PADDING;
  return {
    x1: bounds.x - padding,
    y1: bounds.y - padding,
    x2: bounds.x + bounds.width + padding,
    y2: bounds.y + bounds.height + padding
  };
}

/**
 * Create collision bounds for individual nodes
 */
export function createNodeCollisionBounds(nodes: Node[]): BoundingBox[] {
  return nodes
    .map(node => {
      if (!node.position) return null;
      return {
        id: node.id,
        x1: node.position.x - LAYOUT_CONFIG.NODE_WIDTH / 2,
        y1: node.position.y - LAYOUT_CONFIG.NODE_HEIGHT / 2,
        x2: node.position.x + LAYOUT_CONFIG.NODE_WIDTH / 2,
        y2: node.position.y + LAYOUT_CONFIG.NODE_HEIGHT / 2
      };
    })
    .filter(bounds => bounds !== null) as BoundingBox[];
}

// ============ Collision Detection ============
/**
 * Check if a position conflicts with existing node bounds
 */
export function isPositionOccupied(
  x: number, 
  y: number, 
  lockedNodeBounds: BoundingBox[]
): boolean {
  const candidateBounds = {
    x1: x - LAYOUT_CONFIG.NODE_WIDTH / 2 - LAYOUT_CONFIG.COLLISION_PADDING,
    y1: y - LAYOUT_CONFIG.NODE_HEIGHT / 2 - LAYOUT_CONFIG.COLLISION_PADDING,
    x2: x + LAYOUT_CONFIG.NODE_WIDTH / 2 + LAYOUT_CONFIG.COLLISION_PADDING,
    y2: y + LAYOUT_CONFIG.NODE_HEIGHT / 2 + LAYOUT_CONFIG.COLLISION_PADDING
  };
  
  // Check overlap with all locked nodes (including their padding)
  for (const bounds of lockedNodeBounds) {
    const paddedBounds = {
      x1: bounds.x1 - LAYOUT_CONFIG.COLLISION_PADDING,
      y1: bounds.y1 - LAYOUT_CONFIG.COLLISION_PADDING,
      x2: bounds.x2 + LAYOUT_CONFIG.COLLISION_PADDING,
      y2: bounds.y2 + LAYOUT_CONFIG.COLLISION_PADDING
    };
    
    // Check if rectangles overlap using standard bounding box collision detection
    if (candidateBounds.x1 < paddedBounds.x2 && 
        candidateBounds.x2 > paddedBounds.x1 && 
        candidateBounds.y1 < paddedBounds.y2 && 
        candidateBounds.y2 > paddedBounds.y1) {
      return true;
    }
  }
  return false;
}

/**
 * Find a non-colliding position near the target position
 */
export function findNonCollidingPosition(
  targetX: number,
  targetY: number,
  lockedNodeBounds: BoundingBox[],
  _startPos: { x: number; y: number }
): { x: number; y: number } {
  let candidateX = targetX;
  let candidateY = targetY;
  let attempts = 0;
  const maxAttempts = 50;
  let deltaX = 0;
  let deltaY = 0;
  
  while (isPositionOccupied(candidateX, candidateY, lockedNodeBounds) && attempts < maxAttempts) {
    attempts++;
    
    if (attempts <= 10) {
      // First 10 attempts: try moving right
      deltaX += LAYOUT_CONFIG.HORIZONTAL_SPACING / 3;
      candidateX = targetX + deltaX;
    } else if (attempts <= 20) {
      // Next 10 attempts: try moving down
      deltaY += LAYOUT_CONFIG.VERTICAL_SPACING / 3;
      candidateY = targetY + deltaY;
      candidateX = targetX; // Reset X
    } else if (attempts <= 30) {
      // Next 10 attempts: try moving left
      deltaX -= LAYOUT_CONFIG.HORIZONTAL_SPACING / 3;
      candidateX = targetX + deltaX;
      candidateY = targetY; // Reset Y
    } else {
      // Final attempts: move both right and down progressively
      deltaX += LAYOUT_CONFIG.HORIZONTAL_SPACING / 4;
      deltaY += LAYOUT_CONFIG.VERTICAL_SPACING / 4;
      candidateX = targetX + deltaX;
      candidateY = targetY + deltaY;
    }
  }
  
  return { x: candidateX, y: candidateY };
}

// ============ Graph Building ============
/**
 * Build adjacency map and calculate in/out degrees for nodes
 */
export function buildGraphData(nodes: Node[], edges: Edge[]): GraphData {
  const adjacencyMap = new Map<string, string[]>();
  const inDegree = new Map<string, number>();
  const outDegree = new Map<string, number>();
  
  // Initialize maps for nodes
  nodes.forEach(node => {
    adjacencyMap.set(node.id, []);
    inDegree.set(node.id, 0);
    outDegree.set(node.id, 0);
  });

  // Build the graph from edges - maintaining order of edges
  edges.forEach(edge => {
    if (adjacencyMap.has(edge.source) && adjacencyMap.has(edge.target)) {
      adjacencyMap.get(edge.source)!.push(edge.target);
      inDegree.set(edge.target, (inDegree.get(edge.target) || 0) + 1);
      outDegree.set(edge.source, (outDegree.get(edge.source) || 0) + 1);
    }
  });

  return { adjacencyMap, inDegree, outDegree };
}

// ============ Position Calculation ============
/**
 * Calculate ideal X position based on connected nodes
 */
export function calculateIdealXPosition(
  nodeId: string,
  edges: Edge[],
  nodes: Node[]
): number | null {
  let idealX = 0;
  let connectionCount = 0;
  
  // Consider parent positions
  edges.forEach(edge => {
    if (edge.target === nodeId) {
      const parentNode = nodes.find(n => n.id === edge.source);
      if (parentNode?.position) {
        idealX += parentNode.position.x;
        connectionCount++;
      }
    }
  });
  
  // Consider child positions
  edges.forEach(edge => {
    if (edge.source === nodeId) {
      const childNode = nodes.find(n => n.id === edge.target);
      if (childNode?.position) {
        idealX += childNode.position.x;
        connectionCount++;
      }
    }
  });
  
  return connectionCount > 0 ? idealX / connectionCount : null;
}

// ============ Edge Proximity ============
/**
 * Find the nearest edge to a given position (for condition drag-and-drop).
 * Calculates distance from the position to the midpoint of each edge.
 */
export function findNearestEdge(
  position: { x: number; y: number },
  edges: Edge[],
  nodes: Node[],
  maxDistance = 80
): Edge | null {
  let nearestEdge: Edge | null = null;
  let minDistance = Infinity;

  edges.forEach(edge => {
    const sourceNode = nodes.find(n => n.id === edge.source);
    const targetNode = nodes.find(n => n.id === edge.target);
    
    if (!sourceNode || !targetNode) return;

    // Calculate node centers (considering node dimensions)
    const sourceCenter = {
      x: sourceNode.position.x + (sourceNode.width || NODE_DIMENSIONS.DEFAULT_WIDTH) / 2,
      y: sourceNode.position.y + (sourceNode.height || NODE_DIMENSIONS.DEFAULT_HEIGHT) / 2,
    };
    const targetCenter = {
      x: targetNode.position.x + (targetNode.width || NODE_DIMENSIONS.DEFAULT_WIDTH) / 2,
      y: targetNode.position.y + (targetNode.height || NODE_DIMENSIONS.DEFAULT_HEIGHT) / 2,
    };

    // Calculate edge midpoint (where conditions are typically placed)
    const midpoint = {
      x: (sourceCenter.x + targetCenter.x) / 2,
      y: (sourceCenter.y + targetCenter.y) / 2,
    };

    // Calculate distance from position to edge midpoint
    const distance = Math.sqrt(
      Math.pow(position.x - midpoint.x, 2) + Math.pow(position.y - midpoint.y, 2)
    );

    if (distance < minDistance && distance <= maxDistance) {
      minDistance = distance;
      nearestEdge = edge;
    }
  });

  return nearestEdge;
}

/**
 * Determine starting position for unlocked nodes layout
 */
export function determineStartPosition(lockedBounds: BoundingBox | null): { x: number; y: number } {
  if (lockedBounds) {
    // Start positioning unlocked nodes to the right of the locked elements bounding box
    return { 
      x: lockedBounds.x2 + LAYOUT_CONFIG.HORIZONTAL_SPACING, 
      y: Math.max(LAYOUT.DEFAULT_POSITION_Y, lockedBounds.y1) 
    };
  }
  // No locked nodes, use default position
  return { x: LAYOUT.LAYOUT_START_X, y: LAYOUT.LAYOUT_START_Y };
}