import RNFS from 'react-native-fs';
import UPNG from 'upng-js';
import {
  OpenCV,
  ObjectType,
  DataTypes,
  ColorConversionCodes,
  type Mat,
} from 'react-native-fast-opencv';

export const PNG_EXT = '.png';
/** PNG compression level 0 = lossless */
export const PNG_COMPRESSION = 0;

export function normalizePath(path: string): string {
  return path.startsWith('file://') ? path.slice(7) : path;
}

/** Skia useImage requires a URI; OpenCV / RNFS use bare paths */
export function toSkiaUri(path: string | null | undefined): string | null {
  if (!path) {
    return null;
  }
  if (
    path.startsWith('http://') ||
    path.startsWith('https://') ||
    path.startsWith('data:') ||
    path.startsWith('file://')
  ) {
    return path;
  }
  const normalized = normalizePath(path);
  return `file://${normalized}`;
}

export function toPngFileName(name: string): string {
  const base = name.replace(/\.(jpe?g|webp|gif|bmp|heic|heif)$/i, '');
  return base.toLowerCase().endsWith(PNG_EXT) ? base : `${base}${PNG_EXT}`;
}

export function isPngPath(path: string): boolean {
  return normalizePath(path).toLowerCase().endsWith(PNG_EXT);
}

function hashString(value: string): string {
  let hash = 5381;
  for (let i = 0; i < value.length; i++) {
    hash = Math.imul(hash, 33) ^ value.charCodeAt(i);
  }
  return (hash >>> 0).toString(16);
}

function hashPath(path: string): string {
  return hashString(path);
}

/** Generate fingerprint from file metadata to avoid full-file read (original base64 full hash was extremely slow on 1.5MB images) */
export async function fileContentFingerprint(path: string): Promise<string> {
  const normalized = normalizePath(path);
  const stat = await RNFS.stat(normalized);
  return hashString(`${stat.size}:${stat.mtime ?? stat.ctime}`);
}

function versionedCachePath(cacheFileName: string, fingerprint: string): string {
  const baseName = toPngFileName(cacheFileName).replace(/\.png$/i, '');
  return `${RNFS.CachesDirectoryPath}/${baseName}_${fingerprint}${PNG_EXT}`;
}

async function cleanupStaleVersionedCache(
  cacheFileName: string,
  keepDest: string,
): Promise<void> {
  const baseName = toPngFileName(cacheFileName).replace(/\.png$/i, '');
  const legacyDest = `${RNFS.CachesDirectoryPath}/${toPngFileName(cacheFileName)}`;
  const prefix = `${baseName}_`;

  if (legacyDest !== keepDest && (await RNFS.exists(legacyDest))) {
    await RNFS.unlink(legacyDest);
  }

  const files = await RNFS.readDir(RNFS.CachesDirectoryPath);
  for (const file of files) {
    if (
      file.isFile() &&
      file.name.startsWith(prefix) &&
      file.name.endsWith(PNG_EXT) &&
      file.path !== keepDest
    ) {
      await RNFS.unlink(file.path);
    }
  }
}

/** Any image path → PNG file path under cache dir (copy if already PNG, otherwise decode and save) */
export async function ensurePngFile(
  sourcePath: string,
  cacheFileName: string,
): Promise<string> {
  const src = normalizePath(sourcePath);
  const fingerprint = await fileContentFingerprint(src);
  const dest = versionedCachePath(cacheFileName, fingerprint);

  if (!(await RNFS.exists(src))) {
    throw new Error(`Image does not exist: ${src}`);
  }

  if (src === dest) {
    return dest;
  }

  if (await RNFS.exists(dest)) {
    return dest;
  }

  if (isPngPath(src)) {
    await RNFS.copyFile(src, dest);
    await cleanupStaleVersionedCache(cacheFileName, dest);
    return dest;
  }

  const base64 = await RNFS.readFile(src, 'base64');
  const mat = OpenCV.base64ToMat(base64);
  OpenCV.saveMatToFile(mat, dest, 'png', PNG_COMPRESSION);
  OpenCV.releaseBuffers([mat.id]);
  await cleanupStaleVersionedCache(cacheFileName, dest);
  return dest;
}

