const DEFAULT_BLUE_SRC = "/static/logo2.png";
const LOGO_SRC_WIDTH = 2222;
const LOGO_SRC_HEIGHT = 1024;
/** ~28 строк сетки, как у старого logo2 (305px @ cell 11). */
const INITIAL_CELL = 37;
const DURATION_MS = 4750;
const WAVE2_START_MS = 0.36 * 4500;
const WAVE2_SPAN_MS = 0.34 * 6800;
const WAVE2_START = WAVE2_START_MS / DURATION_MS;
const WAVE2_SPAN = WAVE2_SPAN_MS / DURATION_MS;
const WAVE2_END = (WAVE2_START_MS + WAVE2_SPAN_MS) / DURATION_MS;
const HOLD_END = 0.94;
const PIXEL_SCALE = WAVE2_START / 0.36;
const WAVE0_START = 0.04 * PIXEL_SCALE;
const WAVE0_SPAN = 0.20 * PIXEL_SCALE;
const WAVE1_START = WAVE0_START + WAVE0_SPAN - 0.04 * PIXEL_SCALE;
const WAVE1_SPAN = 0.20 * PIXEL_SCALE;
const PIXEL_FADE = 0.012;
const PIXEL_GAP = 1;
const ASSEMBLY_FEATHER = 10;
const COLOR_CROSSFADE = 0.024;
const SHIMMER_MS = 1400;
const SHIMMER_PROGRESS_START = 88;
const BRAND_BLUE = [44, 123, 229];
const BRAND_ORGANIC = [72, 129, 109];
const BRAND_ORGANIC_HC = [47, 107, 87];
const BRAND_WHITE = [238, 240, 243];
const PIXEL_GRAY = [148, 152, 158];
const BG = [246, 246, 244];
const GRID_STEP = 10;

function shimmerDurationMs() {
  return SHIMMER_MS;
}

function wave2EndMs(durationMs) {
  return WAVE2_END * durationMs;
}

function animationEndMs(durationMs) {
  return Math.max(durationMs, wave2EndMs(durationMs) + SHIMMER_MS + 180);
}

/** Фактическая длительность проигрывания (с учётом totalDurationMs). */
function resolvePlaybackEndMs(durationMs, totalDurationMs) {
  const natural = animationEndMs(durationMs);
  if (totalDurationMs == null || !Number.isFinite(totalDurationMs)) {
    return natural;
  }
  return Math.min(natural, Math.max(400, totalDurationMs));
}

export const LOGO_LOADER_DURATION_MS = DURATION_MS;
export { animationEndMs, resolvePlaybackEndMs, drawShimmer, LOGO_SRC_WIDTH, LOGO_SRC_HEIGHT };

/** Буфер логотипа для navbar-shimmer (выравнивание слева, как object-position: left). */
export function createNavLogoShimmerBuffer(img, slotW, slotH, inkRgb = [255, 255, 255]) {
  const srcW = img.naturalWidth || LOGO_SRC_WIDTH;
  const srcH = img.naturalHeight || LOGO_SRC_HEIGHT;
  const scale = Math.min(slotW / srcW, slotH / srcH);
  const drawWidth = srcW * scale;
  const drawHeight = srcH * scale;
  const layout = {
    offsetX: 0,
    offsetY: (slotH - drawHeight) / 2,
    drawWidth,
    drawHeight,
  };
  const canvas = document.createElement("canvas");
  canvas.width = slotW;
  canvas.height = slotH;
  const ctx = canvas.getContext("2d");
  ctx.clearRect(0, 0, slotW, slotH);
  ctx.drawImage(img, layout.offsetX, layout.offsetY, drawWidth, drawHeight);
  knockoutBackgroundFromLogo(canvas, slotW, slotH);
  if (inkRgb) recolorVisiblePixels(canvas, slotW, slotH, inkRgb);
  return { logoCanvas: canvas, layout };
}

function prefersReducedMotion() {
  return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
}

