/**
 * Erzeugt `icon.ico` und `icon.png` aus `build-resources/symbol.svg`.
 *
 * ## Warum es dieses Werkzeug gibt
 *
 * Das Programmsymbol ist eine gezeichnete Form, keine Schrift. Damit es
 * überall gleich aussieht, muss es in Bilder umgerechnet werden — und zwar in
 * ein Dutzend Größen, weil Windows je nach Ort eine andere greift: 16 in der
 * Titelleiste, 24 in der Taskleiste, 32 im Explorer, 256 in der großen
 * Kachelansicht. Von Hand ist das genau einmal richtig und danach nie wieder.
 *
 * ## Warum kein Zusatzprogramm
 *
 * Gerastert wird mit demselben Chromium, mit dem das Projekt ohnehin prüft.
 * Eine eigene Abhängigkeit für eine Aufgabe, die alle paar Monate anfällt,
 * müsste im Gate mitgeprüft und in der Lizenzübersicht geführt werden; das
 * steht in keinem Verhältnis. Die Pixel kommen aus einer Leinwand im Browser,
 * die PNG-Kodierung ebenfalls. Zusammengesetzt wird nur der ICO-Behälter —
 * das sind ein Kopf, ein Verzeichnis und die Bilddaten.
 *
 * ## Was in der ICO-Datei steht
 *
 * Bis 96 Bildpunkte liegen die Bilder als unkomprimiertes BMP darin, ab 128
 * als PNG. Das ist die Aufteilung, die auch Symboleditoren wählen: PNG in
 * kleinen Größen versteht zwar jedes heutige Windows, aber BMP versteht auch
 * jedes ältere, und bei 16 x 16 macht die Ersparnis nichts aus.
 *
 * ## Aufruf
 *
 *     npm run symbol
 *
 * Danach steht in `symbol.json` die Prüfsumme der Quelle. `symbol.test.ts`
 * vergleicht sie und schlägt an, wenn jemand die Zeichnung ändert, ohne die
 * Bilder neu zu erzeugen.
 */

import { createHash } from 'node:crypto';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import { chromium } from '@playwright/test';

const hier = dirname(fileURLToPath(import.meta.url));
const app = join(hier, '..');
const quellordner = join(app, 'build-resources');

const QUELLE = join(quellordner, 'symbol.svg');
const ICO = join(quellordner, 'icon.ico');
const PNG = join(quellordner, 'icon.png');
const BUCH = join(quellordner, 'symbol.json');
/** Ordner, in dem electron-builder die Store-Grafiken sucht. */
const APPX = join(quellordner, 'appx');

/**
 * Die Größen in der ICO-Datei.
 *
 * Windows greift je nach Ort eine andere und rechnet sonst selbst um — was
 * bei kleinen Größen sichtbar schlechter aussieht als eine eigens gerechnete.
 * 20, 40 und 96 sind die Zwischenstufen, die Windows bei 125-, 150- und
 * 200-prozentiger Anzeigegröße verlangt.
 */
const GROESSEN = [16, 20, 24, 32, 40, 48, 64, 96, 128, 256];

/** Ab dieser Größe liegt das Bild als PNG in der Datei, darunter als BMP. */
const AB_PNG = 128;

/** Die Kantenlänge der eigenständigen PNG-Datei (macOS, Linux). */
const PNG_GROESSE = 1024;

/**
 * Die Grafiken des Store-Pakets (AppX/MSIX).
 *
 * ## Warum sie hier entstehen müssen
 *
 * electron-builder sucht sie ausschließlich unter
 * `<buildResources>/appx/` und **schweigt**, wenn es sie nicht findet: Es
 * greift dann zu `SampleAppx.*` aus seinem eigenen Werkzeugpaket. Am Paket
 * 0.22.0 nachgemessen lagen genau diese Beispieldateien darin, byteweise
 * gleich – Store-Eintrag, Startmenü-Kachel und Taskleiste zeigten also ein
 * fremdes Platzhalterbild. Der Store hat es am 31.08.2026 unter Richtlinie
 * 10.1.1.11 („On Device Tiles“) beanstandet. `icon.ico` und `icon.png` fasst
 * das AppX-Ziel nicht an; die eine geht in die exe, die andere nach macOS.
 *
 * ## Warum diese sechs
 *
 * Die ersten vier sind die Pflichtgrafiken des Manifests und zugleich genau
 * die vier, für die electron-builder ein Beispielbild einsetzt.
 *
 * `SmallTile.png` und `LargeTile.png` kamen mit 0.26.3 dazu. Sie sind keine
 * Pflicht, aber electron-builder trägt sie in das Manifest ein, sobald es
 * sie findet (`Square71x71Logo` und `Square310x310Logo` in `defaultTileTag`,
 * `AppxTarget.js`) – und ohne sie hat Windows für die kleine und die große
 * Kachel nichts von diesem Programm. Die Namen stammen aus electron-builder
 * und sind nicht frei wählbar.
 *
 * Drei Größen sind quadratisch, die breite Kachel ist es nicht: Dort steht
 * das Zeichen mittig auf der Grundfarbe der Zeichnung, damit die Kachel
 * nicht halb durchsichtig ist.
 */
