// OCR preprocessing for artifact crops (ADR-009). Tesseract reads a clean, high
// contrast, dark-text-on-light image far more reliably than Genshin's native
// bright-text-on-dark UI. This binarizes a crop with Otsu thresholding and (by
// default) inverts, because artifact text is the bright foreground.
//
// Works on a raw BGRA bitmap (Electron NativeImage.getBitmap() layout on
// Windows). Kept pure and channel-order-agnostic for luminance so it is unit
// testable without Electron. Upscaling is done separately via NativeImage.resize
// before this runs - interpolated upscaling of small crops is a big Tesseract win
// and NativeImage does it better than hand-rolled JS.

export interface Bitmap {
  data: Uint8Array | Buffer;
  width: number;
  height: number;
}

export interface BinarizeOptions {
  /** Artifact text is the bright foreground, so invert to dark-on-light. */
  invertBrightForeground?: boolean;
  /** Override Otsu with a fixed 0-255 luminance threshold. */
  threshold?: number;
  /** Optional contrast adjustment in the usual -255..255 image-processing range. */
  contrast?: number;
}

const BYTES_PER_PIXEL = 4;

// Rec. 601 luma. Channel order does not matter for a weighted sum as long as we
// read the same three bytes; BGRA and RGBA give the same luminance here because
// we weight by position-independent coefficients applied to the actual R/G/B.
function luminanceAt(data: Uint8Array | Buffer, index: number): number {
  // NativeImage on Windows is BGRA: byte0=B, byte1=G, byte2=R.
  const b = data[index];
  const g = data[index + 1];
  const r = data[index + 2];
  return 0.299 * r + 0.587 * g + 0.114 * b;
}

function adjustContrast(value: number, contrast = 0) {
  if (!contrast) return value;
  const safeContrast = Math.max(-255, Math.min(255, contrast));
  const factor = (259 * (safeContrast + 255)) / (255 * (259 - safeContrast));
  return Math.max(0, Math.min(255, factor * (value - 128) + 128));
}

export function computeLuminanceHistogram(bitmap: Bitmap, options: Pick<BinarizeOptions, "contrast"> = {}): number[] {
  const histogram = new Array<number>(256).fill(0);
  const { data, width, height } = bitmap;
  const pixels = width * height;
  for (let pixel = 0; pixel < pixels; pixel++) {
    const value = Math.round(adjustContrast(luminanceAt(data, pixel * BYTES_PER_PIXEL), options.contrast));
    histogram[Math.max(0, Math.min(255, value))]++;
  }
  return histogram;
}

// Otsu's method: pick the threshold that maximizes between-class variance.
export function otsuThreshold(histogram: readonly number[]): number {
  const total = histogram.reduce((sum, count) => sum + count, 0);
  if (total === 0) return 127;

  let sumAll = 0;
  for (let level = 0; level < 256; level++) sumAll += level * histogram[level];

  let sumBackground = 0;
  let weightBackground = 0;
  let maxVariance = -1;
  let threshold = 127;

  for (let level = 0; level < 256; level++) {
    weightBackground += histogram[level];
    if (weightBackground === 0) continue;
    const weightForeground = total - weightBackground;
    if (weightForeground === 0) break;

    sumBackground += level * histogram[level];
    const meanBackground = sumBackground / weightBackground;
    const meanForeground = (sumAll - sumBackground) / weightForeground;
    const betweenVariance = weightBackground * weightForeground * (meanBackground - meanForeground) ** 2;

    if (betweenVariance > maxVariance) {
      maxVariance = betweenVariance;
      threshold = level;
    }
  }

  return threshold;
}

export function binarizeForOcr(bitmap: Bitmap, options: BinarizeOptions = {}): Bitmap {
  const { data, width, height } = bitmap;
  const invert = options.invertBrightForeground ?? true;
  const threshold = options.threshold ?? otsuThreshold(computeLuminanceHistogram(bitmap, options));

  const output = Buffer.alloc(width * height * BYTES_PER_PIXEL);
  const pixels = width * height;
  for (let pixel = 0; pixel < pixels; pixel++) {
    const index = pixel * BYTES_PER_PIXEL;
    const isBright = adjustContrast(luminanceAt(data, index), options.contrast) > threshold;
    // Bright foreground text -> black; dark background -> white (inverted).
    const value = invert ? (isBright ? 0 : 255) : (isBright ? 255 : 0);
    output[index] = value;
    output[index + 1] = value;
    output[index + 2] = value;
    output[index + 3] = 255;
  }

  return { data: output, width, height };
}