/** light | organic | dark — для цвета логотипа в анимации загрузки */
function resolveLoaderTheme(options = {}) {
  const explicit = options.theme;
  if (explicit === "light" || explicit === "organic" || explicit === "dark") {
    return explicit;
  }
  const docTheme = document.documentElement.getAttribute("data-theme");
  if (docTheme === "light" || docTheme === "organic" || docTheme === "dark") {
    return docTheme;
  }
  try {
    const saved = localStorage.getItem("theme");
    if (saved === "light" || saved === "organic" || saved === "dark") return saved;
  } catch {
    /* ignore */
  }
  if (options.darkTheme) return "dark";
  return "light";
}

function prefersHighContrast() {
  const attr = document.documentElement.getAttribute("data-high-contrast");
  if (attr === "true") return true;
  if (attr === "false") return false;
  return globalThis.matchMedia?.("(prefers-contrast: more)")?.matches ?? false;
}

function loaderAccentRgb(theme) {
  if (theme === "dark") return BRAND_WHITE;
  if (theme === "organic") return prefersHighContrast() ? BRAND_ORGANIC_HC : BRAND_ORGANIC;
  return BRAND_BLUE;
}

function loaderUsesDarkShimmer(theme) {
  return theme === "dark";
}

function loadImage(src) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error(`Failed to load image: ${src}`));
    img.src = src;
  });
}

function isLogoInk(data, index) {
  const r = data[index];
  const g = data[index + 1];
  const b = data[index + 2];
  const a = data[index + 3];
  if (a < 20) return false;
  if (r + g + b < 80) return false;
  return b > 70 && b >= r && b >= g * 0.85;
}

function isLogoBlueFringe(data, index) {
  const r = data[index];
  const g = data[index + 1];
  const b = data[index + 2];
  const a = data[index + 3];
  if (a < 1) return false;
  if (isLogoInk(data, index)) return true;
  return b > r + 5 && b > g - 2 && b > 68 && r + g + b < 700;
}

function knockoutBackgroundFromLogo(canvas, width, height) {
  const ctx = canvas.getContext("2d");
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    if (data[i + 3] < 1) continue;
    if (isLogoBlueFringe(data, i)) continue;
    data[i + 3] = 0;
  }
  ctx.putImageData(imageData, 0, 0);
}

function recolorVisiblePixels(canvas, width, height, rgb) {
  const ctx = canvas.getContext("2d");
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    if (data[i + 3] < 1) continue;
    data[i] = rgb[0];
    data[i + 1] = rgb[1];
    data[i + 2] = rgb[2];
  }
  ctx.putImageData(imageData, 0, 0);
}

function cloneLogoBuffer(sourceCanvas, width, height, rgb = null) {
  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext("2d");
  ctx.drawImage(sourceCanvas, 0, 0);
  if (rgb) recolorVisiblePixels(canvas, width, height, rgb);
  return canvas;
}

function drawLogoToBuffer(img, width, height, knockoutBackground = false) {
  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext("2d");
  ctx.clearRect(0, 0, width, height);

  const scale = Math.min(width / img.naturalWidth, height / img.naturalHeight);
  const drawWidth = img.naturalWidth * scale;
  const drawHeight = img.naturalHeight * scale;
  const offsetX = (width - drawWidth) / 2;
  const offsetY = (height - drawHeight) / 2;
  ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
  if (knockoutBackground) knockoutBackgroundFromLogo(canvas, width, height);
  return canvas;
}

function hashCell(col, row) {
  return ((col * 92837111) ^ (row * 689287499)) >>> 0;
}

function easeOutQuad(t) {
  return 1 - (1 - t) * (1 - t);
}

function easeInOutQuad(t) {
  return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
}

function rgbaString(r, g, b, a) {
  return `rgba(${r | 0}, ${g | 0}, ${b | 0}, ${a})`;
}

function desaturateColor(r, g, b, amount = 0.72) {
  const gray = (r + g + b) / 3;
  return [
    r + (gray - r) * amount,
    g + (gray - g) * amount,
    b + (gray - b) * amount,
  ];
}

function lerpColor(c1, c2, t) {
  return [
    c1[0] + (c2[0] - c1[0]) * t,
    c1[1] + (c2[1] - c1[1]) * t,
    c1[2] + (c2[2] - c1[2]) * t,
  ];
}

