// Run from the ImageGuide checkout with its dependencies installed.
// For the old AVIF run, set IMAGEGUIDE_SHARP_MODULE to Sharp 0.34.5's absolute entry path.
import assert from 'node:assert/strict';
import { readFile, writeFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { cpus } from 'node:os';

const { default: sharp } = await import(process.env.IMAGEGUIDE_SHARP_MODULE || 'sharp');
sharp.cache(false);
sharp.concurrency(1);
const path = (name) => fileURLToPath(new URL(name, import.meta.url));
const save = (name, value) => writeFile(path(name), JSON.stringify(value, null, 2) + '\n');
const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
const mode = process.argv[2];

if (mode === 'fixtures') {
  await sharp(path('shirt-source.jpg')).resize({ width: 800 }).png().toFile(path('photo.png'));
  await sharp(Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">
    <rect width="800" height="600" fill="#fff"/>
    <text x="40" y="90" font-family="sans-serif" font-size="32">Image export settings</text>
    <text x="40" y="140" font-family="sans-serif" font-size="16">Small text, colored edges and fine rules</text>
    <path d="M40 180H760 M40 182H760 M40 184H760" stroke="#1970bb"/>
    <rect x="40" y="240" width="300" height="220" fill="#c23548"/>
    <rect x="342" y="240" width="300" height="220" fill="#1d8380"/>
  </svg>`)).png().toFile(path('text.png'));
  await sharp(Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">
    <defs><linearGradient id="g"><stop stop-color="#16283e"/><stop offset="1" stop-color="#e0aab2"/></linearGradient></defs>
    <rect width="800" height="600" fill="url(#g)"/>
  </svg>`)).png().toFile(path('gradient.png'));
  await sharp(Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">
    <circle cx="400" cy="300" r="220" fill="#167b91" fill-opacity="0.55"/>
    <circle cx="500" cy="300" r="180" fill="#e4a329" fill-opacity="0.7"/>
  </svg>`)).png().toFile(path('alpha.png'));
  for (const size of [1000, 2000, 4000]) {
    await sharp({ create: { width: size, height: size, channels: 4, background: '#448899' } })
      .png().toFile(path(`memory-${size}.png`));
  }
  console.log('Created bounded, lossless experiment fixtures');
} else if (mode === 'avif') {
  const rows = [];
  for (const name of ['photo', 'text', 'gradient', 'alpha']) {
    const input = await readFile(path(`${name}.png`));
    const options = { quality: 50, effort: 4, chromaSubsampling: '4:4:4', bitdepth: 8 };
    await sharp(input).avif(options).toBuffer(); // Warm-up excluded from timings.
    const times = [];
    let output;
    for (let repeat = 0; repeat < 5; repeat++) {
      const start = performance.now();
      output = await sharp(input).avif(options).toBuffer();
      times.push(performance.now() - start);
    }
    const file = `${name}-${sharp.versions.sharp}.avif`;
    await writeFile(path(file), output);
    const metadata = await sharp(output).metadata();
    assert.equal(metadata.width, 800);
    assert.equal(metadata.hasAlpha, (await sharp(input).metadata()).hasAlpha);
    await sharp(output).extract({ left: 250, top: 200, width: 300, height: 250 })
      .png().toFile(path(`${name}-${sharp.versions.sharp}-detail.png`));
    rows.push({ name, file, sourceSha256: hash(input), sha256: hash(output), bytes: output.length,
      medianMs: +[...times].sort((a, b) => a - b)[2].toFixed(2), timesMs: times, options });
  }
  await save(`avif-${sharp.versions.sharp}.json`, {
    measuredAt: new Date().toISOString(), node: process.version, platform: process.platform,
    architecture: process.arch, cpu: cpus()[0].model, concurrency: 1, cache: false,
    versions: sharp.versions, rows,
  });
  console.log(rows.map(({ name, bytes, medianMs }) => ({ name, bytes, medianMs })));
} else if (mode === 'jpeg') {
  const source = await readFile(path('photo.png'));
  const options = { quality: 60, progressive: true, mozjpeg: true, chromaSubsampling: '4:2:0' };
  const rows = [];
  const decode = (input) => sharp(input).removeAlpha().raw().toBuffer();
  const original = await decode(source);
  for (const branch of ['fresh', 'resaved', 'edited']) {
    let previous = source;
    for (let generation = 1; generation <= 10; generation++) {
      let input = branch === 'fresh' ? source : previous;
      if (branch === 'edited' && generation > 1) {
        // A one-pixel translation changes JPEG's block alignment without a resize.
        const meta = await sharp(input).metadata();
        input = await sharp(input).extract({ left: 1, top: 0, width: meta.width - 1, height: meta.height })
          .extend({ right: 1, left: 0, top: 0, bottom: 0, background: '#fff' }).png().toBuffer();
      }
      const output = await sharp(input).jpeg(options).toBuffer();
      const file = `jpeg-${branch}-${generation}.jpg`;
      await writeFile(path(file), output);
      const pixels = await decode(output);
      assert.equal(pixels.length, original.length);
      // Only comparable to the unshifted source for the two unchanged branches.
      const mae = branch === 'edited' ? null : pixels.reduce((sum, value, i) => sum + Math.abs(value - original[i]), 0) / pixels.length;
      rows.push({ branch, generation, file, bytes: output.length, sha256: hash(output), meanAbsoluteError: mae });
      if ([1, 10].includes(generation)) {
        await sharp(output).extract({ left: 360, top: 410, width: 220, height: 220 })
          .png().toFile(path(`jpeg-${branch}-${generation}-detail.png`));
      }
      previous = output;
    }
  }
  assert.equal(new Set(rows.filter(r => r.branch === 'fresh').map(r => r.sha256)).size, 1);
  await save('jpeg.json', { measuredAt: new Date().toISOString(), versions: sharp.versions, sourceSha256: hash(source), options, rows });
  console.log(rows.filter(r => [1, 5, 10].includes(r.generation)));
} else if (mode === 'metadata') {
  const xmp = `<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="" xmlns:Iptc4xmpExt="http://iptc.org/std/Iptc4xmpExt/2008-02-29/"><Iptc4xmpExt:DigitalSourceType>http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia</Iptc4xmpExt:DigitalSourceType></rdf:Description></rdf:RDF></x:xmpmeta>`;
  // This is a metadata fixture, not a claim that the shirt photo was AI-generated.
  const input = await sharp({ create: { width: 800, height: 600, channels: 3, background: '#448899' } })
    .withXmp(xmp).jpeg().toBuffer();
  const stripped = await sharp(input).resize(400).jpeg().toBuffer();
  const preserved = await sharp(input).resize(400).keepXmp().jpeg().toBuffer();
  const rows = [];
  for (const [stage, bytes] of [['fixture', input], ['default', stripped], ['keep-xmp', preserved]]) {
    await writeFile(path(`metadata-${stage}.jpg`), bytes);
    const meta = await sharp(bytes).metadata();
    rows.push({ stage, width: meta.width, bytes: bytes.length, hasXmp: Boolean(meta.xmp), xmp: meta.xmp?.toString() || null });
  }
  assert.equal(rows[1].hasXmp, false);
  assert(rows[2].xmp.includes('trainedAlgorithmicMedia'));
  await save('metadata.json', { measuredAt: new Date().toISOString(), versions: sharp.versions, rows });
  console.log(rows.map(({ xmp, ...row }) => row));
} else if (mode === 'memory-child') {
  const size = Number(process.argv[3]);
  assert([1000, 2000, 4000].includes(size));
  const input = await readFile(path(`memory-${size}.png`));
  const before = process.memoryUsage().rss;
  const pixels = await sharp(input, { limitInputPixels: 16_000_000 }).ensureAlpha().raw().toBuffer();
  assert.equal(pixels.length, size * size * 4);
  console.log(JSON.stringify({ size, encodedBytes: input.length, rawBytes: pixels.length,
    rssBefore: before, rssAfter: process.memoryUsage().rss, maxRssKiB: process.resourceUsage().maxRSS }));
} else if (mode === 'memory') {
  const rows = [];
  for (const size of [1000, 2000, 4000]) {
    const result = spawnSync(process.execPath, [path('run.mjs'), 'memory-child', String(size)], { encoding: 'utf8', timeout: 30_000 });
    assert.equal(result.status, 0, result.stderr);
    rows.push(JSON.parse(result.stdout));
  }
  await assert.rejects(sharp(path('memory-4000.png'), { limitInputPixels: 4_000_000 }).raw().toBuffer(), /pixel limit/i);
  await save('memory.json', { measuredAt: new Date().toISOString(), node: process.version, platform: process.platform, versions: sharp.versions, rows });
  console.log(rows);
} else if (mode === 'crops') {
  const input = path('shirt-source.jpg');
  await sharp(input).resize({ width: 600, height: 600, fit: 'contain', background: '#eee' }).webp({ quality: 85 }).toFile(path('shirt-card.webp'));
  await sharp(input).resize({ width: 960, height: 320, fit: 'cover', position: 'centre' }).webp({ quality: 85 }).toFile(path('shirt-hero-center.webp'));
  await sharp(input).extract({ left: 0, top: 390, width: 1152, height: 384 }).resize(960).webp({ quality: 85 }).toFile(path('shirt-hero-detail.webp'));
  await sharp(input).extract({ left: 180, top: 380, width: 800, height: 1000 }).resize(640).webp({ quality: 85 }).toFile(path('shirt-mobile.webp'));
  for (const [name, width, height] of [['shirt-card', 600, 600], ['shirt-hero-center', 960, 320], ['shirt-hero-detail', 960, 320], ['shirt-mobile', 640, 800]]) {
    const meta = await sharp(path(`${name}.webp`)).metadata();
    assert.equal(meta.width, width);
    assert.equal(meta.height, height);
  }
  console.log('Crop dimensions verified');
} else {
  throw new Error('Choose fixtures, avif, jpeg, metadata, memory or crops');
}
