import fs from "fs";
import path from "path";
import sharp from "sharp";

/**
 * Recursively search an object for any property called `icon`
 * and push the string value to the supplied Set.
 *
 * @param obj  The object to search (may be nested)
 * @param set  The Set that collects unique icon strings
 */
export function walkForIcons(obj: unknown, set: Set<string>) {
  if (obj && typeof obj === "object") {
    if (Array.isArray(obj)) {
      for (const item of obj) walkForIcons(item, set);
    } else {
      for (const [key, value] of Object.entries(obj)) {
        if (key === "icon" && typeof value === "string") {
          set.add(value);
        } else {
          walkForIcons(value, set);
        }
      }
    }
  }
}

/**
 * Metadata for a single icon.
 * All icons are guaranteed to be square.
 */
interface IconMeta {
  name: string; // icon name (without extension)
  size: number; // width = height
  buffer: Buffer;
  file: string; // absolute path to the PNG
}
/**
 * Position of an icon inside its texture.
 */
export interface IconPosition {
  name: string;
  x: number;
  y: number;
  size: number;
}
async function validateIcons(icons: string[], scriptOutputPath: string) {
  const validIcons: IconMeta[] = [];
  for (const icon of icons) {
    const absPath = path.resolve(scriptOutputPath, icon);

    // Skip if file does not exist or is not a PNG
    if (!fs.existsSync(absPath) || path.extname(absPath).toLowerCase() !== ".png") continue;

    const stat = fs.statSync(absPath);
    if (stat.size === 0) continue; // skip empty files

    const buffer = fs.readFileSync(absPath);
    const meta = await sharp(buffer).metadata();

    if (!meta.width || !meta.height || meta.width !== meta.height) continue; // skip non‑square

    validIcons.push({
      name: icon,
      file: absPath,
      buffer,
      size: meta.width,
    });
  }
  return validIcons;
}
function groupBySize(metas: IconMeta[]): Record<string, IconMeta[]> {
  const groups: Record<string, IconMeta[]> = {};
  for (const m of metas) {
    const key = String(m.size);
    groups[key] ??= [];
    groups[key].push(m);
  }
  return groups;
}
/**
 * Packs a set of square icons of the same size into a single texture image.
 *
 * The icons are laid out in a regular grid (rows × columns) where the
 * number of columns is the ceiling of the square root of the icon count,
 * and the number of rows is the smallest integer that can hold all icons.
 *
 * @param icons      Array of {@link IconMeta} objects that all share the same `size`.
 * @param iconSize   Width (and height) of each icon in pixels.
 * @returns An object containing:
 *   - `textureBuffer`:  Buffer with the resulting WEBP image that contains
 *     all icons arranged in the grid.
 *   - `iconMap`:        Mapping from icon name to its position (`x`, `y`,
 *     `width`, `height`) inside the texture.  `width` and `height` are equal
 *     to `iconSize`.
 *
 * @remarks
 *   * All icons are guaranteed to be square; `iconSize` is the common
 *     dimension of every icon in `icons`.
 *   * The texture background is fully transparent.
 */
async function createTextureMap(
  icons: IconMeta[],
  iconSize: number
): Promise<{ textureBuffer: Buffer; iconMap: Record<string, IconPosition> }> {
  const n = icons.length;
  const cols = Math.ceil(Math.sqrt(n));
  const rows = Math.ceil(n / cols);
  const canvasW = cols * iconSize;
  const canvasH = rows * iconSize;

  const composites: sharp.OverlayOptions[] = [];
  const map: Record<string, IconPosition> = {};

  icons.forEach((icon, idx) => {
    const x = (idx % cols) * iconSize;
    const y = Math.floor(idx / cols) * iconSize;
    composites.push({ input: icon.file, left: x, top: y });
    map[icon.name] = { name: icon.name, x, y, size: iconSize };
  });

  const buffer = await sharp({
    create: {
      width: canvasW,
      height: canvasH,
      channels: 4,
      background: { r: 0, g: 0, b: 0, alpha: 0 },
    },
  })
    .composite(composites)
    .webp({ lossless: true })
    .toBuffer();

  return { textureBuffer: buffer, iconMap: map };
}
/**
 * Build a texture map from a list of PNG icons.
 *
 * @param icons            Array of icon file paths (relative to scriptOutputPath)
 * @param scriptOutputPath Base directory where the icons are located
 * @param outputDir        Destination directory for the generated files.
 *
 * The function will:
 *   • Filter out non‑PNG files, missing files or empty entries.
 *   • Ensure every PNG is square and that group icons by dimensions.
 *   • Pack the icons into a single WebP texture (grid layout).
 *   • Output `icon_{size}.webp` and `iconMap.json` into `outputDir`.
 */
export async function buildIconTextureMap(icons: string[], scriptOutputPath: string, outputDir: string): Promise<void> {
  const validIcons = await validateIcons(icons, scriptOutputPath);
  if (validIcons.length === 0) {
    throw new Error("No valid PNG icons found.");
  }

  const iconPositions: IconPosition[] = [];
  for (let [size, group] of Object.entries(groupBySize(validIcons))) {
    const outTexturePath = path.join(outputDir, `icon_${size}.webp`);
    const { textureBuffer, iconMap } = await createTextureMap(group, parseInt(size));
    fs.writeFileSync(outTexturePath, textureBuffer);
    iconPositions.push(...Object.values(iconMap));
  }

  const outMapPath = path.join(outputDir, "iconMap.json");
  fs.writeFileSync(outMapPath, JSON.stringify(iconPositions, null, 2), "utf-8");
}