function colorCss(color) {
  return `rgb(${color[0] | 0}, ${color[1] | 0}, ${color[2] | 0})`;
}

function displayProgress(t, elapsedMs = null, durationMs = DURATION_MS) {
  const shimmerStartMs = wave2EndMs(durationMs);
  const shimmerEndMs = shimmerStartMs + shimmerDurationMs();

  if (elapsedMs != null && elapsedMs >= shimmerStartMs) {
    if (elapsedMs >= shimmerEndMs) return 100;
    const shimmerP = (elapsedMs - shimmerStartMs) / shimmerDurationMs();
    return Math.round(
      SHIMMER_PROGRESS_START + shimmerP * (100 - SHIMMER_PROGRESS_START),
    );
  }

  if (t >= WAVE2_END) return SHIMMER_PROGRESS_START;
  if (t >= WAVE2_START) {
    const p = (t - WAVE2_START) / (WAVE2_END - WAVE2_START);
    return Math.round(
      65 + Math.min(1, p) * (SHIMMER_PROGRESS_START - 65),
    );
  }
  if (t >= WAVE1_START) {
    const wave1End = WAVE1_START + WAVE1_SPAN;
    const p = Math.min(1, (t - WAVE1_START) / Math.max(0.001, wave1End - WAVE1_START));
    return Math.round(35 + p * 30);
  }
  if (t >= WAVE0_START) {
    const wave0End = WAVE0_START + WAVE0_SPAN;
    const p = Math.min(1, (t - WAVE0_START) / Math.max(0.001, wave0End - WAVE0_START));
    return Math.round(p * 35);
  }
  return 0;
}

function progressBarColor(progress, loaderTheme = "light") {
  const accent = loaderAccentRgb(loaderTheme);
  if (progress >= 65) return colorCss(accent);
  if (progress >= 35) {
    const p = (progress - 35) / 30;
    return colorCss(lerpColor(PIXEL_GRAY, accent, p));
  }
  return colorCss(PIXEL_GRAY);
}

function withLogoTransform(ctx, layout, t, drawFn) {
  drawFn();
}

/** Фаза 0 — серые пиксели; фаза 1 — синие/белые; обе слева направо. */
function buildLogoCells(logoData, logoWidth, logoHeight, cell, accentColor = null) {
  const cols = Math.ceil(logoWidth / cell);
  const rows = Math.ceil(logoHeight / cell);
  const colDenom = Math.max(1, cols - 1);
  const cells = [];

  for (let row = 0; row < rows; row += 1) {
    for (let col = 0; col < cols; col += 1) {
      const cx = Math.min(logoWidth - 1, col * cell + Math.floor(cell / 2));
      const cy = Math.min(logoHeight - 1, row * cell + Math.floor(cell / 2));
      const index = (cy * logoWidth + cx) * 4;
      if (!isLogoInk(logoData, index)) continue;

      const r = logoData[index];
      const g = logoData[index + 1];
      const b = logoData[index + 2];
      const noise = (hashCell(col, row) % 1000) / 1000;
      const sweep = Math.max(0, Math.min(1, col / colDenom + (noise - 0.5) * 0.028));
      const color = accentColor ?? [r, g, b];

      cells.push({
        col,
        row,
        wave0At: WAVE0_START + sweep * WAVE0_SPAN,
        wave1At: WAVE1_START + sweep * WAVE1_SPAN,
        color,
        gray: desaturateColor(r, g, b),
      });
    }
  }

  return cells;
}

function wave2FrontX(t, layout) {
  if (t < WAVE2_START) {
    return layout.offsetX;
  }
  const progress = easeInOutQuad(Math.min(1, (t - WAVE2_START) / WAVE2_SPAN));
  return layout.offsetX + layout.drawWidth * progress;
}

