(function attachPyroProjectContainer(root) {
  'use strict';

  const CONTAINER_FORMAT = 'pyro-container';
  const CONTAINER_VERSION = 1;
  const MEDIA_TYPE = 'application/vnd.pyro.project+zip';
  const ASSET_PREFIX = 'pyro-asset:';
  const textEncoder = new TextEncoder();
  const textDecoder = new TextDecoder('utf-8', { fatal: true });
  const defaultLimits = Object.freeze({
    maxEntries: 4096,
    maxEntryBytes: 64 * 1024 * 1024,
    maxTotalBytes: 128 * 1024 * 1024
  });
  const crcTable = new Uint32Array(256);
  for (let index = 0; index < crcTable.length; index += 1) {
    let value = index;
    for (let bit = 0; bit < 8; bit += 1) value = (value >>> 1) ^ (value & 1 ? 0xedb88320 : 0);
    crcTable[index] = value >>> 0;
  }

  class ProjectContainerError extends Error {
    constructor(message, code = 'PROJECT_CONTAINER_INVALID') {
      super(message);
      this.name = 'ProjectContainerError';
      this.code = code;
    }
  }

  function asBytes(value) {
    if (value instanceof Uint8Array) return value;
    if (value instanceof ArrayBuffer) return new Uint8Array(value);
    if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
    if (typeof value === 'string') return textEncoder.encode(value);
    throw new TypeError('Containerdaten müssen Text oder Binärdaten sein');
  }

  function concatBytes(parts) {
    const length = parts.reduce((sum, part) => sum + part.length, 0);
    const output = new Uint8Array(length); let offset = 0;
    parts.forEach((part) => { output.set(part, offset); offset += part.length; });
    return output;
  }

  function crc32(bytes) {
    let crc = 0xffffffff;
    for (const byte of bytes) crc = (crc >>> 8) ^ crcTable[(crc ^ byte) & 0xff];
    return (crc ^ 0xffffffff) >>> 0;
  }

  function dosTimestamp(date = new Date()) {
    const year = Math.min(2107, Math.max(1980, date.getFullYear()));
    return {
      time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
      date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate()
    };
  }

  function validPath(path) {
    return typeof path === 'string'
      && path.length > 0
      && path.length <= 512
      && !path.startsWith('/')
      && !path.includes('\\')
      && !path.split('/').some((part) => !part || part === '.' || part === '..' || part.includes('\0'));
  }

  function createZip(entries, { date = new Date() } = {}) {
    if (!Array.isArray(entries) || !entries.length) throw new ProjectContainerError('Der PYRO-Container enthält keine Dateien');
    if (entries.length > 0xffff) throw new ProjectContainerError('Der PYRO-Container enthält zu viele Dateien', 'PROJECT_CONTAINER_TOO_MANY_ENTRIES');
    const timestamp = dosTimestamp(date); const localParts = []; const centralParts = []; let localOffset = 0;
    const seen = new Set();
    entries.forEach((entry) => {
      const name = String(entry?.name || '');
      if (!validPath(name) || seen.has(name)) throw new ProjectContainerError(`Ungültiger oder doppelter Containerpfad: ${name || '(leer)'}`);
      seen.add(name);
      const nameBytes = textEncoder.encode(name); const data = asBytes(entry.data);
      if (data.length > 0xffffffff || localOffset > 0xffffffff) throw new ProjectContainerError('Der PYRO-Container ist zu groß', 'PROJECT_CONTAINER_TOO_LARGE');
      const checksum = crc32(data);
      const local = new Uint8Array(30 + nameBytes.length); const localView = new DataView(local.buffer);
      localView.setUint32(0, 0x04034b50, true); localView.setUint16(4, 20, true); localView.setUint16(6, 0x0800, true);
      localView.setUint16(8, 0, true); localView.setUint16(10, timestamp.time, true); localView.setUint16(12, timestamp.date, true);
      localView.setUint32(14, checksum, true); localView.setUint32(18, data.length, true); localView.setUint32(22, data.length, true);
      localView.setUint16(26, nameBytes.length, true); localView.setUint16(28, 0, true); local.set(nameBytes, 30);

      const central = new Uint8Array(46 + nameBytes.length); const centralView = new DataView(central.buffer);
      centralView.setUint32(0, 0x02014b50, true); centralView.setUint16(4, 20, true); centralView.setUint16(6, 20, true);
      centralView.setUint16(8, 0x0800, true); centralView.setUint16(10, 0, true); centralView.setUint16(12, timestamp.time, true); centralView.setUint16(14, timestamp.date, true);
      centralView.setUint32(16, checksum, true); centralView.setUint32(20, data.length, true); centralView.setUint32(24, data.length, true);
      centralView.setUint16(28, nameBytes.length, true); centralView.setUint16(30, 0, true); centralView.setUint16(32, 0, true);
      centralView.setUint16(34, 0, true); centralView.setUint16(36, 0, true); centralView.setUint32(38, 0, true); centralView.setUint32(42, localOffset, true);
      central.set(nameBytes, 46);
      localParts.push(local, data); centralParts.push(central); localOffset += local.length + data.length;
    });
    const centralDirectory = concatBytes(centralParts);
    const end = new Uint8Array(22); const endView = new DataView(end.buffer);
    endView.setUint32(0, 0x06054b50, true); endView.setUint16(4, 0, true); endView.setUint16(6, 0, true);
    endView.setUint16(8, entries.length, true); endView.setUint16(10, entries.length, true);
    endView.setUint32(12, centralDirectory.length, true); endView.setUint32(16, localOffset, true); endView.setUint16(20, 0, true);
    return concatBytes([...localParts, centralDirectory, end]);
  }

  function endOfCentralDirectory(bytes) {
    const minimum = Math.max(0, bytes.length - 0xffff - 22);
    for (let offset = bytes.length - 22; offset >= minimum; offset -= 1) {
      if (bytes[offset] === 0x50 && bytes[offset + 1] === 0x4b && bytes[offset + 2] === 0x05 && bytes[offset + 3] === 0x06) return offset;
    }
    return -1;
  }

  async function inflateRaw(data) {
    if (typeof DecompressionStream !== 'function') throw new ProjectContainerError('Komprimierte ZIP-Einträge werden von diesem Browser nicht unterstützt', 'PROJECT_CONTAINER_DEFLATE_UNSUPPORTED');
    try {
      const stream = new Blob([data]).stream().pipeThrough(new DecompressionStream('deflate-raw'));
      return new Uint8Array(await new Response(stream).arrayBuffer());
    } catch {
      throw new ProjectContainerError('Ein komprimierter Container-Eintrag ist beschädigt', 'PROJECT_CONTAINER_DEFLATE_FAILED');
    }
  }

  async function readZipEntries(value, limits = {}) {
    const bytes = asBytes(value); const resolved = { ...defaultLimits, ...(limits || {}) };
    const endOffset = endOfCentralDirectory(bytes);
    if (endOffset < 0) throw new ProjectContainerError('Die ZIP-Verzeichnisstruktur fehlt');
    const end = new DataView(bytes.buffer, bytes.byteOffset + endOffset, 22);
    if (end.getUint16(4, true) !== 0 || end.getUint16(6, true) !== 0) throw new ProjectContainerError('Mehrteilige ZIP-Dateien werden nicht unterstützt');
    const entryCount = end.getUint16(10, true); const centralSize = end.getUint32(12, true); const centralOffset = end.getUint32(16, true);
    if (entryCount > resolved.maxEntries) throw new ProjectContainerError('Der PYRO-Container enthält zu viele Dateien', 'PROJECT_CONTAINER_TOO_MANY_ENTRIES');
    if (centralOffset + centralSize > endOffset) throw new ProjectContainerError('Das ZIP-Verzeichnis liegt außerhalb der Datei');
    const entries = new Map(); let offset = centralOffset; let totalBytes = 0;
    for (let index = 0; index < entryCount; index += 1) {
      if (offset + 46 > bytes.length) throw new ProjectContainerError('Ein ZIP-Verzeichniseintrag ist abgeschnitten');
      const central = new DataView(bytes.buffer, bytes.byteOffset + offset, 46);
      if (central.getUint32(0, true) !== 0x02014b50) throw new ProjectContainerError('Ein ZIP-Verzeichniseintrag ist ungültig');
      const flags = central.getUint16(8, true); const method = central.getUint16(10, true); const checksum = central.getUint32(16, true);
      const compressedSize = central.getUint32(20, true); const uncompressedSize = central.getUint32(24, true);
      const nameLength = central.getUint16(28, true); const extraLength = central.getUint16(30, true); const commentLength = central.getUint16(32, true);
      const localOffset = central.getUint32(42, true); const entryEnd = offset + 46 + nameLength + extraLength + commentLength;
      if (entryEnd > bytes.length) throw new ProjectContainerError('Ein ZIP-Verzeichniseintrag ist abgeschnitten');
      const name = textDecoder.decode(bytes.subarray(offset + 46, offset + 46 + nameLength));
      if (!validPath(name) || entries.has(name)) throw new ProjectContainerError(`Ungültiger oder doppelter Containerpfad: ${name || '(leer)'}`);
      if (flags & 1) throw new ProjectContainerError('Verschlüsselte ZIP-Einträge werden nicht unterstützt');
      if (![0, 8].includes(method)) throw new ProjectContainerError(`Nicht unterstützte ZIP-Kompression für ${name}`, 'PROJECT_CONTAINER_COMPRESSION_UNSUPPORTED');
      if (uncompressedSize > resolved.maxEntryBytes || totalBytes + uncompressedSize > resolved.maxTotalBytes) {
        throw new ProjectContainerError('Der entpackte PYRO-Container ist zu groß', 'PROJECT_CONTAINER_TOO_LARGE');
      }
      if (localOffset + 30 > bytes.length) throw new ProjectContainerError(`Der ZIP-Eintrag ${name} ist abgeschnitten`);
      const local = new DataView(bytes.buffer, bytes.byteOffset + localOffset, 30);
      if (local.getUint32(0, true) !== 0x04034b50) throw new ProjectContainerError(`Der ZIP-Eintrag ${name} besitzt keinen lokalen Header`);
      if (local.getUint16(6, true) !== flags || local.getUint16(8, true) !== method) throw new ProjectContainerError(`Die ZIP-Header für ${name} widersprechen sich`);
      const localNameLength = local.getUint16(26, true); const localExtraLength = local.getUint16(28, true);
      const localName = textDecoder.decode(bytes.subarray(localOffset + 30, localOffset + 30 + localNameLength));
      if (localName !== name) throw new ProjectContainerError(`Der lokale ZIP-Pfad für ${name} stimmt nicht mit dem Verzeichnis überein`);
      const dataOffset = localOffset + 30 + localNameLength + localExtraLength;
      if (dataOffset + compressedSize > bytes.length) throw new ProjectContainerError(`Der ZIP-Eintrag ${name} ist abgeschnitten`);
      const compressed = bytes.slice(dataOffset, dataOffset + compressedSize);
      const data = method === 0 ? compressed : await inflateRaw(compressed);
      if (data.length !== uncompressedSize || crc32(data) !== checksum) throw new ProjectContainerError(`Der ZIP-Eintrag ${name} ist beschädigt`, 'PROJECT_CONTAINER_CHECKSUM_FAILED');
      entries.set(name, data); totalBytes += data.length; offset = entryEnd;
    }
    return entries;
  }

  function cloneJson(value) {
    return JSON.parse(JSON.stringify(value));
  }

  function base64ToBytes(value) {
    if (typeof atob === 'function') {
      const binary = atob(value); const bytes = new Uint8Array(binary.length);
      for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
      return bytes;
    }
    if (typeof Buffer !== 'undefined') return new Uint8Array(Buffer.from(value, 'base64'));
    throw new ProjectContainerError('Base64-Daten können nicht dekodiert werden');
  }

  function bytesToBase64(bytes) {
    if (typeof btoa === 'function') {
      let output = ''; const chunkSize = 0x8000;
      for (let offset = 0; offset < bytes.length; offset += chunkSize) output += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
      return btoa(output);
    }
    if (typeof Buffer !== 'undefined') return Buffer.from(bytes).toString('base64');
    throw new ProjectContainerError('Binärdaten können nicht als Base64 kodiert werden');
  }

  function dataImage(value) {
    const match = /^data:(image\/(?:png|jpe?g|webp|gif|svg\+xml))((?:;[^,\u0000-\u001f]*)?),(.*)$/is.exec(value);
    if (!match) return null;
    const mediaType = match[1].toLowerCase() === 'image/jpg' ? 'image/jpeg' : match[1].toLowerCase();
    const base64 = /(?:^|;)base64(?:;|$)/i.test(match[2]);
    try {
      return { mediaType, bytes: base64 ? base64ToBytes(match[3].replace(/\s/g, '')) : textEncoder.encode(decodeURIComponent(match[3])) };
    } catch {
      throw new ProjectContainerError('Ein eingebettetes Bild ist ungültig', 'PROJECT_CONTAINER_ASSET_INVALID');
    }
  }

  const extensionForMediaType = Object.freeze({
    'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', 'image/gif': 'gif', 'image/svg+xml': 'svg'
  });

  function walkStrings(value, transform) {
    if (typeof value === 'string') return transform(value);
    if (Array.isArray(value)) return value.map((item) => walkStrings(item, transform));
    if (!value || typeof value !== 'object') return value;
    Object.keys(value).forEach((key) => { value[key] = walkStrings(value[key], transform); });
    return value;
  }

  function externalizeAssets(payload) {
    const document = cloneJson(payload); const assets = []; const bySource = new Map();
    walkStrings(document, (value) => {
      if (!/^data:image\//i.test(value)) return value;
      if (bySource.has(value)) return `${ASSET_PREFIX}${bySource.get(value).path}`;
      const parsed = dataImage(value); if (!parsed) return value;
      const path = `assets/asset-${String(assets.length + 1).padStart(4, '0')}.${extensionForMediaType[parsed.mediaType] || 'bin'}`;
      const asset = { path, mediaType: parsed.mediaType, bytes: parsed.bytes }; assets.push(asset); bySource.set(value, asset);
      return `${ASSET_PREFIX}${path}`;
    });
    return { document, assets, bySource };
  }

  function hydrateAssets(payload, manifest, entries) {
    const assets = new Map();
    if (!Array.isArray(manifest.assets)) throw new ProjectContainerError('Das Asset-Verzeichnis des PYRO-Containers ist ungültig');
    manifest.assets.forEach((asset) => {
      if (!asset || !validPath(asset.path) || typeof asset.mediaType !== 'string' || !asset.mediaType.startsWith('image/')) {
        throw new ProjectContainerError('Das Asset-Verzeichnis des PYRO-Containers ist ungültig');
      }
      if (assets.has(asset.path)) throw new ProjectContainerError(`Das Container-Asset ${asset.path} ist doppelt eingetragen`);
      const bytes = entries.get(asset.path);
      if (!bytes) throw new ProjectContainerError(`Das Container-Asset ${asset.path} fehlt`, 'PROJECT_CONTAINER_ASSET_MISSING');
      if (Number.isFinite(asset.bytes) && Number(asset.bytes) !== bytes.length) throw new ProjectContainerError(`Das Container-Asset ${asset.path} besitzt eine falsche Größe`);
      assets.set(asset.path, `data:${asset.mediaType};base64,${bytesToBase64(bytes)}`);
    });
    return walkStrings(cloneJson(payload), (value) => {
      if (!value.startsWith(ASSET_PREFIX)) return value;
      const path = value.slice(ASSET_PREFIX.length); const source = assets.get(path);
      if (!source) throw new ProjectContainerError(`Der Verweis auf das Container-Asset ${path} ist ungültig`, 'PROJECT_CONTAINER_ASSET_MISSING');
      return source;
    });
  }

  async function create(payload, { previewSvg = '', date = new Date() } = {}) {
    const externalized = externalizeAssets(payload);
    let containerPreview = String(previewSvg || '');
    externalized.bySource.forEach((asset, source) => { containerPreview = containerPreview.split(source).join(asset.path); });
    const manifest = {
      format: CONTAINER_FORMAT,
      version: CONTAINER_VERSION,
      mediaType: MEDIA_TYPE,
      document: 'document.json',
      preview: containerPreview ? 'preview.svg' : null,
      assets: externalized.assets.map((asset) => ({ path: asset.path, mediaType: asset.mediaType, bytes: asset.bytes.length })),
      fonts: []
    };
    const entries = [
      { name: 'mimetype', data: MEDIA_TYPE },
      { name: 'manifest.json', data: JSON.stringify(manifest, null, 2) },
      { name: 'document.json', data: JSON.stringify(externalized.document, null, 2) }
    ];
    if (containerPreview) entries.push({ name: 'preview.svg', data: containerPreview });
    externalized.assets.forEach((asset) => entries.push({ name: asset.path, data: asset.bytes }));
    return { bytes: createZip(entries, { date }), manifest, document: externalized.document };
  }

  function isContainer(value) {
    const bytes = asBytes(value);
    return bytes.length >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04;
  }

  async function open(value, limits = {}) {
    const entries = await readZipEntries(value, limits);
    const mimetypeBytes = entries.get('mimetype');
    if (!mimetypeBytes || textDecoder.decode(mimetypeBytes) !== MEDIA_TYPE) throw new ProjectContainerError('Der MIME-Typ des PYRO-Containers fehlt oder ist ungültig');
    const manifestBytes = entries.get('manifest.json');
    if (!manifestBytes) throw new ProjectContainerError('manifest.json fehlt im PYRO-Container');
    let manifest;
    try { manifest = JSON.parse(textDecoder.decode(manifestBytes)); }
    catch { throw new ProjectContainerError('manifest.json ist ungültig'); }
    if (manifest?.format !== CONTAINER_FORMAT || manifest?.version !== CONTAINER_VERSION || manifest?.mediaType !== MEDIA_TYPE || manifest?.document !== 'document.json') {
      throw new ProjectContainerError('Die PYRO-Containerversion wird nicht unterstützt', 'PROJECT_CONTAINER_VERSION_UNSUPPORTED');
    }
    if (manifest.preview != null && (!validPath(manifest.preview) || !entries.has(manifest.preview))) {
      throw new ProjectContainerError('Die Vorschau des PYRO-Containers fehlt oder ist ungültig');
    }
    const documentBytes = entries.get(manifest.document);
    if (!documentBytes) throw new ProjectContainerError('document.json fehlt im PYRO-Container');
    let document;
    try { document = JSON.parse(textDecoder.decode(documentBytes)); }
    catch { throw new ProjectContainerError('document.json ist ungültig'); }
    return { payload: hydrateAssets(document, manifest, entries), manifest, entries };
  }

  async function readProject(value, limits = {}) {
    const bytes = asBytes(value);
    if (isContainer(bytes)) {
      const result = await open(bytes, limits);
      return { ...result, isContainer: true };
    }
    try {
      return { payload: JSON.parse(textDecoder.decode(bytes)), manifest: null, entries: null, isContainer: false };
    } catch {
      throw new ProjectContainerError('Die Projektdatei ist weder ein PYRO-Container noch gültiges JSON', 'PROJECT_FILE_INVALID');
    }
  }

  const api = Object.freeze({
    CONTAINER_FORMAT, CONTAINER_VERSION, MEDIA_TYPE, ASSET_PREFIX, ProjectContainerError,
    crc32, createZip, readZipEntries, externalizeAssets, create, isContainer, open, readProject
  });
  root.PyroProjectContainer = api;
  if (typeof module === 'object' && module?.exports) module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : window);
