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

  const AutoShapes = root.PyroAutoShapes;
  const pointTypes = new Set(['pencil', 'brush', 'eraser', 'line', 'polyline', 'arrow', 'measure', 'polygon', 'pen']);
  const simplePathNodes = Object.freeze({
    rectangle: 4,
    ellipse: 4,
    'rounded-rectangle': 8,
    'beveled-rectangle': 8,
    'chamfer-rectangle': 8,
    'block-arrow': 7,
    'connector-line': 4,
    'l-shape': 6,
    pie: 4
  });

  function utf8ByteLength(value) {
    const text = String(value ?? '');
    let bytes = 0;
    for (let index = 0; index < text.length; index += 1) {
      const code = text.charCodeAt(index);
      if (code < 0x80) bytes += 1;
      else if (code < 0x800) bytes += 2;
      else if (code >= 0xd800 && code <= 0xdbff && index + 1 < text.length && text.charCodeAt(index + 1) >= 0xdc00 && text.charCodeAt(index + 1) <= 0xdfff) {
        bytes += 4; index += 1;
      } else bytes += 3;
    }
    return bytes;
  }

  function dataUrlBytes(value) {
    const source = String(value || ''); const comma = source.indexOf(',');
    if (!/^data:image\//i.test(source) || comma < 0) return 0;
    const header = source.slice(0, comma); const payload = source.slice(comma + 1);
    if (/;base64(?:;|$)/i.test(header)) {
      const compact = payload.replace(/\s+/g, '');
      const padding = compact.endsWith('==') ? 2 : compact.endsWith('=') ? 1 : 0;
      return Math.max(0, Math.floor(compact.length * 3 / 4) - padding);
    }
    let bytes = 0;
    for (let index = 0; index < payload.length; index += 1) {
      if (payload[index] === '%' && /^[0-9a-f]{2}$/i.test(payload.slice(index + 1, index + 3))) { bytes += 1; index += 2; }
      else {
        const code = payload.charCodeAt(index);
        if (code < 0x80) bytes += 1;
        else if (code < 0x800) bytes += 2;
        else if (code >= 0xd800 && code <= 0xdbff && index + 1 < payload.length && payload.charCodeAt(index + 1) >= 0xdc00 && payload.charCodeAt(index + 1) <= 0xdfff) { bytes += 4; index += 1; }
        else bytes += 3;
      }
    }
    return bytes;
  }

  function validContours(value) {
    return Array.isArray(value) ? value.filter((contour) => Array.isArray(contour) && contour.length >= 2) : [];
  }

  function pathDataComplexity(value) {
    const data = String(value || '');
    const paths = (data.match(/[Mm]/g) || []).length;
    const nodes = (data.match(/[MmLlHhVvCcSsQqTtAa]/g) || []).length;
    return paths && nodes ? { paths, nodes } : null;
  }

  function objectComplexity(object, symbols, symbolStack = new Set(), depth = 0) {
    if (!object || typeof object !== 'object' || depth > 128) return { paths: 0, nodes: 0 };
    if (object.type === 'symbol-instance') {
      const symbol = symbols.get(object.symbolId);
      if (!symbol?.object || symbolStack.has(object.symbolId)) return { paths: 0, nodes: 0 };
      const nextStack = new Set(symbolStack); nextStack.add(object.symbolId);
      return objectComplexity(symbol.object, symbols, nextStack, depth + 1);
    }

    let paths = 0; let nodes = 0;
    const contours = validContours(object.outlineContours);
    if (contours.length) {
      paths += contours.length;
      nodes += contours.reduce((sum, contour) => sum + contour.length, 0);
    } else if (Array.isArray(object.points) && object.points.length >= 2) {
      paths += 1; nodes += object.points.length;
    } else {
      const pathData = pathDataComplexity(object.pathData);
      if (pathData) { paths += pathData.paths; nodes += pathData.nodes; }
      else if (Object.hasOwn(simplePathNodes, object.type)) { paths += 1; nodes += simplePathNodes[object.type]; }
      else if (object.type === 'doughnut') { paths += 2; nodes += 8; }
      else if (object.type === 'smart-polygon') {
        const sides = Math.max(3, Math.round(Number(object.autoShape?.sides ?? object.sides ?? 5) || 5));
        paths += Number(object.autoShape?.inner ?? object.inner ?? 0) > 0 ? 2 : 1;
        nodes += paths === 2 ? sides * 2 : sides;
      } else if (object.type === 'star') {
        const points = Math.max(3, Math.round(Number(object.autoShape?.points ?? object.starPoints ?? 5) || 5));
        paths += 1; nodes += points * 2;
      } else if (object.type === 'spiral') {
        const turns = Math.max(.5, Number(object.autoShape?.turns ?? object.turns ?? 3) || 3);
        paths += 1; nodes += Math.ceil(turns * 8) + 1;
      } else if (pointTypes.has(object.type)) {
        paths += 1; nodes += object.type === 'polygon' ? Math.max(3, Math.round(Number(object.sides) || 5)) : 2;
      } else if (AutoShapes?.isAutoShape?.(object.type)) {
        const generated = validContours(AutoShapes.contours(object));
        paths += generated.length;
        nodes += generated.reduce((sum, contour) => sum + contour.length, 0);
      }
    }

    if (Array.isArray(object.children)) object.children.forEach((child) => {
      const nested = objectComplexity(child, symbols, symbolStack, depth + 1);
      paths += nested.paths; nodes += nested.nodes;
    });
    return { paths, nodes };
  }

  function embeddedMedia(document) {
    const media = new Set(); const visited = new WeakSet();
    const visit = (value, depth = 0) => {
      if (depth > 256 || value == null) return;
      if (typeof value === 'string') { if (/^data:image\//i.test(value)) media.add(value); return; }
      if (typeof value !== 'object' || visited.has(value)) return;
      visited.add(value);
      if (Array.isArray(value)) value.forEach((entry) => visit(entry, depth + 1));
      else Object.values(value).forEach((entry) => visit(entry, depth + 1));
    };
    visit(document);
    return { count: media.size, bytes: [...media].reduce((sum, value) => sum + dataUrlBytes(value), 0) };
  }

  function projectSize(document, snapshot, assetStore, assetPrefix) {
    let serialized = typeof snapshot === 'string' && snapshot ? snapshot : '';
    if (!serialized) {
      try { serialized = JSON.stringify(document || {}); }
      catch { return 0; }
    }
    let bytes = utf8ByteLength(serialized);
    if (assetStore?.byId instanceof Map && assetPrefix) {
      const escapedPrefix = String(assetPrefix).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      const expression = new RegExp(`${escapedPrefix}([^"\\\\]+)`, 'g');
      for (const match of serialized.matchAll(expression)) {
        const asset = assetStore.byId.get(match[1]);
        if (typeof asset === 'string') bytes += Math.max(0, utf8ByteLength(asset) - utf8ByteLength(match[0]));
      }
    }
    return bytes + 192;
  }

  function analyze(document, { snapshot = '', assetStore = null, assetPrefix = 'pyro-history-asset:' } = {}) {
    const layers = Array.isArray(document?.layers) ? document.layers : [];
    const objects = layers.reduce((sum, layer) => sum + (Array.isArray(layer?.objects) ? layer.objects.length : 0), 0);
    const symbols = new Map((Array.isArray(document?.resources?.symbols) ? document.resources.symbols : []).map((symbol) => [symbol.id, symbol]));
    let paths = 0; let nodes = 0;
    layers.forEach((layer) => (Array.isArray(layer?.objects) ? layer.objects : []).forEach((object) => {
      const result = objectComplexity(object, symbols);
      paths += result.paths; nodes += result.nodes;
    }));
    const media = embeddedMedia(document);
    return Object.freeze({
      objects,
      paths,
      nodes,
      mediaCount: media.count,
      mediaBytes: media.bytes,
      projectBytes: projectSize(document, snapshot, assetStore, assetPrefix)
    });
  }

  const locale = () => root.PyroI18n?.language === 'en' ? 'en-GB' : 'de-DE';
  const formatNumber = (value) => new Intl.NumberFormat(locale()).format(Math.max(0, Math.round(Number(value) || 0)));
  function formatBytes(value) {
    const bytes = Math.max(0, Number(value) || 0);
    if (bytes < 1024) return `${Math.round(bytes)} B`;
    const units = ['KB', 'MB', 'GB']; let size = bytes / 1024; let unit = units[0];
    for (let index = 1; index < units.length && size >= 1024; index += 1) { size /= 1024; unit = units[index]; }
    const digits = size >= 100 ? 0 : 1;
    return `${new Intl.NumberFormat(locale(), { minimumFractionDigits: digits, maximumFractionDigits: digits }).format(size)} ${unit}`;
  }

  const api = Object.freeze({ analyze, formatBytes, formatNumber, dataUrlBytes, utf8ByteLength });
  root.PyroDocumentStats = api;
  if (typeof module === 'object' && module?.exports) module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : window);