function drawBackground(ctx, width, height, layout, t, plain = false, transparent = false) {
  if (transparent) {
    ctx.clearRect(0, 0, width, height);
    return;
  }

  if (plain) {
    ctx.fillStyle = "#ffffff";
    ctx.fillRect(0, 0, width, height);
    return;
  }

  const cx = width / 2;
  const cy = layout ? layout.offsetY + layout.drawHeight / 2 : height * 0.44;
  const bgGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(width, height) * 0.72);
  bgGrad.addColorStop(0, "#FAFAF8");
  bgGrad.addColorStop(1, "#F0F0EE");
  ctx.fillStyle = bgGrad;
  ctx.fillRect(0, 0, width, height);

  const animating = t > 0 && t < HOLD_END;
  const logoCx = layout ? layout.offsetX + layout.drawWidth / 2 : cx;
  const logoCy = layout ? layout.offsetY + layout.drawHeight / 2 : cy;
  const focusRadius = layout
    ? Math.max(layout.drawWidth, layout.drawHeight) * 0.72
    : Math.min(width, height) * 0.35;

  for (let x = GRID_STEP / 2; x < width; x += GRID_STEP) {
    for (let y = GRID_STEP / 2; y < height; y += GRID_STEP) {
      let alpha = 0.055;
      if (layout && animating) {
        const dist = Math.hypot(x - logoCx, y - logoCy);
        if (dist < focusRadius) {
          alpha += 0.045 * (1 - dist / focusRadius);
        }
      }
      ctx.fillStyle = `rgba(26, 30, 28, ${alpha})`;
      ctx.fillRect(x, y, 1, 1);
    }
  }

  const vig = ctx.createRadialGradient(cx, cy, Math.min(width, height) * 0.28, cx, cy, Math.max(width, height) * 0.82);
  vig.addColorStop(0, "rgba(26, 30, 28, 0)");
  vig.addColorStop(1, "rgba(26, 30, 28, 0.07)");
  ctx.fillStyle = vig;
  ctx.fillRect(0, 0, width, height);
}

function ensureLoaderChrome(root, hideChrome = false) {
  let canvas = root.querySelector(".wesp-logo-loader__canvas");
  let footer = root.querySelector(".wesp-logo-loader__footer");

  if (!canvas) {
    canvas = document.createElement("canvas");
    canvas.className = "wesp-logo-loader__canvas";
    root.appendChild(canvas);
  }

  if (!footer && !hideChrome) {
    footer = document.createElement("div");
    footer.className = "wesp-logo-loader__footer";
    footer.innerHTML = `
      <div class="wesp-logo-loader__footer-row">
        <span class="wesp-logo-loader__label">Загрузка</span>
        <span class="wesp-logo-loader__percent">00%</span>
      </div>
      <div class="wesp-logo-loader__bar">
        <div class="wesp-logo-loader__bar-fill"></div>
      </div>
      <p class="wesp-logo-loader__tagline" aria-live="polite"></p>`;
    root.appendChild(footer);
  }

  return {
    canvas,
    percentEl: footer?.querySelector(".wesp-logo-loader__percent") ?? null,
    barFill: footer?.querySelector(".wesp-logo-loader__bar-fill") ?? null,
    taglineEl: footer?.querySelector(".wesp-logo-loader__tagline") ?? null,
  };
}

function computeLogoLayout(viewport, logoWidth, logoHeight, embedded = false) {
  const maxWidth = viewport.w * (embedded ? 0.94 : 0.88);
  const maxHeight = viewport.h * (embedded ? 0.88 : 0.34);
  const scale = Math.min(maxWidth / logoWidth, maxHeight / logoHeight);
  const drawWidth = logoWidth * scale;
  const drawHeight = logoHeight * scale;
  const offsetX = (viewport.w - drawWidth) / 2;
  const offsetY = embedded
    ? (viewport.h - drawHeight) / 2
    : viewport.h * 0.44 - drawHeight / 2;

  return { scale, drawWidth, drawHeight, offsetX, offsetY };
}

function drawSharpLogo(ctx, offBlue, layout, alpha = 1) {
  if (alpha <= 0) return;
  ctx.save();
  ctx.imageSmoothingEnabled = true;
  ctx.globalAlpha = alpha;
  ctx.drawImage(offBlue, layout.offsetX, layout.offsetY, layout.drawWidth, layout.drawHeight);
  ctx.restore();
}

