(function () {
  "use strict";

  const MAX_HISTORY = 2;
  const HISTORY_KEY = "aivt_recent_photos";
  const HISTORY_TTL = 24 * 60 * 60 * 1000; // 24 hours in ms

  // Fix #8: debug only via URL param, never from aivtConfig
  var debug = window.location.search.indexOf('aivt_debug=1') !== -1;

  // Fix #5: isGenerating lock — prevents double-tap quota waste
  var isGenerating = false;

  let userFile = null;
  let userFileDataUrl = null;
  let progressInterval = null;

  const $ = (selector) => document.querySelector(selector);
  const $$ = (selector) => document.querySelectorAll(selector);

  let eventsAttached = false;

  function init() {
    const btn = $("#aivt-tryon-btn");
    if (!btn) return;

    btn.addEventListener("click", openModal);

    // ESC key
    document.addEventListener("keydown", (e) => {
      if (e.key === "Escape") closeModal();
    });
  }

  function attachModalEvents() {
    if (eventsAttached) return;
    eventsAttached = true;

    // Close handlers
    $$("[data-aivt-close]").forEach((el) => {
      el.addEventListener("click", closeModal);
    });

    // File input change
    const fileInput = $("#aivt-user-image");
    if (fileInput) {
      fileInput.addEventListener("change", function(e) {
        if (e.target.files && e.target.files.length > 0) {
          handleFile(e.target.files[0]);
        }
      });
    }

    // Fallback: if label click doesn't trigger file input in some browsers
    const uploadLabel = $("#aivt-upload-btn");
    if (uploadLabel && fileInput) {
      uploadLabel.addEventListener("click", function(e) {
        setTimeout(function() {
          if (document.activeElement !== fileInput) {
            fileInput.click();
          }
        }, 50);
      });
    }

    // Change photo button
    const changeBtn = $("#aivt-change-photo");
    if (changeBtn) {
      changeBtn.addEventListener("click", function(e) {
        e.preventDefault();
        resetToUpload();
      });
    }

    // Fix #5: Generate button — lock on click, disable while generating
    const genBtn = $("#aivt-generate-btn");
    if (genBtn) {
      genBtn.addEventListener("click", function(e) {
        e.preventDefault();
        if (isGenerating) return; // silently ignore double-taps
        generate();
      });
    }

    // View product button
    const viewBtn = $("#aivt-view-product-btn");
    if (viewBtn) {
      viewBtn.addEventListener("click", function(e) {
        e.preventDefault();
        closeModal();
        const productSection = document.querySelector(".product, .product-single, [data-product]");
        if (productSection) {
          productSection.scrollIntoView({ behavior: "smooth", block: "start" });
        }
      });
    }

    // Try again button
    const tryAgainBtn = $("#aivt-try-again");
    if (tryAgainBtn) {
      tryAgainBtn.addEventListener("click", function(e) {
        e.preventDefault();
        resetToUpload();
      });
    }

    // Remove preview button
    const removeBtn = $("#aivt-remove-preview");
    if (removeBtn) {
      removeBtn.addEventListener("click", function(e) {
        e.preventDefault();
        const resultImg = $("#aivt-result-img");
        if (resultImg) resultImg.src = "";
        resetToUpload();
      });
    }

    // Retry from error button
    const retryBtn = $("#aivt-retry-btn");
    if (retryBtn) {
      retryBtn.addEventListener("click", function(e) {
        e.preventDefault();
        resetToUpload();
      });
    }

    // History button
    const historyBtn = $("#aivt-history-btn");
    if (historyBtn) {
      historyBtn.addEventListener("click", function(e) {
        e.preventDefault();
        toggleHistory();
      });
    }

    // History close button
    const historyCloseBtn = $("#aivt-history-close");
    if (historyCloseBtn) {
      historyCloseBtn.addEventListener("click", function(e) {
        e.preventDefault();
        closeHistory();
      });
    }
  }

  function openModal() {
    const modal = $("#aivt-modal");
    if (!modal) return;

    attachModalEvents();

    const productImg = $("#aivt-product-img");
    const productImgConfirm = $("#aivt-product-img-confirm");
    if (productImg && aivtConfig.productImageUrl) {
      productImg.src = aivtConfig.productImageUrl;
    }
    if (productImgConfirm && aivtConfig.productImageUrl) {
      productImgConfirm.src = aivtConfig.productImageUrl;
    }

    applyTheme();

    modal.style.display = "flex";
    document.body.style.overflow = "hidden";
    showStep("upload");
    loadHistory();
  }

  function closeModal() {
    const modal = $("#aivt-modal");
    if (modal) {
      modal.style.display = "none";
      document.body.style.overflow = "";
    }
    closeHistory();
    stopProgress();
  }

  function applyTheme() {
    const content = $(".aivt-modal-content");
    if (!content) return;

    const root = document.documentElement;

    if (aivtConfig.primaryColor) {
      root.style.setProperty("--aivt-primary", aivtConfig.primaryColor);
    }
    if (aivtConfig.accentColor) {
      root.style.setProperty("--aivt-accent", aivtConfig.accentColor);
    }
    if (aivtConfig.borderRadius) {
      root.style.setProperty("--aivt-radius", aivtConfig.borderRadius + "px");
      root.style.setProperty("--aivt-radius-lg", (parseInt(aivtConfig.borderRadius) + 4) + "px");
    }

    if (aivtConfig.darkMode) {
      content.classList.add("aivt-dark");
    } else {
      content.classList.remove("aivt-dark");
    }
  }

  function showStep(step) {
    ["upload", "confirm", "processing", "result", "error"].forEach((s) => {
      const el = $(`#aivt-step-${s}`);
      if (el) el.style.display = s === step ? "flex" : "none";
    });
  }

  function isHeicFile(file) {
    if (file.type === "image/heic" || file.type === "image/heif") return true;
    if (file.name && /\.(heic|heif)$/i.test(file.name)) return true;
    return false;
  }

  function convertHeicToJpeg(file) {
    return new Promise(function(resolve, reject) {
      if (window.heic2any) {
        window.heic2any({ blob: file, toType: "image/jpeg", quality: 0.85 })
          .then(function(blob) { resolve(blob); })
          .catch(function() { reject(new Error("Could not convert HEIC image. Please export as JPEG from your Photos app.")); });
        return;
      }
      var script = document.createElement("script");
      // Bundled locally in the plugin (no remote CDN) — URL passed via aivtConfig
      script.src = (window.aivtConfig && aivtConfig.heicUrl) ? aivtConfig.heicUrl : "";
      if (!script.src) { reject(new Error("Could not load HEIC converter. Please export your photo as JPEG from the Photos app.")); return; }
      script.onload = function() {
        window.heic2any({ blob: file, toType: "image/jpeg", quality: 0.85 })
          .then(function(blob) { resolve(blob); })
          .catch(function() { reject(new Error("Could not convert HEIC image. Please export as JPEG from your Photos app.")); });
      };
      script.onerror = function() { reject(new Error("Could not load HEIC converter. Please export your photo as JPEG from the Photos app.")); };
      document.head.appendChild(script);
    });
  }

  function handleFile(file) {
    var allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif", "image/heic", "image/heif"];
    if (!allowedTypes.includes(file.type) && !isHeicFile(file)) {
      showError("Please upload a JPEG, PNG, WebP, or HEIC image.");
      return;
    }

    if (file.size > 15 * 1024 * 1024) {
      showError("Image must be under 15MB.");
      return;
    }

    if (isHeicFile(file)) {
      showStep("processing");
      convertHeicToJpeg(file).then(function(jpegBlob) {
        return normalizeImage(new File([jpegBlob], "photo.jpg", { type: "image/jpeg" }));
      }).then(function(normalizedBlob) {
        userFile = new File([normalizedBlob], "photo.jpg", { type: "image/jpeg" });
        var reader = new FileReader();
        reader.onload = function(e) {
          userFileDataUrl = e.target.result;
          var userImg = $("#aivt-user-img");
          if (userImg) userImg.src = userFileDataUrl;
          showStep("confirm");
        };
        reader.readAsDataURL(normalizedBlob);
      }).catch(function(err) {
        showStep("upload");
        showError(err.message || "Could not process HEIC image.");
      });
      return;
    }

    normalizeImage(file).then(function(normalizedBlob) {
      userFile = new File([normalizedBlob], "photo.jpg", { type: "image/jpeg" });
      var reader = new FileReader();
      reader.onload = function(e) {
        userFileDataUrl = e.target.result;
        var userImg = $("#aivt-user-img");
        if (userImg) userImg.src = userFileDataUrl;
        showStep("confirm");
      };
      reader.onerror = function() {
        showError("Could not read the image. Please try again.");
      };
      reader.readAsDataURL(normalizedBlob);
    }).catch(function(err) {
      showError(err.message || "Could not process image. Please try again.");
    });
  }

  // Normalize: convert to JPEG, resize to max 2048px, compress to ~500KB-1.5MB
  function normalizeImage(file) {
    return new Promise(function(resolve, reject) {
      // HEIC must be converted before canvas — handled by convertHeicToJpeg() in handleFile
      if (isHeicFile(file)) {
        return convertHeicToJpeg(file).then(function(jpegBlob) {
          normalizeImage(new File([jpegBlob], "photo.jpg", { type: "image/jpeg" })).then(resolve).catch(reject);
        }).catch(reject);
      }
      var img = new Image();
      img.crossOrigin = "anonymous";
      var objectUrl = URL.createObjectURL(file);
      img.onload = function() {
        URL.revokeObjectURL(objectUrl);
        var MAX_DIM = 2048;
        var w = img.naturalWidth;
        var h = img.naturalHeight;
        if (w > MAX_DIM || h > MAX_DIM) {
          if (w >= h) { h = Math.round(h * MAX_DIM / w); w = MAX_DIM; }
          else        { w = Math.round(w * MAX_DIM / h); h = MAX_DIM; }
        }
        var canvas = document.createElement("canvas");
        canvas.width  = w;
        canvas.height = h;
        var ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0, w, h);

        var quality = 0.85;
        var attempt = function() {
          canvas.toBlob(function(blob) {
            if (!blob) return reject(new Error("Failed to compress image."));
            if (blob.size > 1.5 * 1024 * 1024 && quality > 0.5) {
              quality -= 0.1;
              attempt();
            } else {
              resolve(blob);
            }
          }, "image/jpeg", quality);
        };
        attempt();
      };
      img.onerror = function() {
        URL.revokeObjectURL(objectUrl);
        reject(new Error("Could not read image. Please try a JPEG or PNG."));
      };
      img.src = objectUrl;
    });
  }

  function resetToUpload() {
    // Fix #5: release lock when returning to upload step
    setGenerating(false);
    userFile = null;
    userFileDataUrl = null;
    showStep("upload");
    const fileInput = $("#aivt-user-image");
    if (fileInput) fileInput.value = "";
    stopProgress();
  }

  // Fix #5: centralised lock setter — also updates button state + text
  function setGenerating(value) {
    isGenerating = value;
    var genBtn = $("#aivt-generate-btn");
    if (!genBtn) return;
    if (value) {
      genBtn.disabled = true;
      genBtn.textContent = "Generating...";
    } else {
      genBtn.disabled = false;
      genBtn.textContent = "Try It On Now";
    }
  }

  // ============================================
  // HISTORY MANAGEMENT (max 2 photos, 24h TTL)
  // ============================================

  function loadHistory() {
    const history = getHistory();
    const list = $("#aivt-history-list");
    const empty = $("#aivt-history-empty");

    if (!list) return;

    list.innerHTML = "";

    if (history.length === 0) {
      if (empty) empty.style.display = "block";
      return;
    }

    if (empty) empty.style.display = "none";

    history.forEach((item, index) => {
      const div = document.createElement("div");
      div.className = "aivt-history-item";
      div.innerHTML = `
        <img src="${item.dataUrl}" alt="Recent photo" />
        <div class="aivt-history-item-actions">
          <button type="button" data-action="use" data-index="${index}" title="Use this photo">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <polyline points="9 11 12 14 22 4"/>
              <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
            </svg>
          </button>
          <button type="button" data-action="delete" data-index="${index}" title="Delete">
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
              <polyline points="3 6 5 6 21 6"/>
              <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
            </svg>
          </button>
        </div>
      `;

      div.querySelector('[data-action="use"]').addEventListener("click", (e) => {
        e.stopPropagation();
        useHistoryPhoto(index);
      });

      div.querySelector('[data-action="delete"]').addEventListener("click", (e) => {
        e.stopPropagation();
        deleteHistoryPhoto(index);
      });

      list.appendChild(div);
    });
  }

  function getHistory() {
    try {
      const data = localStorage.getItem(HISTORY_KEY);
      if (!data) return [];
      var all = JSON.parse(data);
      // Fix #7: filter out entries older than 24 hours on read
      var now = Date.now();
      var fresh = all.filter(function(item) {
        return item.timestamp && (now - item.timestamp) < HISTORY_TTL;
      });
      // Persist pruned list silently if items were removed
      if (fresh.length !== all.length) {
        try { localStorage.setItem(HISTORY_KEY, JSON.stringify(fresh)); } catch(e) {}
      }
      return fresh;
    } catch {
      return [];
    }
  }

  function saveToHistory(dataUrl) {
    var history = getHistory();

    var existingIndex = history.findIndex(function(item) { return item.dataUrl === dataUrl; });
    if (existingIndex !== -1) {
      var existing = history.splice(existingIndex, 1)[0];
      existing.timestamp = Date.now(); // refresh TTL on re-use
      history.unshift(existing);
    } else {
      history.unshift({ dataUrl: dataUrl, timestamp: Date.now() });
    }

    while (history.length > MAX_HISTORY) {
      history.pop();
    }

    // Fix #7: silent try/catch — never crash if storage is full
    try {
      localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
    } catch {
      // Storage full or disabled — silently skip
    }
  }

  function useHistoryPhoto(index) {
    var history = getHistory();
    if (history[index]) {
      userFileDataUrl = history[index].dataUrl;
      userFile = null;
      $("#aivt-user-img").src = userFileDataUrl;
      showStep("confirm");
      closeHistory();
    }
  }

  function deleteHistoryPhoto(index) {
    var history = getHistory();
    history.splice(index, 1);
    try {
      localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
    } catch {}
    loadHistory();
  }

  function toggleHistory() {
    const panel = $("#aivt-history-panel");
    if (panel) {
      panel.style.display = panel.style.display === "none" ? "flex" : "none";
    }
  }

  function closeHistory() {
    const panel = $("#aivt-history-panel");
    if (panel) panel.style.display = "none";
  }

  // ============================================
  // PROGRESS ANIMATION
  // ============================================

  function startProgress() {
    let progress = 0;
    const fill = $("#aivt-progress-fill");
    const percent = $("#aivt-progress-percent");

    if (!fill || !percent) return;

    fill.style.width = "0%";
    percent.textContent = "0%";

    progressInterval = setInterval(() => {
      if (progress < 30) {
        progress += 2;
      } else if (progress < 60) {
        progress += 1;
      } else if (progress < 85) {
        progress += 0.5;
      } else if (progress < 95) {
        progress += 0.2;
      }

      fill.style.width = Math.round(progress) + "%";
      percent.textContent = Math.round(progress) + "%";
    }, 200);
  }

  function stopProgress() {
    if (progressInterval) {
      clearInterval(progressInterval);
      progressInterval = null;
    }
  }

  function completeProgress() {
    stopProgress();
    const fill = $("#aivt-progress-fill");
    const percent = $("#aivt-progress-percent");
    if (fill) fill.style.width = "100%";
    if (percent) percent.textContent = "100%";
  }

  // ============================================
  // GENERATION — Fix #1: proxy via WP AJAX
  // ============================================

  var retryCount = 0;
  var MAX_RETRIES = 2;

  async function generate() {
    if (!userFile && !userFileDataUrl) return;

    // Fix #5: acquire lock
    setGenerating(true);
    showStep("processing");
    startProgress();
    retryCount = 0;
    await attemptGenerate();
  }

  async function attemptGenerate() {
    // Fix #1: validate AJAX config instead of apiKey
    if (!aivtConfig.ajaxUrl || !aivtConfig.nonce) {
      showError("Plugin not configured. Please check settings.");
      setGenerating(false);
      return;
    }

    if (!aivtConfig.productImageUrl) {
      showError("Product image not found. Please refresh the page.");
      setGenerating(false);
      return;
    }

    try {
      // Prepare file
      let fileToUpload = userFile;
      if (!fileToUpload && userFileDataUrl) {
        const resp = await fetch(userFileDataUrl);
        const blob = await resp.blob();
        fileToUpload = await normalizeImage(blob)
          .then(b => new File([b], "photo.jpg", { type: "image/jpeg" }))
          .catch(() => blob);
      }

      // Fix #1: Send to WordPress AJAX proxy — API key stays server-side
      // Fix #3: nonce included in every request
      var formData = new FormData();
      formData.append("action", "aivt_tryon");
      formData.append("nonce", aivtConfig.nonce);
      formData.append("user_image", fileToUpload, "photo.jpg");
      formData.append("product_image_url", aivtConfig.productImageUrl);

      var response = await fetch(aivtConfig.ajaxUrl, {
        method: "POST",
        body: formData,
        credentials: "same-origin",
      });

      var responseData = await response.json().catch(function() { return {}; });

      if (debug) console.log("[TryOnCloud] Proxy response:", response.status, responseData);

      // Fix #3: nonce expired — friendly message
      if (response.status === 403 && responseData.data && responseData.data.code === "NONCE_EXPIRED") {
        showError("Session expired. Please refresh the page and try again.");
        setGenerating(false);
        return;
      }

      if (!responseData.success) {
        var errorCode = responseData.data ? (responseData.data.code || "") : "";
        var retriable = ["GENERATION_FAILED", "SERVICE_UNAVAILABLE"];
        if ((retriable.includes(errorCode) || response.status >= 500) && retryCount < MAX_RETRIES) {
          retryCount++;
          if (debug) console.log("[TryOnCloud] Retrying " + retryCount + "/" + MAX_RETRIES);
          await new Promise(function(r) { setTimeout(r, 1500); });
          await attemptGenerate();
          return;
        }
        var message = responseData.data ? (responseData.data.message || "") : "";
        showError(message || getErrorMessage(errorCode, response.status));
        setGenerating(false);
        return;
      }

      var data = responseData.data || {};

      // Async job — poll for result
      if (data.jobId) {
        await pollForResult(data, debug);
        return;
      }

      // Direct result URL
      if (data.resultUrl) {
        completeProgress();
        showResultWithLoading(data.resultUrl);
        if (userFileDataUrl) saveToHistory(userFileDataUrl);
        setGenerating(false);
        return;
      }

      showError("Unexpected response. Please try again.");
      setGenerating(false);

    } catch (err) {
      stopProgress();
      if (debug) console.error("[TryOnCloud] Error:", err);

      if (retryCount < MAX_RETRIES) {
        retryCount++;
        await new Promise(function(resolve) { setTimeout(resolve, 1500); });
        await attemptGenerate();
        return;
      }

      showError(err.message || "Connection failed. Please check your internet.");
      setGenerating(false);
    }
  }

  // Fix #9: exponential backoff polling with progress messages
  async function pollForResult(submitData, dbg) {
    var jobId     = submitData.jobId;
    var statusUrl = aivtConfig.ajaxUrl;  // poll via proxy too if needed; direct status OK since no key
    var maxWait   = 90000; // 90 seconds hard cap
    var elapsed   = 0;
    var interval  = 2000;  // start at 2s
    var MAX_INTERVAL = 15000; // cap at 15s

    // Progress message sequence shown to user
    var messages  = [
      "Processing your try-on...",
      "Analyzing your photo...",
      "Fitting the garment...",
      "Almost there...",
      "Still working...",
    ];
    var msgIndex  = 0;
    var msgEl     = $("#aivt-progress-status");

    function updateMessage() {
      if (!msgEl) return;
      msgEl.textContent = messages[Math.min(msgIndex, messages.length - 1)];
      msgIndex++;
    }

    // Build direct status URL from baseUrl (no API key needed for status polling)
    var baseUrl = (aivtConfig.statusBaseUrl || "").replace(/\/$/, "");
    var directStatusUrl = baseUrl ? (baseUrl + "/tryon/status/" + jobId) : null;

    updateMessage();

    while (elapsed < maxWait) {
      await new Promise(function(r) { setTimeout(r, interval); });
      elapsed += interval;

      // Fix #9: update message on each cycle
      if (elapsed % 10000 === 0 || elapsed === 15000) updateMessage();

      try {
        var res, data;

        if (directStatusUrl) {
          // No API key required for status — safe direct call
          res  = await fetch(directStatusUrl);
          if (!res.ok) {
            // Backoff and continue
            interval = Math.min(interval * 2, MAX_INTERVAL);
            continue;
          }
          data = await res.json();
        } else {
          // Fallback: poll via WP AJAX proxy
          var fd = new FormData();
          fd.append("action", "aivt_tryon_status");
          fd.append("nonce", aivtConfig.nonce);
          fd.append("job_id", jobId);
          res  = await fetch(aivtConfig.ajaxUrl, { method: "POST", body: fd, credentials: "same-origin" });
          var wrapped = await res.json().catch(function() { return {}; });
          data = wrapped.data || {};
        }

        if (data.status === "completed" && data.resultUrl) {
          completeProgress();
          showResultWithLoading(data.resultUrl);
          if (userFileDataUrl) saveToHistory(userFileDataUrl);
          setGenerating(false);
          return;
        }

        if (data.status === "failed") {
          showError("Generation failed. Please try with a clearer photo.");
          setGenerating(false);
          return;
        }

        // Exponential backoff: 2s → 4s → 8s → 15s max
        interval = Math.min(interval * 2, MAX_INTERVAL);

      } catch (e) {
        if (dbg) console.warn("[TryOnCloud] Status poll error:", e);
        interval = Math.min(interval * 2, MAX_INTERVAL);
      }
    }

    // Fix #9: hard timeout — offer retry, never a dead end
    showError("This is taking longer than usual. Please try again in a moment.");
    setGenerating(false);
  }

  function getErrorMessage(errorCode, status) {
    switch (errorCode) {
      case "STORE_NOT_REGISTERED":
        return "This store is not registered with TryOnCloud.";
      case "CORS_BLOCKED":
        return "This website is not authorized.";
      case "INVALID_API_KEY":
      case "MISSING_API_KEY":
        return "Virtual try-on is not configured correctly.";
      case "MONTHLY_QUOTA_EXCEEDED":
        return "Monthly limit reached. Please try later.";
      case "DAILY_LIMIT":
        return "Daily limit reached. Please try tomorrow.";
      case "RATE_LIMIT_KEY":
      case "RATE_LIMIT_IP":
      case "RATE_LIMIT_IP_MINUTE":
      case "RATE_LIMIT_IP_HOUR":
        return "Too many requests. Please wait a moment.";
      case "CONTENT_VIOLATION":
        return "Please upload a photo of yourself wearing clothes. Explicit images are not allowed.";
      case "INVALID_USER_IMAGE":
      case "GENERATION_FAILED":
        return "Please try with a clearer photo showing your face and upper body.";
      case "SERVICE_PAUSED":
        return "Service temporarily under maintenance.";
      case "UPLOAD_EXPIRED":
        return "Upload session expired. Please try again.";
      default:
        if (status === 500 || status === 503) {
          return "Service temporarily unavailable. Please try again.";
        }
        return "Something went wrong. Please try with a different photo.";
    }
  }

  function showError(message) {
    stopProgress();
    setGenerating(false);
    const errorEl = $("#aivt-error-message");
    if (errorEl) errorEl.textContent = message;
    showStep("error");
  }

  // Show result step with loading state until image loads from GCS
  function showResultWithLoading(resultUrl) {
    var img = $("#aivt-result-img");
    var loader = $("#aivt-result-loading");
    // Reset: show spinner, hide image
    if (loader) loader.style.display = "flex";
    if (img) { img.style.opacity = "0"; img.src = ""; }
    // Show result step (with spinner visible)
    showStep("result");
    // Set src to start loading — onload handler in HTML will hide spinner
    if (img) img.src = resultUrl;
  }

  // Initialize
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }
})();