const KACHELN = [
  { datei: 'StoreLogo.png', breite: 50, hoehe: 50 },
  { datei: 'Square44x44Logo.png', breite: 44, hoehe: 44 },
  { datei: 'Square150x150Logo.png', breite: 150, hoehe: 150 },
  { datei: 'Wide310x150Logo.png', breite: 310, hoehe: 150 },
  { datei: 'SmallTile.png', breite: 71, hoehe: 71 },
  { datei: 'LargeTile.png', breite: 310, hoehe: 310 },
];

/** Grundfarbe der Zeichnung – dieselbe wie das Feld in `symbol.svg`. */
const KACHELGRUND = '#17505f';

// ── Bilder holen ───────────────────────────────────────────────────────────

/**
 * Rastert die Zeichnung in allen verlangten Größen.
 *
 * Gezeichnet wird in eine Leinwand statt über einen Bildschirmabzug: Die
 * Leinwand liefert die Bildpunkte unmittelbar, ohne Umweg über Fenstergröße
 * und Geräteauflösung, und dieselbe Leinwand kodiert das PNG gleich mit.
 */
async function rastern(svg, groessen) {
  const browser = await chromium.launch();
  try {
    const seite = await browser.newPage();
    return await seite.evaluate(
      /* eslint-disable no-undef -- Dieser Rumpf läuft nicht hier, sondern im
         Browser: Playwright schickt ihn als Zeichenkette hinüber. `Image` und
         `document` gibt es dort, in Node nicht. Der Linter sieht die Datei als
         Node-Umgebung und kann den Wechsel nicht erkennen. */
      async ({ svg: zeichnung, groessen: liste }) => {
        const adresse = URL.createObjectURL(new Blob([zeichnung], { type: 'image/svg+xml' }));
        const bild = new Image();
        bild.src = adresse;
        await bild.decode();

        const ergebnis = {};
        for (const n of liste) {
          const leinwand = document.createElement('canvas');
          leinwand.width = n;
          leinwand.height = n;
          const stift = leinwand.getContext('2d');
          stift.clearRect(0, 0, n, n);
          stift.drawImage(bild, 0, 0, n, n);
          ergebnis[n] = {
            roh: Array.from(stift.getImageData(0, 0, n, n).data),
            png: leinwand.toDataURL('image/png').split(',')[1],
          };
        }
        URL.revokeObjectURL(adresse);
        return ergebnis;
      },
      /* eslint-enable no-undef */
      { svg, groessen },
    );
  } finally {
    await browser.close();
  }
}

/**
 * Rastert die Zeichnung auf Leinwände beliebigen Seitenverhältnisses.
 *
 * Für die breite Store-Kachel: Das Zeichen bleibt quadratisch und sitzt
 * mittig, der Rest bekommt die Grundfarbe der Zeichnung. Ein durchsichtiger
 * Rand sähe je nach Windows-Design anders aus als das Feld daneben.
 */
async function kachelnRastern(svg, kacheln, grund) {
  const browser = await chromium.launch();
  try {
    const seite = await browser.newPage();
    return await seite.evaluate(
      /* eslint-disable no-undef -- läuft im Browser, siehe `rastern`. */
      async ({ svg: zeichnung, kacheln: liste, grund: farbe }) => {
        const adresse = URL.createObjectURL(new Blob([zeichnung], { type: 'image/svg+xml' }));
        const bild = new Image();
        bild.src = adresse;
        await bild.decode();

        const ergebnis = {};
        for (const { datei, breite, hoehe } of liste) {
          const leinwand = document.createElement('canvas');
          leinwand.width = breite;
          leinwand.height = hoehe;
          const stift = leinwand.getContext('2d');
          const kante = Math.min(breite, hoehe);
          if (breite !== hoehe) {
            stift.fillStyle = farbe;
            stift.fillRect(0, 0, breite, hoehe);
          } else {
            stift.clearRect(0, 0, breite, hoehe);
          }
          stift.drawImage(bild, (breite - kante) / 2, (hoehe - kante) / 2, kante, kante);
          ergebnis[datei] = leinwand.toDataURL('image/png').split(',')[1];
        }
        URL.revokeObjectURL(adresse);
        return ergebnis;
      },
      /* eslint-enable no-undef */
      { svg, kacheln, grund },
    );
  } finally {
    await browser.close();
  }
}

// ── ICO zusammensetzen ─────────────────────────────────────────────────────

/**
 * Ein Bild als unkomprimiertes BMP, wie es in einer ICO-Datei steht.
 *
 * Zwei Eigenheiten des Formats: Die angegebene Höhe ist die doppelte, weil
 * hinter den Bildpunkten noch eine Maske steht; und die Zeilen laufen von
 * unten nach oben. Die Maske bleibt leer — die Durchsichtigkeit steht schon
 * im Alphakanal jedes Bildpunkts.
 */