/** Generate a stable PNG cache name from the source path */
export function pngCacheName(sourcePath: string, prefix: string): string {
  return `${prefix}_${hashPath(normalizePath(sourcePath))}${PNG_EXT}`;
}

const DERIVED_CACHE_PREFIXES = [
  'seg_lowfreq_',
  'seg_highfreq_',
  'imread_',
  'img_',
  'tmp_',
];

/** Clean up segmentation/OpenCV derived cache, keep original image and mask source files */
export async function clearDerivedImageCache(): Promise<number> {
  const files = await RNFS.readDir(RNFS.CachesDirectoryPath);
  let removed = 0;

  for (const file of files) {
    if (!file.isFile() || !file.name.endsWith(PNG_EXT)) {
      continue;
    }
    if (!DERIVED_CACHE_PREFIXES.some(prefix => file.name.startsWith(prefix))) {
      continue;
    }
    await RNFS.unlink(file.path);
    removed += 1;
  }

  return removed;
}

const CV_MAT_DEPTH_MASK = 7;

function matDepth(type: number): number {
  return type & CV_MAT_DEPTH_MASK;
}

function matChannelsFromType(type: number): number {
  return (type >> 3) + 1;
}

type PngHeader = {
  width: number;
  height: number;
  bitDepth: number;
  colorType: number; // 0=Gray 2=RGB 3=Palette 4=GrayAlpha 6=RGBA
};

function pngColorTypeToChannels(colorType: number): number {
  switch (colorType) {
    case 0:
      return 1;
    case 2:
    case 3:
      return 3;
    case 4:
      return 2;
    case 6:
      return 4;
    default:
      return 3;
  }
}

/** Parse PNG IHDR from base64 (without OpenCV), used as fallback when 16-bit Mat toJSValue crashes */
export function readPngHeaderFromBase64(base64: string): PngHeader {
  // PNG signature 8 bytes + IHDR length 4 + "IHDR" 4 + width 4 + height 4 + bit depth 1 + color type 1 = 26 bytes
  // base64 every 3 bytes → 4 chars, take first 40 chars covering ~30 bytes
  const headerPart = base64.slice(0, 40);
  const binary = atob(headerPart);

  function readUint32BE(offset: number): number {
    return (
      ((binary.charCodeAt(offset) << 24) |
        (binary.charCodeAt(offset + 1) << 16) |
        (binary.charCodeAt(offset + 2) << 8) |
        binary.charCodeAt(offset + 3)) >>>
      0
    );
  }

  // Verify PNG signature
  const sig = [137, 80, 78, 71, 13, 10, 26, 10];
  for (let i = 0; i < 8; i++) {
    if (binary.charCodeAt(i) !== sig[i]) {
      throw new Error('Invalid PNG file (signature mismatch)');
    }
  }

  const width = readUint32BE(16);
  const height = readUint32BE(20);
  const bitDepth = binary.charCodeAt(24);
  const colorType = binary.charCodeAt(25);

  if (width === 0 || height === 0) {
    throw new Error(`Invalid PNG size: ${width}x${height}`);
  }

  return { width, height, bitDepth, colorType };
}

/** 16-bit / float Mat → 8-bit (fast-opencv toJSValue truncates high bits before patch).
 *  Semantic mask PNGs use 16-bit RGB where the label (0-255) is stored as value×257;
 *  use 1/257 scaling for 3+ channel 16-bit cases so the resulting 8-bit channels contain
 *  the original semantic label values (consistent with the native ensure8U patch).
 *
 *  pngHeader (optional): when toJSValue crashes due to 16-bit Mat, use PNG file header info
 *  for fallback conversion, bypassing native toJSValue call.
 */
