/** Standard trading-card aspect ratio (width / height). */
export const CARD_ASPECT_RATIO = 5 / 7;

/** Order four corners as top-left, top-right, bottom-right, bottom-left. */
export function orderQuadCorners(points) {
  if (!points || points.length !== 4) {
    throw new Error('orderQuadCorners expects exactly four points');
  }

  const sortedByY = [...points].sort((a, b) => a.y - b.y);
  const top = sortedByY.slice(0, 2).sort((a, b) => a.x - b.x);
  const bottom = sortedByY.slice(2, 4).sort((a, b) => a.x - b.x);

  return [top[0], top[1], bottom[1], bottom[0]];
}

/** Axis-aligned bounds enclosing a quad. */
export function boundsFromCorners(corners) {
  const xs = corners.map((point) => point.x);
  const ys = corners.map((point) => point.y);
  const minX = Math.min(...xs);
  const minY = Math.min(...ys);
  const maxX = Math.max(...xs);
  const maxY = Math.max(...ys);

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

/** True when corners form a convex quad with plausible card aspect ratio. */
export function isValidCardQuad(corners, { minAspect = 0.6, maxAspect = 0.8 } = {}) {
  if (!corners || corners.length !== 4) return false;

  const ordered = orderQuadCorners(corners);
  const bounds = boundsFromCorners(ordered);
  if (bounds.width < 20 || bounds.height < 28) return false;

  const aspect = bounds.width / bounds.height;
  if (aspect < minAspect || aspect > maxAspect) return false;

  const area = polygonArea(ordered);
  const boundsArea = bounds.width * bounds.height;
  if (area / boundsArea < 0.55) return false;

  return isConvexQuad(ordered);
}

function polygonArea(points) {
  let sum = 0;
  for (let i = 0; i < points.length; i++) {
    const current = points[i];
    const next = points[(i + 1) % points.length];
    sum += current.x * next.y - next.x * current.y;
  }
  return Math.abs(sum) / 2;
}

function isConvexQuad(points) {
  let sign = 0;
  for (let i = 0; i < 4; i++) {
    const a = points[i];
    const b = points[(i + 1) % 4];
    const c = points[(i + 2) % 4];
    const cross = (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x);
    if (cross === 0) continue;
    const currentSign = cross > 0 ? 1 : -1;
    if (sign === 0) {
      sign = currentSign;
    } else if (sign !== currentSign) {
      return false;
    }
  }
  return sign !== 0;
}

/** Solve 8×8 homography mapping src quad → axis-aligned dst rectangle. */
export function computeHomography(srcCorners, dstWidth, dstHeight) {
  const src = orderQuadCorners(srcCorners);
  const dst = [
    { x: 0, y: 0 },
    { x: dstWidth, y: 0 },
    { x: dstWidth, y: dstHeight },
    { x: 0, y: dstHeight },
  ];

  const rows = [];
  for (let i = 0; i < 4; i++) {
    const { x, y } = src[i];
    const { x: u, y: v } = dst[i];
    rows.push([x, y, 1, 0, 0, 0, -u * x, -u * y, u]);
    rows.push([0, 0, 0, x, y, 1, -v * x, -v * y, v]);
  }

  const h = solveLinearSystem(rows);
  return [
    [h[0], h[1], h[2]],
    [h[3], h[4], h[5]],
    [h[6], h[7], 1],
  ];
}

function solveLinearSystem(rows) {
  const matrix = rows.map((row) => row.slice());
  const size = 8;

  for (let col = 0; col < size; col++) {
    let pivotRow = col;
    for (let row = col + 1; row < size; row++) {
      if (Math.abs(matrix[row][col]) > Math.abs(matrix[pivotRow][col])) {
        pivotRow = row;
      }
    }

    if (Math.abs(matrix[pivotRow][col]) < 1e-9) {
      throw new Error('Homography system is singular');
    }

    [matrix[col], matrix[pivotRow]] = [matrix[pivotRow], matrix[col]];

    const pivot = matrix[col][col];
    for (let j = col; j <= size; j++) {
      matrix[col][j] /= pivot;
    }

    for (let row = 0; row < size; row++) {
      if (row === col) continue;
      const factor = matrix[row][col];
      for (let j = col; j <= size; j++) {
        matrix[row][j] -= factor * matrix[col][j];
      }
    }
  }

  return matrix.map((row) => row[size]);
}

function applyHomographyInverse(matrix, x, y) {
  const denom = matrix[2][0] * x + matrix[2][1] * y + matrix[2][2];
  const srcX = (matrix[0][0] * x + matrix[0][1] * y + matrix[0][2]) / denom;
  const srcY = (matrix[1][0] * x + matrix[1][1] * y + matrix[1][2]) / denom;
  return { x: srcX, y: srcY };
}

function invert3x3(matrix) {
  const [
    [a, b, c],
    [d, e, f],
    [g, h, i],
  ] = matrix;

  const A = e * i - f * h;
  const B = -(d * i - f * g);
  const C = d * h - e * g;
  const D = -(b * i - c * h);
  const E = a * i - c * g;
  const F = -(a * h - b * g);
  const G = b * f - c * e;
  const H = -(a * f - c * d);
  const I = a * e - b * d;
  const det = a * A + b * B + c * C;

  if (Math.abs(det) < 1e-9) {
    throw new Error('Homography matrix is not invertible');
  }

  const invDet = 1 / det;
  return [
    [A * invDet, D * invDet, G * invDet],
    [B * invDet, E * invDet, H * invDet],
    [C * invDet, F * invDet, I * invDet],
  ];
}

function sampleBilinear(data, width, height, x, y) {
  const clampedX = Math.max(0, Math.min(width - 1, x));
  const clampedY = Math.max(0, Math.min(height - 1, y));
  const x0 = Math.floor(clampedX);
  const y0 = Math.floor(clampedY);
  const x1 = Math.min(x0 + 1, width - 1);
  const y1 = Math.min(y0 + 1, height - 1);
  const tx = clampedX - x0;
  const ty = clampedY - y0;

  const idx = (row, col) => (row * width + col) * 4;
  const sample = (row, col) => {
    const base = idx(row, col);
    return [data[base], data[base + 1], data[base + 2], data[base + 3]];
  };

  const c00 = sample(y0, x0);
  const c10 = sample(y0, x1);
  const c01 = sample(y1, x0);
  const c11 = sample(y1, x1);

  const out = [0, 0, 0, 255];
  for (let channel = 0; channel < 3; channel++) {
    const top = c00[channel] * (1 - tx) + c10[channel] * tx;
    const bottom = c01[channel] * (1 - tx) + c11[channel] * tx;
    out[channel] = Math.round(top * (1 - ty) + bottom * ty);
  }
  return out;
}

/**
 * Perspective-correct a card region from the live video frame.
 * Returns a JPEG data URL sized to the card aspect ratio.
 */
export function warpCardCaptureFromVideo(
  video,
  canvas,
  corners,
  { jpegQuality = 0.92, maxWidth = 480 } = {}
) {
  if (!video || !canvas || !corners || corners.length !== 4) {
    throw new Error('warpCardCaptureFromVideo requires video, canvas, and four corners');
  }

  const ordered = orderQuadCorners(corners);
  const bounds = boundsFromCorners(ordered);
  const outputHeight = Math.max(1, Math.round(maxWidth / CARD_ASPECT_RATIO));
  const outputWidth = maxWidth;

  const homography = computeHomography(ordered, outputWidth, outputHeight);
  const inverse = invert3x3(homography);

  const sourceCanvas = document.createElement('canvas');
  sourceCanvas.width = video.videoWidth;
  sourceCanvas.height = video.videoHeight;
  const sourceCtx = sourceCanvas.getContext('2d');
  sourceCtx.drawImage(video, 0, 0);
  const sourceData = sourceCtx.getImageData(0, 0, sourceCanvas.width, sourceCanvas.height).data;

  canvas.width = outputWidth;
  canvas.height = outputHeight;
  const ctx = canvas.getContext('2d');
  const output = ctx.createImageData(outputWidth, outputHeight);

  for (let y = 0; y < outputHeight; y++) {
    for (let x = 0; x < outputWidth; x++) {
      const mapped = applyHomographyInverse(inverse, x, y);
      if (
        mapped.x < bounds.x - 5 ||
        mapped.y < bounds.y - 5 ||
        mapped.x > bounds.x + bounds.width + 5 ||
        mapped.y > bounds.y + bounds.height + 5
      ) {
        continue;
      }

      const rgba = sampleBilinear(
        sourceData,
        sourceCanvas.width,
        sourceCanvas.height,
        mapped.x,
        mapped.y
      );
      const outIdx = (y * outputWidth + x) * 4;
      output.data[outIdx] = rgba[0];
      output.data[outIdx + 1] = rgba[1];
      output.data[outIdx + 2] = rgba[2];
      output.data[outIdx + 3] = 255;
    }
  }

  ctx.putImageData(output, 0, 0);
  return canvas.toDataURL('image/jpeg', jpegQuality);
}