function alsBmp(n, roh) {
  const kopf = Buffer.alloc(40);
  kopf.writeUInt32LE(40, 0); // Größe dieses Kopfes
  kopf.writeInt32LE(n, 4); // Breite
  kopf.writeInt32LE(n * 2, 8); // Höhe, doppelt gezählt
  kopf.writeUInt16LE(1, 12); // Ebenen
  kopf.writeUInt16LE(32, 14); // Bit je Bildpunkt
  kopf.writeUInt32LE(0, 16); // keine Kompression

  const punkte = Buffer.alloc(n * n * 4);
  for (let zielzeile = 0; zielzeile < n; zielzeile += 1) {
    const quellzeile = n - 1 - zielzeile;
    for (let x = 0; x < n; x += 1) {
      const q = (quellzeile * n + x) * 4;
      const z = (zielzeile * n + x) * 4;
      punkte[z] = roh[q + 2]; // Blau
      punkte[z + 1] = roh[q + 1]; // Grün
      punkte[z + 2] = roh[q]; // Rot
      punkte[z + 3] = roh[q + 3]; // Deckung
    }
  }

  // Die Maske ist ein Bit je Bildpunkt, jede Zeile auf vier Byte aufgefüllt.
  const maske = Buffer.alloc(Math.ceil(n / 32) * 4 * n);

  kopf.writeUInt32LE(punkte.length + maske.length, 20);
  return Buffer.concat([kopf, punkte, maske]);
}

/** Setzt die einzelnen Bilder zu einer ICO-Datei zusammen. */
function alsIco(bilder) {
  const kopf = Buffer.alloc(6);
  kopf.writeUInt16LE(0, 0); // Vorbehalten
  kopf.writeUInt16LE(1, 2); // 1 = Symbol
  kopf.writeUInt16LE(bilder.length, 4);

  const verzeichnis = [];
  let versatz = 6 + 16 * bilder.length;

  for (const { groesse, daten } of bilder) {
    const eintrag = Buffer.alloc(16);
    // 256 wird als 0 geschrieben - ein Byte fasst die Zahl nicht.
    eintrag.writeUInt8(groesse === 256 ? 0 : groesse, 0);
    eintrag.writeUInt8(groesse === 256 ? 0 : groesse, 1);
    eintrag.writeUInt8(0, 2); // keine Farbtafel
    eintrag.writeUInt8(0, 3); // vorbehalten
    eintrag.writeUInt16LE(1, 4); // Ebenen
    eintrag.writeUInt16LE(32, 6); // Bit je Bildpunkt
    eintrag.writeUInt32LE(daten.length, 8);
    eintrag.writeUInt32LE(versatz, 12);
    verzeichnis.push(eintrag);
    versatz += daten.length;
  }

  return Buffer.concat([kopf, ...verzeichnis, ...bilder.map((b) => b.daten)]);
}

// ── Ablauf ─────────────────────────────────────────────────────────────────

const svg = readFileSync(QUELLE, 'utf8');
const alle = [...GROESSEN, PNG_GROESSE];
const bilder = await rastern(svg, alle);

const eintraege = GROESSEN.map((groesse) => ({
  groesse,
  daten:
    groesse >= AB_PNG
      ? Buffer.from(bilder[groesse].png, 'base64')
      : alsBmp(groesse, bilder[groesse].roh),
}));

writeFileSync(ICO, alsIco(eintraege));
writeFileSync(PNG, Buffer.from(bilder[PNG_GROESSE].png, 'base64'));

/* Die vier Pflichtgrafiken des Store-Pakets. Ohne sie greift
   electron-builder still zu seinen Beispielbildern – siehe `KACHELN`. */
mkdirSync(APPX, { recursive: true });
const kachelbilder = await kachelnRastern(svg, KACHELN, KACHELGRUND);
for (const { datei } of KACHELN) {
  writeFileSync(join(APPX, datei), Buffer.from(kachelbilder[datei], 'base64'));
}

const pruefsumme = createHash('sha256').update(svg, 'utf8').digest('hex');
writeFileSync(
  BUCH,
  `${JSON.stringify(
    {
      hinweis:
        'Erzeugt von tools/symbol-bauen.mjs. Nicht von Hand ändern — ' +
        'die Prüfsumme belegt, dass icon.ico und icon.png zu symbol.svg passen.',
      quelle: 'symbol.svg',
      pruefsumme,
      groessen: GROESSEN,
      pngGroesse: PNG_GROESSE,
      kacheln: KACHELN,
    },
    null,
    2,
  )}\n`,
);

const summe = eintraege.reduce((s, e) => s + e.daten.length, 0);
console.log(
  `icon.ico: ${String(GROESSEN.length)} Größen (${GROESSEN.join(', ')}), ${String(Math.round(summe / 1024))} KiB`,
);
console.log(`icon.png: ${String(PNG_GROESSE)} x ${String(PNG_GROESSE)}`);
console.log(
  `appx/: ${KACHELN.map((k) => `${k.datei} ${String(k.breite)}x${String(k.hoehe)}`).join(', ')}`,
);
console.log(`symbol.json: Prüfsumme ${pruefsumme.slice(0, 12)}…`);
