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 computeGradients(data: Uint8ClampedArray, width: number, height: number): { magnitude: Float32Array, direction: Float32Array } {
    const magnitude = new Float32Array(width * height);
    const direction = new Float32Array(width * height);
    const gx_kernel = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
    const gy_kernel = [-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];
            sumX += pixel * gx_kernel[(ky + 1) * 3 + (kx + 1)];
            sumY += pixel * gy_kernel[(ky + 1) * 3 + (kx + 1)];
          }
        }
        magnitude[y * width + x] = Math.sqrt(sumX * sumX + sumY * sumY);
        direction[y * width + x] = Math.atan2(sumY, sumX);
      }
    }
    return { magnitude, direction };
  }

  static nonMaximumSuppression(magnitude: Float32Array, direction: Float32Array, width: number, height: number): Float32Array {
    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;
        const mag = magnitude[idx];
        const angle = direction[idx] * (180 / Math.PI);
        let normalizedAngle = angle < 0 ? angle + 180 : angle;

        let neighbor1 = 0;
        let neighbor2 = 0;

        if ((normalizedAngle >= 0 && normalizedAngle < 22.5) || (normalizedAngle >= 157.5 && normalizedAngle <= 180)) {
          neighbor1 = magnitude[y * width + (x + 1)];
          neighbor2 = magnitude[y * width + (x - 1)];
        } else if (normalizedAngle >= 22.5 && normalizedAngle < 67.5) {
          neighbor1 = magnitude[(y - 1) * width + (x + 1)];
          neighbor2 = magnitude[(y + 1) * width + (x - 1)];
        } else if (normalizedAngle >= 67.5 && normalizedAngle < 112.5) {
          neighbor1 = magnitude[(y - 1) * width + x];
          neighbor2 = magnitude[(y + 1) * width + x];
        } else if (normalizedAngle >= 112.5 && normalizedAngle < 157.5) {
          neighbor1 = magnitude[(y - 1) * width + (x - 1)];
          neighbor2 = magnitude[(y + 1) * width + (x + 1)];
        }

        if (mag >= neighbor1 && mag >= neighbor2) {
          suppressed[idx] = mag;
        } else {
          suppressed[idx] = 0;
        }
      }
    }
    return suppressed;
  }

  /**
   * 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]) {
          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 minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
    for (const p of points) {
      if (p.x < minX) minX = p.x;
      if (p.x > maxX) maxX = p.x;
      if (p.y < minY) minY = p.y;
      if (p.y > maxY) maxY = p.y;
    }

    const targets = [
      { x: minX, y: minY },
      { x: maxX, y: minY },
      { x: maxX, y: maxY },
      { x: minX, y: maxY },
    ];

    const corners: Point[] = [];
    for (const target of targets) {
      let closest = points[0];
      let minDist = Infinity;
      for (const p of points) {
        const dist = Math.sqrt((p.x - target.x) ** 2 + (p.y - target.y) ** 2);
        if (dist < minDist) {
          minDist = dist;
          closest = p;
        }
      }
      corners.push(closest);
    }

    return corners;
  }

  static applyHysteresis(edges: Float32Array, width: number, height: number, lowThreshold: number, highThreshold: number): Float32Array {
    const result = new Float32Array(width * height);
    const strongEdges = [];
    
    for (let i = 0; i < edges.length; i++) {
      if (edges[i] >= highThreshold) {
        result[i] = edges[i];
        strongEdges.push(i);
      } else if (edges[i] < lowThreshold) {
        result[i] = 0;
      }
    }

    const stack = [...strongEdges];
    while (stack.length > 0) {
      const idx = stack.pop()!;
      const x = idx % width;
      const y = Math.floor(idx / width);

      const neighbors = [
        [x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1],
        [x + 1, y + 1], [x - 1, y + 1], [x + 1, y - 1], [x - 1, y - 1]
      ];

      for (const [nx, ny] of neighbors) {
        if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
          const nIdx = ny * width + nx;
          if (result[nIdx] === 0 && edges[nIdx] >= lowThreshold) {
            result[nIdx] = edges[nIdx];
            stack.push(nIdx);
          }
        }
      }
    }
    return result;
  }

  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;
  }

  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;
  }

  static detectEdges(imageData: ImageData): Float32Array {
    const gray = this.toGrayscale(imageData);
    const grayWithChannels = new Uint8ClampedArray(gray.length * 4);
    for (let i = 0; i < gray.length; i++) {
      grayWithChannels[i * 4] = gray[i];
    }
    const { magnitude, direction } = this.computeGradients(grayWithChannels, imageData.width, imageData.height);
    const nmsEdges = this.nonMaximumSuppression(magnitude, direction, imageData.width, imageData.height);
    return this.applyHysteresis(nmsEdges, imageData.width, imageData.height, 30, 70);
  }
}
