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

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

export interface Polygon {
  points: Point[];
  bbox: Rect;
}

export class ImageProcessing {
  /**
   * Simple Sobel filter to detect edges in a grayscale image
   */
  static Sobel(data: Uint8ClampedArray, width: number, height: number): Float32Array {
    const output = new Float32Array(width * height);
    const gx = [
      -1, 0, 1,
      -2, 0, 2,
      -1, 0, 1
    ];
    const gy = [
      -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 sumX = 0;
        let sumY = 0;

        for (let ky = -1; ky <= 1; ky++) {
          for (let kx = -1; kx <= 1; kx++) {
            const pixel = data[((y + ky) * width + (x + kx)) * 4]; // use red channel for grayscale
            sumX += pixel * gx[(ky + 1) * 3 + (kx + 1)];
            sumY += pixel * gy[(ky + 1) * 3 + (kx + 1)];
          }
        }
        output[y * width + x] = Math.sqrt(sumX * sumX + sumY * sumY);
      }
    }
    return output;
  }

  /**
   * Find rectangular regions based on edge strength
   */
  static findRectangularRegions(edges: Float32Array, width: number, height: number, threshold: number = 50): Polygon[] {
    const regions: Polygon[] = [];
    const visited = new Uint8Array(width * height);
 
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        if (edges[y * width + x] > threshold && !visited[y * width + x]) {
          // Start a region search
          const region = this.expandRegion(edges, visited, x, y, width, height, threshold);
          if (region && region.bbox.width > 50 && region.bbox.height > 80) {
            regions.push(region);
          }
        }
      }
    }
    return regions;
  }
 
  private static expandRegion(edges: Float32Array, visited: Uint8Array, startX: number, startY: number, width: number, height: number, threshold: number): Polygon | null {

    let minX = startX, maxX = startX;
    let minY = startY, maxY = startY;
    const points: Point[] = [];
    
    const stack = [[startX, startY]];
    visited[startY * width + startX] = 1;

    while (stack.length > 0) {
      const [cx, cy] = stack.pop()!;
      
      minX = Math.min(minX, cx);
      maxX = Math.max(maxX, cx);
      minY = Math.min(minY, cy);
      maxY = Math.max(maxY, cy);
      points.push({ x: cx, y: cy });

      const neighbors = [
        [cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1]
      ];

      for (const [nx, ny] of neighbors) {
        if (nx >= 0 && nx < width && ny >= 0 && ny < height && 
            !visited[ny * width + nx] && edges[ny * width + nx] > threshold) {
          visited[ny * width + nx] = 1;
          stack.push([nx, ny]);
        }
      }
    }

    const w = maxX - minX;
    const h = maxY - minY;
    
    if (w < 50 || h < 80) return null;
    
    return { 
      points, 
      bbox: { x: minX, y: minY, width: w, height: h } 
    };
  }


  /**
   * Find the 4 corners of a point set that most closely resemble a rectangle
   */
  static findCorners(points: Point[]): Point[] {
    if (points.length < 4) return [];

    let topLeft = points[0];
    let topRight = points[0];
    let bottomRight = points[0];
    let bottomLeft = points[0];

    let minSum = Infinity, maxSum = -Infinity;
    let minDiff = Infinity, maxDiff = -Infinity;

    for (const p of points) {
      const sum = p.x + p.y;
      const diff = p.x - p.y;

      if (sum < minSum) { minSum = sum; topLeft = p; }
      if (sum > maxSum) { maxSum = sum; bottomRight = p; }
      if (diff < minDiff) { minDiff = diff; bottomLeft = p; }
      if (diff > maxDiff) { maxDiff = diff; topRight = p; }
    }

    return [topLeft, topRight, bottomRight, bottomLeft];
  }

  /**
   * Performs a basic bilinear interpolation warp of a source image
   * from 4 corners to a destination rectangle
   */
  static warpPerspective(
    sourceCanvas: HTMLCanvasElement, 
    srcCorners: Point[], 
    destWidth: number, 
    destHeight: number
  ): HTMLCanvasElement {
    const destCanvas = document.createElement('canvas');
    destCanvas.width = destWidth;
    destCanvas.height = destHeight;
    const destCtx = destCanvas.getContext('2d');
    if (!destCtx) return destCanvas;

    const srcCtx = sourceCanvas.getContext('2d');
    if (!srcCtx) return destCanvas;

    const srcData = srcCtx.getImageData(0, 0, sourceCanvas.width, sourceCanvas.height).data;
    const destData = destCtx.createImageData(destWidth, destHeight);

    for (let y = 0; y < destHeight; y++) {
      for (let x = 0; x < destWidth; x++) {
        const u = x / destWidth;
        const v = y / destHeight;

        const srcX = (1 - u) * ((1 - v) * srcCorners[0].x + v * srcCorners[3].x) + 
                     u * ((1 - v) * srcCorners[1].x + v * srcCorners[2].x);
        const srcY = (1 - u) * ((1 - v) * srcCorners[0].y + v * srcCorners[3].y) + 
                     u * ((1 - v) * srcCorners[1].y + v * srcCorners[2].y);

        const sx = Math.floor(srcX);
        const sy = Math.floor(srcY);

        if (sx >= 0 && sx < sourceCanvas.width && sy >= 0 && sy < sourceCanvas.height) {
          const srcIdx = (sy * sourceCanvas.width + sx) * 4;
          const destIdx = (y * destWidth + x) * 4;
          destData.data[destIdx] = srcData[srcIdx];
          destData.data[destIdx + 1] = srcData[srcIdx + 1];
          destData.data[destIdx + 2] = srcData[srcIdx + 2];
          destData.data[destIdx + 3] = srcData[srcIdx + 3];
        }
      }
    }

    destCtx.putImageData(destData, 0, 0);
    return destCanvas;
  }

  /**
   * Converts RGB to grayscale ( Luminance )
   */
  static toGrayscale(imageData: ImageData): Uint8ClampedArray {
    const { data, width, height } = imageData;
    const gray = new Uint8ClampedArray(width * height);
    for (let i = 0; i < data.length; i += 4) {
      gray[i / 4] = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
    }
    return gray;
  }

  /**
   * Overloaded Sobel that takes ImageData and returns edge map
   */
  static detectEdges(imageData: ImageData): Float32Array {
    const gray = this.toGrayscale(imageData);
    // Create a fake imageData for the Sobel method since it expects 4-channel
    const fakeData = new Uint8ClampedArray(gray.length * 4);
    for (let i = 0; i < gray.length; i++) {
      fakeData[i * 4] = gray[i];
    }
    return this.Sobel(fakeData, imageData.width, imageData.height);
  }
}

