/**
 * Canvas Layout Utilities
 * Smart positioning and alignment algorithms for canvas nodes
 */

import { AppNodeType } from '../types/node';
import { getNodeBounds, getApproxNodeHeight } from './index';

// ==================== Constants ====================

export const LAYOUT_CONFIG = {
    NODE_WIDTH: 420,
    NODE_MIN_HEIGHT: 200,
    SPACING_X: 60,
    SPACING_Y: 40,
    GRID_COLUMNS: 3,
    VIEWPORT_PADDING: 100,
    ALIGNMENT_THRESHOLD: 20,
};

// ==================== Position Types ====================

export interface Position {
    x: number;
    y: number;
}

export interface Bounds {
    x: number;
    y: number;
    width: number;
    height: number;
}

export type LayoutMode = 'horizontal' | 'vertical' | 'grid' | 'cascade' | 'radial' | 'auto';

// ==================== Smart Position Calculation ====================

/**
 * Find a non-overlapping position for a new node
 * @param existingNodes Current nodes on canvas
 * @param preferredPosition Preferred starting position
 * @param nodeWidth Width of the new node
 * @param nodeHeight Height of the new node
 */
export function findNonOverlappingPosition(
    existingNodes: AppNodeType[],
    preferredPosition: Position,
    nodeWidth: number = LAYOUT_CONFIG.NODE_WIDTH,
    nodeHeight: number = LAYOUT_CONFIG.NODE_MIN_HEIGHT
): Position {
    if (existingNodes.length === 0) {
        return preferredPosition;
    }

    const spacing = LAYOUT_CONFIG.SPACING_X;
    let position = { ...preferredPosition };
    let attempts = 0;
    const maxAttempts = 50;

    while (hasOverlap(position, nodeWidth, nodeHeight, existingNodes) && attempts < maxAttempts) {
        // Try different directions in a spiral pattern
        const spiralStep = Math.floor(attempts / 4);
        const direction = attempts % 4;
        
        switch (direction) {
            case 0: // Right
                position.x = preferredPosition.x + (spiralStep + 1) * (nodeWidth + spacing);
                position.y = preferredPosition.y;
                break;
            case 1: // Down
                position.x = preferredPosition.x;
                position.y = preferredPosition.y + (spiralStep + 1) * (nodeHeight + spacing);
                break;
            case 2: // Left
                position.x = preferredPosition.x - (spiralStep + 1) * (nodeWidth + spacing);
                position.y = preferredPosition.y;
                break;
            case 3: // Up
                position.x = preferredPosition.x;
                position.y = preferredPosition.y - (spiralStep + 1) * (nodeHeight + spacing);
                break;
        }
        attempts++;
    }

    return position;
}

/**
 * Check if a position overlaps with existing nodes
 */
function hasOverlap(
    position: Position,
    width: number,
    height: number,
    existingNodes: AppNodeType[]
): boolean {
    const newBounds: Bounds = {
        x: position.x,
        y: position.y,
        width,
        height
    };

    for (const node of existingNodes) {
        const nodeBounds = getNodeBounds(node);
        if (boundsIntersect(newBounds, nodeBounds)) {
            return true;
        }
    }
    return false;
}

/**
 * Check if two bounds intersect (with padding)
 */
function boundsIntersect(a: Bounds, b: Bounds): boolean {
    const padding = LAYOUT_CONFIG.SPACING_X / 2;
    return !(
        a.x + a.width + padding < b.x ||
        b.x + b.width + padding < a.x ||
        a.y + a.height + padding < b.y ||
        b.y + b.height + padding < a.y
    );
}

// ==================== Layout Algorithms ====================

/**
 * Calculate positions for multiple nodes based on layout mode
 */
export function calculateLayoutPositions(
    count: number,
    startPosition: Position,
    layout: LayoutMode = 'auto',
    existingNodes: AppNodeType[] = []
): Position[] {
    switch (layout) {
        case 'horizontal':
            return calculateHorizontalLayout(count, startPosition);
        case 'vertical':
            return calculateVerticalLayout(count, startPosition);
        case 'grid':
            return calculateGridLayout(count, startPosition);
        case 'cascade':
            return calculateCascadeLayout(count, startPosition);
        case 'radial':
            return calculateRadialLayout(count, startPosition);
        case 'auto':
        default:
            return calculateAutoLayout(count, startPosition, existingNodes);
    }
}

/**
 * Horizontal layout - nodes arranged in a row
 */
function calculateHorizontalLayout(count: number, start: Position): Position[] {
    const positions: Position[] = [];
    const spacing = LAYOUT_CONFIG.NODE_WIDTH + LAYOUT_CONFIG.SPACING_X;
    
    for (let i = 0; i < count; i++) {
        positions.push({
            x: start.x + i * spacing,
            y: start.y
        });
    }
    return positions;
}

/**
 * Vertical layout - nodes arranged in a column
 */
