import sharp from "sharp";

export type WorkbookSourceImage = {
  bytes: Uint8Array;
  contentType: string;
  width?: number | null;
  height?: number | null;
  description: string;
};

export type WorkbookImageNormalizationOptions = {
  maxDimension?: number;
  outputFormat?: "png" | "jpeg";
  jpegQuality?: number;
};

const NORMALIZABLE_IMAGE = /^image\/(?:jpe?g|png|webp|gif|tiff?|avif|heic|heif)$/i;

function hasImageSignature(bytes: Uint8Array) {
  if (bytes.length < 4) return false;
  if (bytes[0] === 0xff && bytes[1] === 0xd8) return true;
  if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
    return true;
  }
  if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) return true;
  if (
    bytes.length >= 12 &&
    String.fromCharCode(...bytes.slice(0, 4)) === "RIFF" &&
    String.fromCharCode(...bytes.slice(8, 12)) === "WEBP"
  ) {
    return true;
  }
  return (
    (bytes[0] === 0x49 && bytes[1] === 0x49 && bytes[2] === 0x2a && bytes[3] === 0x00) ||
    (bytes[0] === 0x4d && bytes[1] === 0x4d && bytes[2] === 0x00 && bytes[3] === 0x2a)
  );
}

/**
 * Excel viewers disagree on whether JPEG EXIF orientation should be applied.
 * Bake that orientation into the pixels before the image enters the workbook.
 * Callers may also resize and encode large source images as JPEG to keep the
 * generated workbook within the upload limit.
 */
export async function normalizeWorkbookImage(
  image: WorkbookSourceImage,
  options: WorkbookImageNormalizationOptions = {},
): Promise<WorkbookSourceImage> {
  if (!NORMALIZABLE_IMAGE.test(image.contentType) && !hasImageSignature(image.bytes)) {
    return image;
  }
  try {
    let normalized = sharp(image.bytes, { animated: false }).rotate();
    if (options.maxDimension && options.maxDimension > 0) {
      normalized = normalized.resize({
        width: Math.floor(options.maxDimension),
        height: Math.floor(options.maxDimension),
        fit: "inside",
        withoutEnlargement: true,
      });
    }
    normalized =
      options.outputFormat === "jpeg"
        ? normalized
            .flatten({ background: "#ffffff" })
            .jpeg({
              quality: Math.min(95, Math.max(50, options.jpegQuality ?? 82)),
              mozjpeg: true,
            })
        : normalized.png({ compressionLevel: 6 });
    const { data, info } = await normalized.toBuffer({ resolveWithObject: true });
    return {
      ...image,
      bytes: new Uint8Array(data),
      contentType:
        options.outputFormat === "jpeg" ? "image/jpeg" : "image/png",
      width: info.width,
      height: info.height,
    };
  } catch {
    return image;
  }
}