export function ensureMat8U(
  srcMat: Mat,
  pngHeader?: PngHeader,
): {
  mat: Mat;
  extraReleaseIds: string[];
} {
  let info: { rows: number; cols: number; type: number } | undefined;
  try {
    info = OpenCV.toJSValue(srcMat) as unknown as {
      rows: number;
      cols: number;
      type: number;
    };
  } catch (toJsError) {
    if (!pngHeader) {
      throw new Error(
        `OpenCV toJSValue failed (mat id=${srcMat?.id ?? 'unknown'}): ${(toJsError as Error).message}`,
      );
    }

    // Fallback path: 16-bit PNG Mat cannot read metadata via toJSValue,
    // use width/height and bit depth info from PNG file header for convertTo
    const { width: cols, height: rows, bitDepth, colorType } = pngHeader;
    const ch = pngColorTypeToChannels(colorType);

    if (bitDepth <= 8) {
      // 8-bit or lower, no convertTo needed, return as-is
      return { mat: srcMat, extraReleaseIds: [] };
    }

    const outType =
      ch === 1
        ? DataTypes.CV_8UC1
        : ch === 4
          ? DataTypes.CV_8UC4
          : DataTypes.CV_8UC3;
    const dstMat = OpenCV.createObject(
      ObjectType.Mat,
      rows,
      cols,
      outType,
    ) as Mat;

    // 16-bit RGB semantic mask: label value = original value / 257
    const alpha = ch >= 3 ? 1 / 257 : 255 / 65535;
    OpenCV.invoke('convertTo', srcMat, dstMat, outType, alpha, 0);
    return { mat: dstMat, extraReleaseIds: [dstMat.id] };
  }

  if (matDepth(info.type) === DataTypes.CV_8U) {
    return { mat: srcMat, extraReleaseIds: [] };
  }

  const rows = info.rows;
  const cols = info.cols;
  const ch = matChannelsFromType(info.type);
  const outType =
    ch === 1
      ? DataTypes.CV_8UC1
      : ch === 4
        ? DataTypes.CV_8UC4
        : ch === 2
          ? DataTypes.CV_8UC2
          : DataTypes.CV_8UC3;
  const dstMat = OpenCV.createObject(
    ObjectType.Mat,
    rows,
    cols,
    outType,
  ) as Mat;
  const depth = matDepth(info.type);
  let alpha = 1;
  if (depth === DataTypes.CV_16U || depth === DataTypes.CV_16S) {
    // Special case for semantic mask 16-bit "RGB label" encoding (value × 257)
    alpha = ch >= 3 ? 1 / 257 : 255 / 65535;
  } else if (depth === DataTypes.CV_32F || depth === DataTypes.CV_64F) {
    alpha = 255;
  }
  OpenCV.invoke('convertTo', srcMat, dstMat, outType, alpha, 0);
  return { mat: dstMat, extraReleaseIds: [dstMat.id] };
}

/**
 * JS fallback PNG decoder (OpenCV base64ToMat may throw HostFunction for 16-bit PNGs).
 * 16-bit RGB semantic masks use value×257 encoding → UPNG.toRGBA8 high byte = original 8-bit label.
 */
function decodePngToBgrJS(base64: string): {
  buffer: Uint8Array;
  cols: number;
  rows: number;
} {
  const atobFn = (globalThis as { atob?: (data: string) => string }).atob;
  if (!atobFn) {
    throw new Error('JS PNG fallback: atob unavailable');
  }
  const binary = atobFn(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }

  const decoded = UPNG.decode(bytes);
  const rgbaFrames = UPNG.toRGBA8(decoded);
  const rgbaBuf = new Uint8Array(rgbaFrames[0] as ArrayBuffer);
  const w = decoded.width;
  const h = decoded.height;
  const pixelCount = w * h;

  // RGBA → BGR (drop alpha)
  const bgr = new Uint8Array(pixelCount * 3);
  for (let i = 0; i < pixelCount; i++) {
    const si = i * 4;
    const di = i * 3;
    bgr[di] = rgbaBuf[si + 2] as number;     // B ← R
    bgr[di + 1] = rgbaBuf[si + 1] as number; // G
    bgr[di + 2] = rgbaBuf[si] as number;     // R ← B
  }

  return { buffer: bgr, cols: w, rows: h };
}