function calculateVerticalLayout(count: number, start: Position): Position[] {
    const positions: Position[] = [];
    const spacing = LAYOUT_CONFIG.NODE_MIN_HEIGHT + LAYOUT_CONFIG.SPACING_Y;
    
    for (let i = 0; i < count; i++) {
        positions.push({
            x: start.x,
            y: start.y + i * spacing
        });
    }
    return positions;
}

/**
 * Grid layout - nodes arranged in a grid pattern
 */
function calculateGridLayout(count: number, start: Position): Position[] {
    const positions: Position[] = [];
    const cols = LAYOUT_CONFIG.GRID_COLUMNS;
    const spacingX = LAYOUT_CONFIG.NODE_WIDTH + LAYOUT_CONFIG.SPACING_X;
    const spacingY = LAYOUT_CONFIG.NODE_MIN_HEIGHT + LAYOUT_CONFIG.SPACING_Y;
    
    for (let i = 0; i < count; i++) {
        const row = Math.floor(i / cols);
        const col = i % cols;
        positions.push({
            x: start.x + col * spacingX,
            y: start.y + row * spacingY
        });
    }
    return positions;
}

/**
 * Cascade layout - diagonal arrangement
 */
function calculateCascadeLayout(count: number, start: Position): Position[] {
    const positions: Position[] = [];
    const offsetX = 60;
    const offsetY = 50;
    
    for (let i = 0; i < count; i++) {
        positions.push({
            x: start.x + i * offsetX,
            y: start.y + i * offsetY
        });
    }
    return positions;
}

/**
 * Radial layout - nodes arranged in a circle
 */
function calculateRadialLayout(count: number, center: Position, radius: number = 300): Position[] {
    const positions: Position[] = [];
    
    for (let i = 0; i < count; i++) {
        const angle = (2 * Math.PI * i) / count - Math.PI / 2; // Start from top
        positions.push({
            x: center.x + Math.cos(angle) * radius,
            y: center.y + Math.sin(angle) * radius
        });
    }
    return positions;
}

/**
 * Auto layout - intelligent positioning based on existing nodes
 */
function calculateAutoLayout(
    count: number,
    preferredStart: Position,
    existingNodes: AppNodeType[]
): Position[] {
    const positions: Position[] = [];
    const tempNodes: AppNodeType[] = [...existingNodes];
    
    // Determine best layout based on count
    let layout: LayoutMode;
    if (count <= 2) {
        layout = 'horizontal';
    } else if (count <= 4) {
        layout = 'grid';
    } else {
        layout = 'grid';
    }
    
    // Get initial positions from chosen layout
    const initialPositions = calculateLayoutPositions(count, preferredStart, layout);
    
    // Adjust each position to avoid overlaps
    for (let i = 0; i < count; i++) {
        const adjustedPos = findNonOverlappingPosition(
            tempNodes,
            initialPositions[i],
            LAYOUT_CONFIG.NODE_WIDTH,
            LAYOUT_CONFIG.NODE_MIN_HEIGHT
        );
        positions.push(adjustedPos);
        
        // Add temporary node for next iteration
        tempNodes.push({
            id: `temp-${i}`,
            type: 'appNode',
            position: adjustedPos,
            data: {} as any
        } as AppNodeType);
    }
    
    return positions;
}

// ==================== Alignment Functions ====================

/**
 * Snap a position to alignment guides
 */
export function snapToAlignment(
    position: Position,
    existingNodes: AppNodeType[],
    threshold: number = LAYOUT_CONFIG.ALIGNMENT_THRESHOLD
): { position: Position; guides: AlignmentGuide[] } {
    let snappedX = position.x;
    let snappedY = position.y;
    const guides: AlignmentGuide[] = [];
    
    for (const node of existingNodes) {
        const bounds = getNodeBounds(node);
        
        // Horizontal alignment (X)
        // Align left edges
        if (Math.abs(position.x - bounds.x) < threshold) {
            snappedX = bounds.x;
            guides.push({ type: 'vertical', position: bounds.x });
        }
        // Align right edges
        if (Math.abs(position.x + LAYOUT_CONFIG.NODE_WIDTH - (bounds.x + bounds.width)) < threshold) {
            snappedX = bounds.x + bounds.width - LAYOUT_CONFIG.NODE_WIDTH;
            guides.push({ type: 'vertical', position: bounds.x + bounds.width });
        }
        // Align centers horizontally
        const nodeCenter = bounds.x + bounds.width / 2;
        const newCenter = position.x + LAYOUT_CONFIG.NODE_WIDTH / 2;
        if (Math.abs(newCenter - nodeCenter) < threshold) {
            snappedX = nodeCenter - LAYOUT_CONFIG.NODE_WIDTH / 2;
            guides.push({ type: 'vertical', position: nodeCenter });
        }
        
        // Vertical alignment (Y)
        // Align top edges
        if (Math.abs(position.y - bounds.y) < threshold) {
            snappedY = bounds.y;
            guides.push({ type: 'horizontal', position: bounds.y });
        }
        // Align bottom edges
        if (Math.abs(position.y + LAYOUT_CONFIG.NODE_MIN_HEIGHT - (bounds.y + bounds.height)) < threshold) {
            snappedY = bounds.y + bounds.height - LAYOUT_CONFIG.NODE_MIN_HEIGHT;
            guides.push({ type: 'horizontal', position: bounds.y + bounds.height });
        }
    }
    
    return {
        position: { x: snappedX, y: snappedY },
        guides
    };
}

