/**
 * Card detection using edge detection and contour analysis.
 * Detects card boundaries in images and returns the four corner points.
 * 
 * This is a pure TypeScript implementation for when OpenCV is not available.
 */

import {
  Point,
  toGrayscale,
  gaussianBlur,
  distance,
  crossProduct,
  pointToLineDistance,
} from "./imageUtils";

/** Standard MTG card aspect ratio (height / width). Cards are 63mm x 88mm. */
const CARD_ASPECT_RATIO = 88 / 63; // ~1.397
const ASPECT_RATIO_TOLERANCE = 0.25;
const MIN_CARD_AREA_RATIO = 0.05;
const MAX_CARD_AREA_RATIO = 0.98;

export interface CardDetectionResult {
  found: boolean;
  corners: Point[];
  confidence: number;
  debugMessage?: string;
}

/**
 * Detect a card in the image and return its corner points.
 */
export function detectCard(
  pixels: Uint8Array | Uint8ClampedArray,
  width: number,
  height: number
): CardDetectionResult {
  // Step 1: Convert to grayscale
  const grayscale = toGrayscale(pixels);
  
  // Step 2: Apply Gaussian blur to reduce noise
  const blurred = gaussianBlur(grayscale, width, height, 5);
  
  // Step 3: Apply Canny edge detection
  const edges = applyCannyEdgeDetection(blurred, width, height, 50, 150);
  
  // Step 4: Find contours
  const contours = findContours(edges, width, height);
  
  if (contours.length === 0) {
    return { found: false, corners: [], confidence: 0, debugMessage: "No contours found" };
  }
  
  // Step 5: Find the best card-like quadrilateral
  const imageArea = width * height;
  const bestQuad = findBestCardQuadrilateral(contours, imageArea);
  
  if (!bestQuad) {
    return { found: false, corners: [], confidence: 0, debugMessage: "No card-like quadrilateral found" };
  }
  
  // Step 6: Order corners consistently
  const orderedCorners = orderCorners(bestQuad);
  
  // Calculate confidence
  const confidence = calculateConfidence(orderedCorners, imageArea);
  
  return { found: true, corners: orderedCorners, confidence };
}

/**
 * Apply Canny edge detection.
 */