/** Only check PNG magic bytes (89 50 4E 47) — agnostic to bit depth.
 *  OpenCV bridge may throw HostFunction for both 8-bit and 16-bit PNGs.
 *  Fall back to UPNG pure-JS decode for any valid PNG (supports all variants). */
function isPngByBase64Magic(base64: string): boolean {
  if (!base64 || base64.length < 8) {
    return false;
  }
  try {
    const atobFn = (globalThis as { atob?: (data: string) => string }).atob;
    if (!atobFn) {
      return false;
    }
    const binary = atobFn(base64.slice(0, 12)); // 8 bytes → 12 base64 chars
    return (
      binary.charCodeAt(0) === 0x89 &&
      binary.charCodeAt(1) === 0x50 &&
      binary.charCodeAt(2) === 0x4e &&
      binary.charCodeAt(3) === 0x47
    );
  } catch {
    return false;
  }
}

/** base64 PNG → continuous BGR buffer (OpenCV native decode, bypassing JS atob + upng) */
function decodeBase64PngToBgr(base64: string): {
  buffer: Uint8Array;
  cols: number;
  rows: number;
} {
  let srcMat: Mat;
  try {
    srcMat = OpenCV.base64ToMat(base64);
  } catch (e) {
    // OpenCV bridge may throw HostFunction for any PNG (8-bit or 16-bit).
    // Fall back to pure JS decode whenever the file header is PNG.
    if (isPngByBase64Magic(base64)) {
      try {
        return decodePngToBgrJS(base64);
      } catch (jsError) {
        throw new Error(
          `JS PNG fallback also failed (base64 length ${base64?.length ?? 0}): ${(jsError as Error).message}`,
        );
      }
    }
    throw new Error(
      `OpenCV base64ToMat failed (base64 length ${base64?.length ?? 0}): ${(e as Error).message}`,
    );
  }
  if (!srcMat || typeof srcMat.id !== 'string' || !srcMat.id) {
    throw new Error(
      `OpenCV base64ToMat returned invalid Mat (base64 length ${base64?.length ?? 0})`,
    );
  }
  const releaseIds: string[] = [srcMat.id];
  try {
    // Parse PNG file header (width, height, bit depth) first, for ensureMat8U fallback when 16-bit toJSValue crashes
    let pngHeader: PngHeader | undefined;
    try {
      pngHeader = readPngHeaderFromBase64(base64);
    } catch {
      // Not a PNG or parse failed, skip header to keep original behavior
    }

    let workMat: Mat;
    let extraReleaseIds: string[];
    try {
      const result = ensureMat8U(srcMat, pngHeader);
      workMat = result.mat;
      extraReleaseIds = result.extraReleaseIds;
    } catch (e) {
      throw new Error(`ensureMat8U failed: ${(e as Error).message}`);
    }
    releaseIds.push(...extraReleaseIds);
    const info = OpenCV.toJSValue(workMat);
    const cols = info.cols;
    const rows = info.rows;
    let bgrMat: Mat = workMat;

    if (info.type === DataTypes.CV_8UC4) {
      bgrMat = OpenCV.createObject(
        ObjectType.Mat,
        rows,
        cols,
        DataTypes.CV_8UC3,
      ) as Mat;
      releaseIds.push(bgrMat.id);
      OpenCV.invoke(
        'cvtColor',
        workMat,
        bgrMat,
        ColorConversionCodes.COLOR_BGRA2BGR,
      );
    } else if (info.type === DataTypes.CV_8UC1) {
      bgrMat = OpenCV.createObject(
        ObjectType.Mat,
        rows,
        cols,
        DataTypes.CV_8UC3,
      ) as Mat;
      releaseIds.push(bgrMat.id);
      OpenCV.invoke(
        'cvtColor',
        workMat,
        bgrMat,
        ColorConversionCodes.COLOR_GRAY2BGR,
      );
    }

    const continuous = OpenCV.invoke('clone', bgrMat) as Mat;
    releaseIds.push(continuous.id);
    const { buffer, cols: outCols, rows: outRows } = OpenCV.matToBuffer(
      continuous,
      'uint8',
    );
    return {
      buffer: new Uint8Array(buffer),
      cols: outCols,
      rows: outRows,
    };
  } finally {
    OpenCV.releaseBuffers(releaseIds);
  }
}

