/**
 * Perceptual hashing implementation using DCT (Discrete Cosine Transform).
 * Computes a 192-bit (24 byte) color hash from an image.
 * 
 * The hash is computed by:
 * 1. Resizing to 32x32
 * 2. For each RGB channel:
 *    - Apply 2D DCT
 *    - Extract 8x8 low-frequency coefficients (skip DC)
 *    - Compare each to median -> 63 bits per channel
 * 3. Concatenate R, G, B hashes -> 24 bytes (192 bits)
 */

const DCT_SIZE = 32;
const HASH_SIZE = 8;
const BITS_PER_CHANNEL = 63; // 8x8 - 1 (skip DC)

/**
 * Precomputed cosine values for DCT.
 */
const cosineCache: number[][] = [];

function initCosineCache(): void {
  if (cosineCache.length > 0) return;
  
  for (let i = 0; i < DCT_SIZE; i++) {
    cosineCache[i] = [];
    for (let j = 0; j < DCT_SIZE; j++) {
      cosineCache[i][j] = Math.cos((Math.PI / DCT_SIZE) * (j + 0.5) * i);
    }
  }
}

/**
 * Apply 2D DCT to a matrix.
 */
function applyDCT2D(matrix: number[][]): number[][] {
  initCosineCache();
  
  const result: number[][] = [];
  
  for (let u = 0; u < DCT_SIZE; u++) {
    result[u] = [];
    for (let v = 0; v < DCT_SIZE; v++) {
      let sum = 0;
      for (let i = 0; i < DCT_SIZE; i++) {
        for (let j = 0; j < DCT_SIZE; j++) {
          sum += matrix[i][j] * cosineCache[u][i] * cosineCache[v][j];
        }
      }
      const cu = u === 0 ? 1 / Math.sqrt(2) : 1;
      const cv = v === 0 ? 1 / Math.sqrt(2) : 1;
      result[u][v] = (cu * cv * sum) / 4;
    }
  }
  
  return result;
}

/**
 * Get the median of an array of numbers.
 */
function getMedian(values: number[]): number {
  const sorted = [...values].sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  return sorted.length % 2 !== 0
    ? sorted[mid]
    : (sorted[mid - 1] + sorted[mid]) / 2;
}

/**
 * Convert a BigInt to a Uint8Array of specified length.
 */
function bigintToBytes(value: bigint, length: number): Uint8Array {
  const bytes = new Uint8Array(length);
  for (let i = 0; i < length; i++) {
    bytes[i] = Number((value >> BigInt(i * 8)) & 0xFFn);
  }
  return bytes;
}

/**
 * Compute hash for a single color channel.
 */
function computeChannelHash(channel: number[][]): Uint8Array {
  const dct = applyDCT2D(channel);
  
  // Extract 8x8 low-frequency coefficients, skip DC (0,0)
  const lowFreq: number[] = [];
  for (let i = 0; i < HASH_SIZE; i++) {
    for (let j = 0; j < HASH_SIZE; j++) {
      if (i === 0 && j === 0) continue; // Skip DC component
      lowFreq.push(dct[i][j]);
    }
  }
  
  const median = getMedian(lowFreq);
  
  // Generate 63-bit hash
  let bits = 0n;
  for (let i = 0; i < lowFreq.length; i++) {
    if (lowFreq[i] > median) {
      bits |= 1n << BigInt(i);
    }
  }
  
  return bigintToBytes(bits, 8);
}

/**
 * Extract a color channel from RGBA pixel data.
 * @param pixels RGBA pixel data (width * height * 4)
 * @param width Image width
 * @param height Image height
 * @param channel 0=R, 1=G, 2=B
 */
function extractChannel(
  pixels: Uint8Array | Uint8ClampedArray,
  width: number,
  height: number,
  channel: 0 | 1 | 2
): number[][] {
  const matrix: number[][] = [];
  
  for (let y = 0; y < height; y++) {
    matrix[y] = [];
    for (let x = 0; x < width; x++) {
      const idx = (y * width + x) * 4;
      matrix[y][x] = pixels[idx + channel];
    }
  }
  
  return matrix;
}

/**
 * Compute a 192-bit perceptual color hash from RGBA pixel data.
 * The image should already be resized to 32x32.
 * 
 * @param pixels RGBA pixel data (32 * 32 * 4 = 4096 bytes)
 * @returns 24-byte hash (8 bytes per RGB channel)
 */
export function computeColorHash(pixels: Uint8Array | Uint8ClampedArray): Uint8Array {
  if (pixels.length !== DCT_SIZE * DCT_SIZE * 4) {
    throw new Error(`Expected ${DCT_SIZE * DCT_SIZE * 4} bytes, got ${pixels.length}`);
  }
  
  const rChannel = extractChannel(pixels, DCT_SIZE, DCT_SIZE, 0);
  const gChannel = extractChannel(pixels, DCT_SIZE, DCT_SIZE, 1);
  const bChannel = extractChannel(pixels, DCT_SIZE, DCT_SIZE, 2);
  
  const rHash = computeChannelHash(rChannel);
  const gHash = computeChannelHash(gChannel);
  const bHash = computeChannelHash(bChannel);
  
  // Combine all channels
  const combined = new Uint8Array(24);
  combined.set(rHash, 0);
  combined.set(gHash, 8);
  combined.set(bHash, 16);
  
  return combined;
}

/**
 * Compute Hamming distance between two hashes.
 * Lower distance = more similar.
 */
export function hammingDistance(a: Uint8Array, b: Uint8Array): number {
  if (a.length !== b.length) {
    throw new Error(`Hash length mismatch: ${a.length} vs ${b.length}`);
  }
  
  let distance = 0;
  for (let i = 0; i < a.length; i++) {
    let xor = a[i] ^ b[i];
    while (xor) {
      distance += xor & 1;
      xor >>>= 1;
    }
  }
  
  return distance;
}

/**
 * Convert hash to hex string for display/storage.
 */
export function hashToHex(hash: Uint8Array): string {
  return Array.from(hash)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

/**
 * Convert hex string back to hash.
 */
export function hexToHash(hex: string): Uint8Array {
  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
  }
  return bytes;
}

// Hash algorithm version for migrations
export const HASH_VERSION = 1;

// Matching thresholds
export const MATCH_THRESHOLD = 25; // Max Hamming distance for a match
export const HASH_BITS = 192; // Total bits in hash
