import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import sharp from 'sharp';

// ERROR: `__dirname` is not defined in ES module scope
// https://iamwebwiz.medium.com/how-to-fix-dirname-is-not-defined-in-es-module-scope-34d94a86694d
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const allowedFileExtensions = [
  '.avif',
  '.gif',
  '.heif',
  '.jpeg',
  '.png',
  '.raw',
  '.tiff',
  '.webp',
] as const;
type AllowedFileExtension = (typeof allowedFileExtensions)[number];

const publicDir = path.join(__dirname, '../public');
const cacheFile = path.join(__dirname, '/compressed-images.json');

const KB_IN_BYTES = 1024;
const COMPRESS_CONFIG = {
  avif: {
    chromaSubsampling: '4:4:4',
    effort: 9.0,
  },
  gif: {
    effort: 10.0,
  },
  jpeg: {
    chromaSubsampling: '4:4:4',
    mozjpeg: true,
    trellisQuantisation: true,
    overshootDeringing: true,
    optimiseScans: true,
  },
  png: {
    compressionLevel: 9.0,
    palette: true,
  },
  raw: {},
  tiff: {
    compression: 'lzw',
  },
  webp: {
    effort: 6.0,
  },
};

(async () => {
  let cache: string[] = [];
  const isCacheFileExists = await fs
    .access(cacheFile)
    .then(() => true)
    .catch(() => false);

  if (isCacheFileExists) {
    const cacheFileContent = await fs.readFile(cacheFile, 'utf8');
    cache = JSON.parse(cacheFileContent);
  }

  const images = await recursiveGetImages(publicDir);
  for (const image of images) {
    const extname = path.extname(image).toLowerCase() as AllowedFileExtension;
    if (
      !allowedFileExtensions.includes(extname) ||
      image.includes('node_modules') ||
      image.includes('.astro') ||
      image.includes('.vscode') ||
      image.includes('.git')
    ) {
      continue;
    }

    const stats = await fs.stat(image);
    const relativeImagePath = path.relative(path.join(__dirname, '..'), image);
    if (cache.includes(relativeImagePath)) {
      continue;
    }

    const prevSize = stats.size / KB_IN_BYTES;

    let imageBuffer: Buffer | undefined;
    switch (extname) {
      case '.avif':
        imageBuffer = await sharp(image).avif(COMPRESS_CONFIG.avif).toBuffer();
        break;
      case '.heif':
        imageBuffer = await sharp(image).heif().toBuffer();
        break;
      case '.jpeg':
        imageBuffer = await sharp(image).jpeg(COMPRESS_CONFIG.jpeg).toBuffer();
        break;
      case '.png':
        imageBuffer = await sharp(image).png(COMPRESS_CONFIG.png).toBuffer();
        break;
      case '.raw':
        imageBuffer = await sharp(image).raw().toBuffer();
        break;
      case '.tiff':
        imageBuffer = await sharp(image).tiff(COMPRESS_CONFIG.tiff).toBuffer();
        break;
      case '.webp':
        imageBuffer = await sharp(image).webp(COMPRESS_CONFIG.webp).toBuffer();
        break;
      case '.gif':
        continue;
    }

    if (!imageBuffer) {
      console.error(`❌ ${image} Compressing failed!`);
      continue;
    }

    const newSize = imageBuffer.length / KB_IN_BYTES;
    const diff = prevSize - newSize;
    if (diff <= 0) {
      console.log(`📦 Skipped ${relativeImagePath}`);
      continue;
    }

    const diffPercent = ((diff / prevSize) * 100).toFixed(2);
    console.log(
      `📦 Reduced ${prevSize.toFixed(2)}KB → ${newSize.toFixed(2)}KB (${diff.toFixed(2)}KB, ${diffPercent}%) for ${relativeImagePath}`,
    );

    await fs.writeFile(image, imageBuffer);
    cache.push(relativeImagePath);

    // So that we don't lose the cache if the script crashes
    await fs.writeFile(cacheFile, JSON.stringify(cache, null, 2), 'utf8');
  }

  await fs.writeFile(cacheFile, JSON.stringify(cache, null, 2), 'utf8');
})();

async function recursiveGetImages(dir: string): Promise<string[]> {
  const subdirs = await fs.readdir(dir, { withFileTypes: true });

  const files = await Promise.all(
    subdirs.map((dirent) => {
      const res = path.resolve(dir, dirent.name);
      return dirent.isDirectory() ? recursiveGetImages(res) : res;
    }),
  );

  return Array.prototype.concat(...files);
}