function applyCannyEdgeDetection(
  gray: Uint8Array,
  width: number,
  height: number,
  lowThreshold: number,
  highThreshold: number
): Uint8Array {
  // Step 1: Compute gradients using Sobel operators
  const gradientX = new Float32Array(width * height);
  const gradientY = new Float32Array(width * height);
  const magnitude = new Float32Array(width * height);
  const direction = new Float32Array(width * height);
  
  // Sobel kernels
  const sobelX = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
  const sobelY = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
  
  for (let y = 1; y < height - 1; y++) {
    for (let x = 1; x < width - 1; x++) {
      let gx = 0, gy = 0;
      
      for (let ky = -1; ky <= 1; ky++) {
        for (let kx = -1; kx <= 1; kx++) {
          const pixel = gray[(y + ky) * width + (x + kx)];
          gx += pixel * sobelX[ky + 1][kx + 1];
          gy += pixel * sobelY[ky + 1][kx + 1];
        }
      }
      
      const idx = y * width + x;
      gradientX[idx] = gx;
      gradientY[idx] = gy;
      magnitude[idx] = Math.sqrt(gx * gx + gy * gy);
      direction[idx] = Math.atan2(gy, gx);
    }
  }
  
  // Step 2: Non-maximum suppression
  const suppressed = new Float32Array(width * height);
  
  for (let y = 1; y < height - 1; y++) {
    for (let x = 1; x < width - 1; x++) {
      const idx = y * width + x;
      let angle = direction[idx] * 180 / Math.PI;
      if (angle < 0) angle += 180;
      
      let neighbor1: number, neighbor2: number;
      
      if (angle < 22.5 || angle >= 157.5) {
        neighbor1 = magnitude[y * width + (x - 1)];
        neighbor2 = magnitude[y * width + (x + 1)];
      } else if (angle >= 22.5 && angle < 67.5) {
        neighbor1 = magnitude[(y - 1) * width + (x + 1)];
        neighbor2 = magnitude[(y + 1) * width + (x - 1)];
      } else if (angle >= 67.5 && angle < 112.5) {
        neighbor1 = magnitude[(y - 1) * width + x];
        neighbor2 = magnitude[(y + 1) * width + x];
      } else {
        neighbor1 = magnitude[(y - 1) * width + (x - 1)];
        neighbor2 = magnitude[(y + 1) * width + (x + 1)];
      }
      
      if (magnitude[idx] >= neighbor1 && magnitude[idx] >= neighbor2) {
        suppressed[idx] = magnitude[idx];
      }
    }
  }
  
  // Step 3: Double thresholding and edge tracking
  const result = new Uint8Array(width * height);
  const strong = new Uint8Array(width * height);
  const weak = new Uint8Array(width * height);
  
  for (let i = 0; i < width * height; i++) {
    if (suppressed[i] >= highThreshold) {
      strong[i] = 1;
    } else if (suppressed[i] >= lowThreshold) {
      weak[i] = 1;
    }
  }
  
  // Edge tracking by hysteresis
  for (let y = 1; y < height - 1; y++) {
    for (let x = 1; x < width - 1; x++) {
      const idx = y * width + x;
      
      if (strong[idx]) {
        result[idx] = 255;
      } else if (weak[idx]) {
        // Check if connected to strong edge
        outer: for (let dy = -1; dy <= 1; dy++) {
          for (let dx = -1; dx <= 1; dx++) {
            if (strong[(y + dy) * width + (x + dx)]) {
              result[idx] = 255;
              break outer;
            }
          }
        }
      }
    }
  }
  
  return result;
}

/**
 * Find contours in a binary edge image using flood fill.
 */
function findContours(
  edges: Uint8Array,
  width: number,
  height: number
): Point[][] {
  const visited = new Uint8Array(width * height);
  const contours: Point[][] = [];
  
  const dx = [-1, 0, 1, 1, 1, 0, -1, -1];
  const dy = [-1, -1, -1, 0, 1, 1, 1, 0];
  
  for (let y = 1; y < height - 1; y++) {
    for (let x = 1; x < width - 1; x++) {
      const idx = y * width + x;
      if (visited[idx]) continue;
      if (edges[idx] < 128) continue;
      
      // Trace contour using BFS
      const contour: Point[] = [];
      const queue: Array<{ x: number; y: number }> = [{ x, y }];
      
      while (queue.length > 0 && contour.length < 10000) {
        const point = queue.shift()!;
        const pidx = point.y * width + point.x;
        
        if (point.x < 0 || point.x >= width || point.y < 0 || point.y >= height) continue;
        if (visited[pidx]) continue;
        if (edges[pidx] < 128) continue;
        
        visited[pidx] = 1;
        contour.push(point);
        
        for (let i = 0; i < 8; i++) {
          queue.push({ x: point.x + dx[i], y: point.y + dy[i] });
        }
      }
      
      if (contour.length >= 4) {
        contours.push(contour);
      }
    }
  }
  
  return contours;
}

/**
 * Find the best quadrilateral that matches a card shape.
 */
function findBestCardQuadrilateral(
  contours: Point[][],
  imageArea: number
): Point[] | null {
  let bestQuad: Point[] | null = null;
  let bestScore = -Infinity;
  
  for (const contour of contours) {
    // Simplify contour using Douglas-Peucker
    const simplified = simplifyContour(contour, contour.length * 0.02);
    
    // Try to approximate as quadrilateral
    const quad = approximateQuadrilateral(simplified);
    if (!quad) continue;
    
    // Check if valid card shape
    const area = calculateQuadArea(quad);
    const areaRatio = area / imageArea;
    
    if (areaRatio < MIN_CARD_AREA_RATIO || areaRatio > MAX_CARD_AREA_RATIO) {
      continue;
    }
    
    // Check aspect ratio
    const aspectScore = calculateAspectRatioScore(quad);
    if (aspectScore < 0.5) continue;
    
    // Check convexity
    if (!isConvex(quad)) continue;
    
    // Score based on area and aspect ratio
    const score = areaRatio * aspectScore;
    
    if (score > bestScore) {
      bestScore = score;
      bestQuad = quad;
    }
  }
  
  return bestQuad;
}