function pixelGrid(layout, cell, logoWidth, logoHeight) {
  const cols = Math.ceil(logoWidth / cell);
  const rows = Math.ceil(logoHeight / cell);
  return {
    originX: layout.offsetX,
    originY: layout.offsetY,
    cellW: layout.drawWidth / cols,
    cellH: layout.drawHeight / rows,
  };
}

function cellRect(grid, col, row) {
  const x0 = grid.originX + col * grid.cellW;
  const y0 = grid.originY + row * grid.cellH;
  const x1 = grid.originX + (col + 1) * grid.cellW;
  const y1 = grid.originY + (row + 1) * grid.cellH;
  return {
    x: Math.round(x0),
    y: Math.round(y0),
    w: Math.max(1, Math.round(x1) - Math.round(x0)),
    h: Math.max(1, Math.round(y1) - Math.round(y0)),
  };
}

function drawPixelBlock(ctx, rect, color, alpha) {
  if (alpha <= 0) return;
  const gap = Math.min(PIXEL_GAP, Math.max(0, rect.w - 1), Math.max(0, rect.h - 1));
  const inset = gap * 0.5;
  const x = rect.x + inset;
  const y = rect.y + inset;
  const w = Math.max(1, rect.w - gap);
  const h = Math.max(1, rect.h - gap);
  ctx.fillStyle = rgbaString(color[0], color[1], color[2], alpha);
  ctx.fillRect(x, y, w, h);
}

function pixelAppear(t, startAt) {
  const appear = Math.min(1, (t - startAt) / PIXEL_FADE);
  return appear >= 0.7 ? 1 : appear;
}

function resolvePixelColor(item, t) {
  if (t < item.wave1At) return item.gray;
  const blend = Math.min(1, (t - item.wave1At) / COLOR_CROSSFADE);
  if (blend >= 1) return item.color;
  return lerpColor(item.gray, item.color, blend);
}

function resolvePixelAlpha(item, t) {
  if (t < item.wave0At) return 0;
  let alpha = pixelAppear(t, item.wave0At);
  if (t >= item.wave1At) {
    alpha = Math.max(alpha, pixelAppear(t, item.wave1At));
  }
  return alpha;
}

function featherPixelAlpha(alpha, cellMid, frontX, feather) {
  if (cellMid <= frontX || cellMid >= frontX + feather) return alpha;
  return alpha * ((cellMid - frontX) / feather);
}

function drawSharpLogoFeathered(ctx, logoCanvas, layout, frontX, feather) {
  const left = layout.offsetX;
  const top = layout.offsetY;
  const width = layout.drawWidth;
  const height = layout.drawHeight;
  const softStart = Math.max(left, frontX - feather);

  if (softStart > left + 0.5) {
    ctx.save();
    ctx.beginPath();
    ctx.rect(left, top, softStart - left, height);
    ctx.clip();
    drawSharpLogo(ctx, logoCanvas, layout, 1);
    ctx.restore();
  }

  const bandLeft = softStart;
  const bandRight = Math.min(left + width, frontX);
  if (bandRight <= bandLeft + 0.5) return;

  ctx.save();
  ctx.beginPath();
  ctx.rect(bandLeft, top, bandRight - bandLeft, height);
  ctx.clip();
  drawSharpLogo(ctx, logoCanvas, layout, 1);
  ctx.globalCompositeOperation = "destination-in";
  const grad = ctx.createLinearGradient(bandLeft, 0, bandRight, 0);
  grad.addColorStop(0, "rgba(255,255,255,1)");
  grad.addColorStop(1, "rgba(255,255,255,0)");
  ctx.fillStyle = grad;
  ctx.fillRect(bandLeft, top, bandRight - bandLeft, height);
  ctx.restore();
}