export interface AlignmentGuide {
    type: 'horizontal' | 'vertical';
    position: number;
}

// ==================== Workflow Layout ====================

/**
 * Calculate positions for a workflow chain (nodes connected in sequence)
 */
export function calculateWorkflowLayout(
    nodeCount: number,
    startPosition: Position,
    direction: 'horizontal' | 'vertical' = 'horizontal'
): Position[] {
    const positions: Position[] = [];
    const spacingX = LAYOUT_CONFIG.NODE_WIDTH + LAYOUT_CONFIG.SPACING_X * 2;
    const spacingY = LAYOUT_CONFIG.NODE_MIN_HEIGHT + LAYOUT_CONFIG.SPACING_Y * 2;
    
    for (let i = 0; i < nodeCount; i++) {
        if (direction === 'horizontal') {
            positions.push({
                x: startPosition.x + i * spacingX,
                y: startPosition.y
            });
        } else {
            positions.push({
                x: startPosition.x,
                y: startPosition.y + i * spacingY
            });
        }
    }
    
    return positions;
}

/**
 * Calculate positions for parallel branches in a workflow
 */
export function calculateParallelBranchLayout(
    branchCount: number,
    startPosition: Position,
    branchNodeCounts: number[]
): Position[][] {
    const branches: Position[][] = [];
    const branchSpacing = LAYOUT_CONFIG.NODE_MIN_HEIGHT + LAYOUT_CONFIG.SPACING_Y * 3;
    
    // Calculate total height needed
    const totalHeight = branchCount * (LAYOUT_CONFIG.NODE_MIN_HEIGHT + branchSpacing);
    const startY = startPosition.y - totalHeight / 2 + LAYOUT_CONFIG.NODE_MIN_HEIGHT / 2;
    
    for (let b = 0; b < branchCount; b++) {
        const branchStart: Position = {
            x: startPosition.x,
            y: startY + b * branchSpacing
        };
        
        const nodeCount = branchNodeCounts[b] || 1;
        branches.push(calculateWorkflowLayout(nodeCount, branchStart, 'horizontal'));
    }
    
    return branches;
}

// ==================== Canvas State Analysis ====================

/**
 * Find empty space on canvas for new nodes
 */
export function findEmptySpace(
    existingNodes: AppNodeType[],
    requiredWidth: number,
    requiredHeight: number,
    viewportCenter: Position
): Position {
    if (existingNodes.length === 0) {
        return {
            x: viewportCenter.x - requiredWidth / 2,
            y: viewportCenter.y - requiredHeight / 2
        };
    }
    
    // Get canvas bounds
    const allBounds = existingNodes.map(getNodeBounds);
    const canvasBounds = {
        minX: Math.min(...allBounds.map(b => b.x)),
        minY: Math.min(...allBounds.map(b => b.y)),
        maxX: Math.max(...allBounds.map(b => b.x + b.width)),
        maxY: Math.max(...allBounds.map(b => b.y + b.height))
    };
    
    // Try positions in order of preference
    const candidatePositions: Position[] = [
        // Right of existing content
        { x: canvasBounds.maxX + LAYOUT_CONFIG.SPACING_X * 2, y: canvasBounds.minY },
        // Below existing content
        { x: canvasBounds.minX, y: canvasBounds.maxY + LAYOUT_CONFIG.SPACING_Y * 2 },
        // Left of existing content
        { x: canvasBounds.minX - requiredWidth - LAYOUT_CONFIG.SPACING_X * 2, y: canvasBounds.minY },
        // Above existing content
        { x: canvasBounds.minX, y: canvasBounds.minY - requiredHeight - LAYOUT_CONFIG.SPACING_Y * 2 },
    ];
    
    // Find first position that doesn't overlap
    for (const pos of candidatePositions) {
        if (!hasOverlap(pos, requiredWidth, requiredHeight, existingNodes)) {
            return pos;
        }
    }
    
    // Fallback: use non-overlapping position algorithm
    return findNonOverlappingPosition(
        existingNodes,
        viewportCenter,
        requiredWidth,
        requiredHeight
    );
}

/**
 * Auto-arrange all nodes on canvas
 */
export function autoArrangeNodes(
    nodes: AppNodeType[],
    centerPosition: Position,
    layout: LayoutMode = 'grid'
): Map<string, Position> {
    const newPositions = new Map<string, Position>();
    
    if (nodes.length === 0) return newPositions;
    
    const positions = calculateLayoutPositions(nodes.length, centerPosition, layout);
    
    nodes.forEach((node, index) => {
        newPositions.set(node.id, positions[index]);
    });
    
    return newPositions;
}