/**
 * Simplify a contour using Douglas-Peucker algorithm.
 */
function simplifyContour(contour: Point[], epsilon: number): Point[] {
  if (contour.length < 3) return contour;
  
  const first = contour[0];
  const last = contour[contour.length - 1];
  
  let maxDist = 0;
  let maxIndex = 0;
  
  for (let i = 1; i < contour.length - 1; i++) {
    const dist = pointToLineDistance(contour[i], first, last);
    if (dist > maxDist) {
      maxDist = dist;
      maxIndex = i;
    }
  }
  
  if (maxDist > epsilon) {
    const left = simplifyContour(contour.slice(0, maxIndex + 1), epsilon);
    const right = simplifyContour(contour.slice(maxIndex), epsilon);
    return [...left.slice(0, -1), ...right];
  }
  
  return [first, last];
}

/**
 * Try to approximate a contour as a quadrilateral.
 */
function approximateQuadrilateral(contour: Point[]): Point[] | null {
  if (contour.length < 4) return null;
  if (contour.length === 4) return contour;
  
  // Find convex hull
  const hull = convexHull(contour);
  if (hull.length < 4) return null;
  if (hull.length === 4) return hull;
  
  // Find 4 extreme points
  return findExtremePoints(hull);
}

/**
 * Calculate convex hull using Graham scan.
 */
function convexHull(points: Point[]): Point[] {
  if (points.length < 3) return points;
  
  // Find bottom-most point
  const start = points.reduce((min, p) =>
    p.y < min.y || (p.y === min.y && p.x < min.x) ? p : min
  );
  
  // Sort by polar angle
  const sorted = points
    .filter(p => p !== start)
    .sort((a, b) => {
      const angleA = Math.atan2(a.y - start.y, a.x - start.x);
      const angleB = Math.atan2(b.y - start.y, b.x - start.x);
      if (angleA !== angleB) return angleA - angleB;
      return distance(a, start) - distance(b, start);
    });
  
  const hull: Point[] = [start];
  
  for (const point of sorted) {
    while (hull.length > 1 && crossProduct(hull[hull.length - 2], hull[hull.length - 1], point) <= 0) {
      hull.pop();
    }
    hull.push(point);
  }
  
  return hull;
}

/**
 * Find 4 extreme points of a convex hull.
 */
function findExtremePoints(hull: Point[]): Point[] {
  if (hull.length <= 4) return hull;
  
  const minX = hull.reduce((min, p) => p.x < min.x ? p : min, hull[0]);
  const maxX = hull.reduce((max, p) => p.x > max.x ? p : max, hull[0]);
  const minY = hull.reduce((min, p) => p.y < min.y ? p : min, hull[0]);
  const maxY = hull.reduce((max, p) => p.y > max.y ? p : max, hull[0]);
  
  const extremes = new Set([minX, maxX, minY, maxY]);
  
  if (extremes.size === 4) {
    return Array.from(extremes);
  }
  
  // Fallback: take points at regular angular intervals
  const centerX = hull.reduce((s, p) => s + p.x, 0) / hull.length;
  const centerY = hull.reduce((s, p) => s + p.y, 0) / hull.length;
  
  const sorted = [...hull].sort((a, b) =>
    Math.atan2(a.y - centerY, a.x - centerX) -
    Math.atan2(b.y - centerY, b.x - centerX)
  );
  
  const step = Math.floor(sorted.length / 4);
  return [sorted[0], sorted[step], sorted[step * 2], sorted[step * 3]];
}