function drawShimmer(ctx, layout, logoCanvas, shimmerT, loaderTheme = "light") {
  const darkShimmer = loaderUsesDarkShimmer(loaderTheme);
  const bandW = layout.drawWidth * (darkShimmer ? 0.36 : 0.14);
  const x = layout.offsetX + (layout.drawWidth + bandW) * shimmerT - bandW;
  const left = layout.offsetX;
  const top = layout.offsetY;
  const width = layout.drawWidth;
  const height = layout.drawHeight;

  ctx.save();
  ctx.beginPath();
  ctx.rect(left, top, width, height);
  ctx.clip();

  if (darkShimmer) {
    const [gr, gg, gb] = PIXEL_GRAY;
    const grayWave = ctx.createLinearGradient(x, 0, x + bandW, 0);
    grayWave.addColorStop(0, "rgba(255,255,255,0)");
    grayWave.addColorStop(0.22, "rgba(255,255,255,0)");
    grayWave.addColorStop(0.5, `rgba(${gr},${gg},${gb},0.95)`);
    grayWave.addColorStop(0.78, "rgba(255,255,255,0)");
    grayWave.addColorStop(1, "rgba(255,255,255,0)");
    ctx.globalCompositeOperation = "source-atop";
    ctx.fillStyle = grayWave;
    ctx.fillRect(left, top, width, height);
    ctx.restore();
    return;
  }

  const peak = 0.72;
  const highlight = ctx.createLinearGradient(x, 0, x + bandW, 0);
  highlight.addColorStop(0, "rgba(255,255,255,0)");
  highlight.addColorStop(0.3, "rgba(255,255,255,0)");
  highlight.addColorStop(0.5, `rgba(255,255,255,${peak})`);
  highlight.addColorStop(0.7, "rgba(255,255,255,0)");
  highlight.addColorStop(1, "rgba(255,255,255,0)");
  ctx.globalCompositeOperation = "lighter";
  ctx.fillStyle = highlight;
  ctx.fillRect(left, top, width, height);
  ctx.globalCompositeOperation = "destination-in";
  ctx.drawImage(logoCanvas, left, top, width, height);
  ctx.restore();
}

/** Фаза 0 — серые; фаза 1 — цветные; фаза 2 — плавная сборка по контуру надписи. */
function drawWaves(ctx, layout, cell, cells, t, logoCanvas, logoWidth, logoHeight) {
  const frontX = wave2FrontX(t, layout);
  const sharpRight = layout.offsetX + layout.drawWidth;
  const grid = pixelGrid(layout, cell, logoWidth, logoHeight);
  const inWave2 = t >= WAVE2_START;

  if (inWave2 && frontX > layout.offsetX + 1) {
    drawSharpLogoFeathered(ctx, logoCanvas, layout, frontX, ASSEMBLY_FEATHER);
  }

  if (inWave2 && frontX >= sharpRight - 1) {
    return;
  }

  for (const item of cells) {
    if (t < item.wave0At) continue;

    const rect = cellRect(grid, item.col, item.row);
    const cellMid = rect.x + rect.w * 0.5;
    if (inWave2 && cellMid < frontX - ASSEMBLY_FEATHER) continue;

    let alpha = resolvePixelAlpha(item, t);
    if (inWave2) {
      alpha = featherPixelAlpha(alpha, cellMid, frontX, ASSEMBLY_FEATHER);
    }
    if (alpha <= 0) continue;

    drawPixelBlock(ctx, rect, resolvePixelColor(item, t), alpha);
  }
}

function updateProgressChrome(
  percentEl,
  barFill,
  t,
  getExternalProgress,
  loaderTheme,
  elapsedMs = null,
  durationMs = DURATION_MS,
) {
  let progress = displayProgress(t, elapsedMs, durationMs);
  const external = getExternalProgress();
  if (external != null) {
    progress = Math.max(progress, Math.min(100, Math.round(external * 100)));
  }

  if (percentEl) percentEl.textContent = `${String(progress).padStart(2, "0")}%`;
  if (barFill) {
    barFill.style.width = `${progress}%`;
    barFill.style.background = progressBarColor(progress, loaderTheme);
  }

  return progress;
}

