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

  const mod = (value, divisor) => ((value % divisor) + divisor) % divisor;
  const freezePattern = (method, width, height, thresholds, thresholdLevels = width * height) => Object.freeze({
    method,
    width,
    height,
    thresholdLevels,
    thresholds: Object.freeze(thresholds)
  });

  function rankedThresholds(width, height, score) {
    const cells = Array.from({ length: width * height }, (_, index) => {
      const x = index % width; const y = Math.floor(index / width);
      return { index, score: score(x, y, index) };
    });
    cells.sort((left, right) => left.score - right.score || left.index - right.index);
    const thresholds = new Array(cells.length);
    cells.forEach((cell, rank) => { thresholds[cell.index] = rank; });
    return thresholds;
  }

  function bayerThresholds(size) {
    if (size === 2) return [0, 2, 3, 1];
    const previous = bayerThresholds(size / 2); const half = size / 2; const thresholds = new Array(size * size);
    const offsets = [[0, 2], [3, 1]];
    for (let y = 0; y < size; y += 1) {
      for (let x = 0; x < size; x += 1) {
        thresholds[y * size + x] = previous[(y % half) * half + (x % half)] * 4 + offsets[Math.floor(y / half)][Math.floor(x / half)];
      }
    }
    return thresholds;
  }

  function scatteredThresholds(size = 16) {
    const values = Array.from({ length: size * size }, (_, index) => index); let state = 0x6d2b79f5;
    for (let index = values.length - 1; index > 0; index -= 1) {
      state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
      const swapIndex = state % (index + 1); [values[index], values[swapIndex]] = [values[swapIndex], values[index]];
    }
    return values;
  }

  function clusteredDotThresholds(width, height, variant = 'round') {
    const centerX = (width - 1) / 2; const centerY = (height - 1) / 2;
    return rankedThresholds(width, height, (x, y) => {
      const dx = Math.abs(x - centerX); const dy = Math.abs(y - centerY);
      const distance = variant === 'diamond'
        ? dx + dy
        : dx * dx + dy * dy;
      const tieBreak = variant === 'diamond'
        ? mod((width - 1 - x) * 7 + y * 5, width * height)
        : mod(x * 5 + y * 3, width * height);
      return distance + tieBreak / (width * height * 100);
    });
  }

  function halftoneThresholds(size = 12) {
    const values = new Array(size * size); const rings = new Map();
    for (let index = 0; index < values.length; index += 1) {
      const x = index % size; const y = Math.floor(index / size);
      const dx = 2 * x - (size - 1); const dy = 2 * y - (size - 1); const distance = dx * dx + dy * dy;
      if (!rings.has(distance)) rings.set(distance, []); rings.get(distance).push(index);
    }
    let occupied = 0;
    [...rings.entries()].sort(([left], [right]) => left - right).forEach(([, indices]) => {
      const threshold = occupied + (indices.length - 1) / 2;
      indices.forEach((index) => { values[index] = threshold; }); occupied += indices.length;
    });
    return values;
  }

  function lineThresholds(width, height, direction = 1, levels = Math.max(width, height), points = false) {
    if (!points) {
      return {
        levels,
        values: Array.from({ length: width * height }, (_, index) => {
          const x = index % width; const y = Math.floor(index / width);
          return mod(x + direction * y, levels);
        })
      };
    }
    return {
      levels: width * height,
      values: rankedThresholds(width, height, (x, y) => {
        const phase = mod(x + direction * y, Math.max(width, height));
        const pointPhase = mod(x * 3 - direction * y * 2, width);
        return phase + pointPhase / (width + 1);
      })
    };
  }

  function heartThresholds(size = 6) {
    return rankedThresholds(size, size, (column, row, index) => {
      const x = ((column + .5) / size * 2 - 1) * 1.2;
      const y = -(((row + .5) / size * 2 - 1) * 1.18);
      const squared = x * x + y * y;
      const implicit = Math.pow(squared - 1, 3) - x * x * y * y * y;
      return implicit + index / (size * size * 1000);
    });
  }

  function triangleThresholds(size) {
    return rankedThresholds(size, size, (column, row, index) => {
      const x = (column + .5) / size * 2 - 1;
      const y = (row + .5) / size * 2 - 1;
      const triangleGauge = Math.max(y * 1.15, Math.sqrt(3) * Math.abs(x) - y * .72);
      return triangleGauge + index / (size * size * 1000);
    });
  }

  const definitions = {};
  const add = (method, width, height, thresholds, thresholdLevels) => {
    definitions[method] = freezePattern(method, width, height, thresholds, thresholdLevels);
  };
  const addLine = (method, width, height, direction, levels, points = false) => {
    const line = lineThresholds(width, height, direction, levels, points);
    add(method, width, height, line.values, line.levels);
  };

  add('bayer2', 2, 2, bayerThresholds(2));
  add('bayer4', 4, 4, bayerThresholds(4));
  add('bayer8', 8, 8, bayerThresholds(8));
  add('dot-matrix-5x5', 5, 5, clusteredDotThresholds(5, 5));
  add('dot-matrix-6x6-a', 6, 6, clusteredDotThresholds(6, 6));
  add('dot-matrix-6x6-b', 6, 6, clusteredDotThresholds(6, 6, 'diamond'));
  add('hearts-6x6', 6, 6, heartThresholds());

  [
    ['line-diag-2x2', 2, 2, 2], ['line-diag-3x3', 3, 3, 3],
    ['line-diag-4x12', 4, 12, 12], ['line-diag-4x16', 4, 16, 16],
    ['line-diag-4x4', 4, 4, 4], ['line-diag-4x8', 4, 8, 8],
    ['line-diag-5x5', 5, 5, 5], ['line-diag-6x6', 6, 6, 6],
    ['line-diag-7x7', 7, 7, 7], ['line-diag-8x8-7lvs', 8, 8, 7],
    ['line-diag-8x8-9lvs', 8, 8, 9]
  ].forEach(([method, width, height, levels]) => addLine(method, width, height, 1, levels));
  addLine('line-diag-4x8-points', 4, 8, 1, 32, true);

  [
    ['line-diag-flip-2x2', 2, 2, 2], ['line-diag-flip-3x3', 3, 3, 3],
    ['line-diag-flip-4x12', 4, 12, 12], ['line-diag-flip-4x16', 4, 16, 16],
    ['line-diag-flip-4x4', 4, 4, 4], ['line-diag-flip-4x8', 4, 8, 8],
    ['line-diag-flip-5x5', 5, 5, 5], ['line-diag-flip-6x6', 6, 6, 6],
    ['line-diag-flip-7x7', 7, 7, 7], ['line-diag-flip-8x8-7lvs', 8, 8, 7],
    ['line-diag-flip-8x8-9lvs', 8, 8, 9]
  ].forEach(([method, width, height, levels]) => addLine(method, width, height, -1, levels));
  addLine('line-diag-flip-4x8-points', 4, 8, -1, 32, true);

  add('line-horiz-1x2', 1, 2, [0, 1], 2);
  [5, 6, 7].forEach((size) => add(`triangle-${size}x${size}`, size, size, triangleThresholds(size)));
  add('scatter', 16, 16, scatteredThresholds());
  add('halftone', 12, 12, halftoneThresholds());

  const patterns = Object.freeze(definitions);
  const groups = Object.freeze([
    Object.freeze({
      label: 'Bayer-Matrizen',
      options: Object.freeze([
        Object.freeze({ value: 'bayer2', label: 'Bayer 2×2' }),
        Object.freeze({ value: 'bayer4', label: 'Bayer 4×4' }),
        Object.freeze({ value: 'bayer8', label: 'Bayer 8×8' })
      ])
    }),
    Object.freeze({
      label: 'Punkt- und Formraster',
      options: Object.freeze([
        Object.freeze({ value: 'dot-matrix-5x5', label: 'dot-matrix-5x5' }),
        Object.freeze({ value: 'dot-matrix-6x6-a', label: 'dot-matrix-6x6-a' }),
        Object.freeze({ value: 'dot-matrix-6x6-b', label: 'dot-matrix-6x6-b' }),
        Object.freeze({ value: 'hearts-6x6', label: 'hearts-6x6' }),
        Object.freeze({ value: 'triangle-5x5', label: 'triangle-5x5' }),
        Object.freeze({ value: 'triangle-6x6', label: 'triangle-6x6' }),
        Object.freeze({ value: 'triangle-7x7', label: 'triangle-7x7' })
      ])
    }),
    Object.freeze({
      label: 'Diagonallinien',
      options: Object.freeze([
        'line-diag-2x2', 'line-diag-3x3', 'line-diag-4x4', 'line-diag-4x8',
        'line-diag-4x8-points', 'line-diag-4x12', 'line-diag-4x16',
        'line-diag-5x5', 'line-diag-6x6', 'line-diag-7x7',
        'line-diag-8x8-7lvs', 'line-diag-8x8-9lvs'
      ].map((value) => Object.freeze({ value, label: value })))
    }),
    Object.freeze({
      label: 'Gespiegelte Diagonallinien',
      options: Object.freeze([
        'line-diag-flip-2x2', 'line-diag-flip-3x3', 'line-diag-flip-4x4',
        'line-diag-flip-4x8', 'line-diag-flip-4x8-points',
        'line-diag-flip-4x12', 'line-diag-flip-4x16',
        'line-diag-flip-5x5', 'line-diag-flip-6x6', 'line-diag-flip-7x7',
        'line-diag-flip-8x8-7lvs', 'line-diag-flip-8x8-9lvs'
      ].map((value) => Object.freeze({ value, label: value })))
    }),
    Object.freeze({
      label: 'Weitere Raster',
      options: Object.freeze([
        Object.freeze({ value: 'line-horiz-1x2', label: 'line-horiz-1x2' }),
        Object.freeze({ value: 'scatter', label: 'Streuraster' }),
        Object.freeze({ value: 'halftone', label: 'Halbtonpunkte' })
      ])
    })
  ]);
  const choices = Object.freeze(groups.flatMap((group) => group.options.map((option) => Object.freeze({ ...option, group: group.label }))));

  function pattern(method) {
    return patterns[method] || patterns.bayer8;
  }

  const api = Object.freeze({
    patterns,
    groups,
    choices,
    methods: Object.freeze(Object.keys(patterns)),
    pattern
  });
  root.PyroDitherPatterns = api;
  if (typeof module === 'object' && module?.exports) module.exports = api;
})(typeof globalThis !== 'undefined' ? globalThis : self);