/**
 * Calculate area of a quadrilateral using shoelace formula.
 */
function calculateQuadArea(quad: Point[]): number {
  let area = 0;
  for (let i = 0; i < 4; i++) {
    const j = (i + 1) % 4;
    area += quad[i].x * quad[j].y;
    area -= quad[j].x * quad[i].y;
  }
  return Math.abs(area) / 2;
}

/**
 * Calculate how well the aspect ratio matches a card.
 */
function calculateAspectRatioScore(quad: Point[]): number {
  const width1 = distance(quad[0], quad[1]);
  const width2 = distance(quad[2], quad[3]);
  const height1 = distance(quad[1], quad[2]);
  const height2 = distance(quad[3], quad[0]);
  
  const avgWidth = (width1 + width2) / 2;
  const avgHeight = (height1 + height2) / 2;
  
  // Ensure we get the right ratio regardless of orientation
  const aspectRatio = avgWidth > avgHeight
    ? avgWidth / avgHeight
    : avgHeight / avgWidth;
  
  const deviation = Math.abs(aspectRatio - CARD_ASPECT_RATIO) / CARD_ASPECT_RATIO;
  
  return Math.max(0, 1 - deviation / ASPECT_RATIO_TOLERANCE);
}

/**
 * Check if a quadrilateral is convex.
 */
function isConvex(quad: Point[]): boolean {
  let sign = 0;
  
  for (let i = 0; i < 4; i++) {
    const cross = crossProduct(
      quad[i],
      quad[(i + 1) % 4],
      quad[(i + 2) % 4]
    );
    
    if (Math.abs(cross) < 0.0001) continue;
    
    const currentSign = cross > 0 ? 1 : -1;
    
    if (sign === 0) {
      sign = currentSign;
    } else if (sign !== currentSign) {
      return false;
    }
  }
  
  return true;
}

/**
 * Order corners consistently: top-left, top-right, bottom-right, bottom-left.
 */
function orderCorners(corners: Point[]): Point[] {
  const centerX = corners.reduce((s, c) => s + c.x, 0) / 4;
  const centerY = corners.reduce((s, c) => s + c.y, 0) / 4;
  
  const topLeft = corners.filter(c => c.x < centerX && c.y < centerY)
    .sort((a, b) => (a.x + a.y) - (b.x + b.y))[0];
  const topRight = corners.filter(c => c.x >= centerX && c.y < centerY)
    .sort((a, b) => (a.y - a.x) - (b.y - b.x))[0];
  const bottomRight = corners.filter(c => c.x >= centerX && c.y >= centerY)
    .sort((a, b) => (b.x + b.y) - (a.x + a.y))[0];
  const bottomLeft = corners.filter(c => c.x < centerX && c.y >= centerY)
    .sort((a, b) => (b.y - b.x) - (a.y - a.x))[0];
  
  // Handle edge cases by sorting by angle
  if (!topLeft || !topRight || !bottomRight || !bottomLeft) {
    const sorted = [...corners].sort((a, b) =>
      Math.atan2(a.y - centerY, a.x - centerX) -
      Math.atan2(b.y - centerY, b.x - centerX)
    );
    
    // Find index of point with minimum sum (top-left)
    let minSumIdx = 0;
    let minSum = Infinity;
    sorted.forEach((c, i) => {
      const sum = c.x + c.y;
      if (sum < minSum) {
        minSum = sum;
        minSumIdx = i;
      }
    });
    
    return [...sorted.slice(minSumIdx), ...sorted.slice(0, minSumIdx)];
  }
  
  return [topLeft, topRight, bottomRight, bottomLeft];
}

/**
 * Calculate confidence of the detection.
 */
function calculateConfidence(corners: Point[], imageArea: number): number {
  const area = calculateQuadArea(corners);
  const areaScore = Math.min(area / imageArea / 0.5, 1);
  const aspectScore = calculateAspectRatioScore(corners);
  
  return areaScore * 0.4 + aspectScore * 0.6;
}