export async function mountWespLogoLoader(root, options = {}) {
  if (!root) {
    throw new Error("mountWespLogoLoader: root element is required");
  }

  const blueSrc = options.blueSrc ?? options.whiteSrc ?? DEFAULT_BLUE_SRC;
  const initialCell = options.cell ?? options.pixel ?? INITIAL_CELL;
  const loop = options.loop !== false;
  const durationMs = options.durationMs ?? DURATION_MS;
  const totalDurationMs = options.totalDurationMs ?? null;
  const playbackEndMs = resolvePlaybackEndMs(durationMs, totalDurationMs);
  const embedded = !!options.embedded;
  const plainBackground = !!options.plainBackground;
  const transparentBackground = !!options.transparentBackground;
  const knockoutBackground = !!options.knockoutBackground;
  const loaderTheme = resolveLoaderTheme(options);
  const staticLogo = !!options.staticLogo;
  const hideChrome = !!options.hideChrome;
  const onComplete = typeof options.onComplete === "function" ? options.onComplete : null;
  const getExternalProgress = typeof options.progress === "function"
    ? options.progress
    : () => (options.progress ?? null);

  root.classList.add("wesp-logo-loader");
  if (embedded) root.classList.add("wesp-logo-loader--embedded");
  if (plainBackground) root.classList.add("wesp-logo-loader--plain");
  if (transparentBackground) root.classList.add("wesp-logo-loader--transparent");
  if (hideChrome) root.classList.add("wesp-logo-loader--no-chrome");

  const { canvas, percentEl, barFill } = ensureLoaderChrome(root, hideChrome);
  const ctx = canvas.getContext("2d", { willReadFrequently: true });
  if (transparentBackground) {
    canvas.style.background = "transparent";
  }

  const blueImg = await loadImage(blueSrc);
  const logoWidth = blueImg.naturalWidth || LOGO_SRC_WIDTH;
  const logoHeight = blueImg.naturalHeight || LOGO_SRC_HEIGHT;
  const offBlue = drawLogoToBuffer(
    blueImg,
    logoWidth,
    logoHeight,
    knockoutBackground,
  );
  const accentRgb = loaderAccentRgb(loaderTheme);
  const displayLogo = loaderTheme === "light"
    ? offBlue
    : cloneLogoBuffer(offBlue, logoWidth, logoHeight, accentRgb);
  const waveAccent = loaderTheme === "light" ? null : accentRgb;
  const logoData = offBlue.getContext("2d").getImageData(0, 0, logoWidth, logoHeight).data;
  const logoCells = buildLogoCells(
    logoData,
    logoWidth,
    logoHeight,
    initialCell,
    waveAccent,
  );

  let viewport = { w: 0, h: 0, dpr: 1 };
  let layout = null;
  let rafId = 0;
  let start = performance.now();
  let disposed = false;
  let completedFired = false;

  function finishIfDone(elapsedMs) {
    if (loop || elapsedMs < playbackEndMs || completedFired || !onComplete) return;
    completedFired = true;
    window.setTimeout(() => onComplete(), 120);
  }

  function readViewportSize() {
    if (embedded) {
      const rect = root.getBoundingClientRect();
      return {
        w: Math.max(1, rect.width || root.clientWidth || 1),
        h: Math.max(1, rect.height || root.clientHeight || 1),
      };
    }
    return { w: globalThis.innerWidth, h: globalThis.innerHeight };
  }

  function resize() {
    const dpr = Math.min(globalThis.devicePixelRatio || 1, 2);
    const size = readViewportSize();
    viewport = { w: size.w, h: size.h, dpr };

    canvas.width = viewport.w * dpr;
    canvas.height = viewport.h * dpr;
    canvas.style.width = `${viewport.w}px`;
    canvas.style.height = `${viewport.h}px`;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.imageSmoothingEnabled = false;

    layout = computeLogoLayout(viewport, logoWidth, logoHeight, embedded);
  }

  function drawStaticLogo() {
    resize();
    drawBackground(ctx, viewport.w, viewport.h, layout, 1, plainBackground, transparentBackground);
    if (layout) drawSharpLogo(ctx, displayLogo, layout, 1);
    updateProgressChrome(
      percentEl,
      barFill,
      1,
      getExternalProgress,
      loaderTheme,
      animationEndMs(durationMs),
      durationMs,
    );
    finishIfDone(playbackEndMs);
  }

  function drawAnimatedFrame(t, elapsedMs) {
    const shimmerMs = shimmerDurationMs();
    const shimmerStartMs = wave2EndMs(durationMs);

    withLogoTransform(ctx, layout, t, () => {
      if (t >= WAVE2_END) {
        drawSharpLogo(ctx, displayLogo, layout, 1);
        if (
          elapsedMs >= shimmerStartMs
          && elapsedMs < shimmerStartMs + shimmerMs
        ) {
          drawShimmer(
            ctx,
            layout,
            displayLogo,
            (elapsedMs - shimmerStartMs) / shimmerMs,
            loaderTheme,
          );
        }
        return;
      }
      drawWaves(ctx, layout, initialCell, logoCells, t, displayLogo, logoWidth, logoHeight);
    });
  }

  function drawStaticIntroFrame(t) {
    drawBackground(ctx, viewport.w, viewport.h, layout, t, plainBackground, transparentBackground);
    if (!layout) return;
    const alpha = easeOutQuad(Math.min(1, t / 0.82));
    drawSharpLogo(ctx, displayLogo, layout, alpha);
  }

  function cycleElapsedMs(elapsed) {
    if (!loop) return elapsed;
    return elapsed % durationMs;
  }

  function drawFrame(now) {
    if (disposed) return;

    const elapsed = now - start;
    const cycleMs = cycleElapsedMs(elapsed);
    const t = loop
      ? cycleMs / durationMs
      : Math.min(1, elapsed / durationMs);
    const shimmerMs = shimmerDurationMs();
    const shimmerStartMs = wave2EndMs(durationMs);
    const inShimmer =
      cycleMs >= shimmerStartMs && cycleMs < shimmerStartMs + shimmerMs;

    updateProgressChrome(
      percentEl,
      barFill,
      t,
      getExternalProgress,
      loaderTheme,
      cycleMs,
      durationMs,
    );

    if (staticLogo) {
      drawStaticIntroFrame(t);
      finishIfDone(elapsed);
      if (loop || elapsed < playbackEndMs) {
        rafId = requestAnimationFrame(drawFrame);
      }
      return;
    }

    drawBackground(ctx, viewport.w, viewport.h, layout, t, plainBackground, transparentBackground);
    if (!layout) return;

    if (t < HOLD_END || inShimmer) {
      drawAnimatedFrame(t, cycleMs);
    } else if (loop) {
      withLogoTransform(ctx, layout, t, () => {
        const fade = 1 - easeOutQuad((t - HOLD_END) / (1 - HOLD_END));
        drawSharpLogo(ctx, displayLogo, layout, fade);
      });
    } else {
      withLogoTransform(ctx, layout, t, () => {
        drawSharpLogo(ctx, displayLogo, layout, 1);
      });
    }

    finishIfDone(elapsed);

    if (loop || elapsed < playbackEndMs) {
      rafId = requestAnimationFrame(drawFrame);
    }
  }

  resize();
  globalThis.addEventListener("resize", resize);

  if (prefersReducedMotion() || staticLogo) {
    if (staticLogo) {
      rafId = requestAnimationFrame(drawFrame);
    } else {
      drawStaticLogo();
    }
  } else {
    rafId = requestAnimationFrame(drawFrame);
  }

  return function disposeWespLogoLoader() {
    if (disposed) return;
    disposed = true;
    cancelAnimationFrame(rafId);
    globalThis.removeEventListener("resize", resize);
    root.classList.add("wesp-logo-loader--exiting");
    window.setTimeout(() => {
      root.classList.add("wesp-logo-loader--hidden");
      window.setTimeout(() => {
        if (root.isConnected) root.remove();
      }, 480);
    }, 520);
  };
}

export async function showWespLogoLoader(options = {}) {
  const root = document.createElement("div");
  root.id = options.id ?? "wespBootLoader";
  root.className = "wesp-logo-loader";
  root.setAttribute("aria-busy", "true");
  root.setAttribute("aria-label", options.label ?? "Загрузка");
  document.body.appendChild(root);
  return mountWespLogoLoader(root, options);
}