export type PngBgrBuffer = {
  buffer: Uint8Array;
  cols: number;
  rows: number;
};

const pngBgrCache = new Map<string, PngBgrBuffer>();
const prewarmInFlight = new Map<string, Promise<PngBgrBuffer>>();

export function prewarmPngBgrCache(paths: string[]): void {
  for (const path of paths) {
    const filePath = normalizePath(path);
    void readPngBgrBuffer(filePath).catch(() => {});
  }
}

export async function prewarmPngBgrCacheAsync(
  paths: string[],
): Promise<void> {
  await Promise.all(paths.map(path => readPngBgrBuffer(normalizePath(path))));
}

export async function pngContentCacheKey(path: string): Promise<string> {
  const stat = await RNFS.stat(normalizePath(path));
  return `${stat.size}:${stat.mtime ?? stat.ctime}`;
}

export async function readPngBgrBuffer(path: string): Promise<PngBgrBuffer> {
  const filePath = normalizePath(path);
  const cacheKey = await pngContentCacheKey(filePath);
  const cached = pngBgrCache.get(cacheKey);
  if (cached) {
    return cached;
  }

  const inflight = prewarmInFlight.get(cacheKey);
  if (inflight) {
    return inflight;
  }

  const loadPromise = (async (): Promise<PngBgrBuffer> => {
    const exists = await RNFS.exists(filePath);
    if (!exists) {
      throw new Error(`Image file does not exist: ${filePath}`);
    }
    const stat = await RNFS.stat(filePath);
    if (stat.size === 0) {
      throw new Error(`Image file is empty (0 bytes): ${filePath}`);
    }
    const base64 = await RNFS.readFile(filePath, 'base64');
    if (!base64 || base64.length === 0) {
      throw new Error(`Read base64 is empty: ${filePath}`);
    }
    let decoded;
    try {
      decoded = decodeBase64PngToBgr(base64);
    } catch (decodeError) {
      throw new Error(
        `PNG decode failed: ${filePath} (${stat.size} bytes) - ${
          (decodeError as Error).message
        }`,
      );
    }
    const entry: PngBgrBuffer = {
      buffer: decoded.buffer,
      cols: decoded.cols,
      rows: decoded.rows,
    };
    pngBgrCache.set(cacheKey, entry);
    return entry;
  })();

  prewarmInFlight.set(cacheKey, loadPromise);
  try {
    const result = await loadPromise;
    return result;
  } finally {
    prewarmInFlight.delete(cacheKey);
  }
}

export function resizeBgrBuffer(
  buffer: Uint8Array,
  srcCols: number,
  srcRows: number,
  dstCols: number,
  dstRows: number,
): Uint8Array {
  if (srcCols === dstCols && srcRows === dstRows) {
    return buffer;
  }

  const out = new Uint8Array(dstCols * dstRows * 3);
  for (let y = 0; y < dstRows; y++) {
    const sy = Math.min(srcRows - 1, Math.floor((y * srcRows) / dstRows));
    for (let x = 0; x < dstCols; x++) {
      const sx = Math.min(srcCols - 1, Math.floor((x * srcCols) / dstCols));
      const si = (sy * srcCols + sx) * 3;
      const di = (y * dstCols + x) * 3;
      out[di] = buffer[si];
      out[di + 1] = buffer[si + 1];
      out[di + 2] = buffer[si + 2];
    }
  }
  return out;
}
