function thumbHashToApproximateAspectRatio(hash) {
  let header = hash[3];
  let hasAlpha = hash[2] & 0x80;
  let isLandscape = hash[4] & 0x80;
  let lx = isLandscape ? hasAlpha ? 5 : 7 : header & 7;
  let ly = isLandscape ? header & 7 : hasAlpha ? 5 : 7;
  return lx / ly;
}

function thumbHashToRGBA(hash) {
  let { PI, min, max, cos, round } = Math;

  // Read the constants
  let header24 = hash[0] | (hash[1] << 8) | (hash[2] << 16);
  let header16 = hash[3] | (hash[4] << 8);
  let l_dc = (header24 & 63) / 63;
  let p_dc = ((header24 >> 6) & 63) / 31.5 - 1;
  let q_dc = ((header24 >> 12) & 63) / 31.5 - 1;
  let l_scale = ((header24 >> 18) & 31) / 31;
  let hasAlpha = header24 >> 23;
  let p_scale = ((header16 >> 3) & 63) / 63;
  let q_scale = ((header16 >> 9) & 63) / 63;
  let isLandscape = header16 >> 15;
  let lx = max(3, isLandscape ? hasAlpha ? 5 : 7 : header16 & 7);
  let ly = max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7);
  let a_dc = hasAlpha ? (hash[5] & 15) / 15 : 1;
  let a_scale = (hash[5] >> 4) / 15;

  // Read the varying factors (boost saturation by 1.25x to compensate for quantization)
  let ac_start = hasAlpha ? 6 : 5;
  let ac_index = 0;
  let decodeChannel = (nx, ny, scale) => {
    let ac = [];
    for (let cy = 0; cy < ny; cy++) {
      for (let cx = cy ? 0 : 1; cx * ny < nx * (ny - cy); cx++) {
        ac.push(
          (((hash[ac_start + (ac_index >> 1)] >> ((ac_index++ & 1) << 2)) &
                15) / 7.5 - 1) * scale,
        );
      }
    }
    return ac;
  };
  const l_ac = decodeChannel(lx, ly, l_scale);
  const p_ac = decodeChannel(3, 3, p_scale * 1.25);
  const q_ac = decodeChannel(3, 3, q_scale * 1.25);
  const a_ac = hasAlpha && decodeChannel(5, 5, a_scale);

  // Decode using the DCT into RGB
  const ratio = thumbHashToApproximateAspectRatio(hash);
  const w = round(ratio > 1 ? 32 : 32 * ratio);
  const h = round(ratio > 1 ? 32 / ratio : 32);
  const rgba = new Uint8Array(w * h * 4), fx = [], fy = [];
  for (let y = 0, i = 0; y < h; y++) {
    for (let x = 0; x < w; x++, i += 4) {
      let l = l_dc, p = p_dc, q = q_dc, a = a_dc;

      // Precompute the coefficients
      for (let cx = 0, n = max(lx, hasAlpha ? 5 : 3); cx < n; cx++) {
        fx[cx] = cos(PI / w * (x + 0.5) * cx);
      }
      for (let cy = 0, n = max(ly, hasAlpha ? 5 : 3); cy < n; cy++) {
        fy[cy] = cos(PI / h * (y + 0.5) * cy);
      }

      // Decode L
      for (let cy = 0, j = 0; cy < ly; cy++) {
        for (
          let cx = cy ? 0 : 1, fy2 = fy[cy] * 2;
          cx * ly < lx * (ly - cy);
          cx++, j++
        ) {
          l += l_ac[j] * fx[cx] * fy2;
        }
      }

      // Decode P and Q
      for (let cy = 0, j = 0; cy < 3; cy++) {
        for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 3 - cy; cx++, j++) {
          const f = fx[cx] * fy2;
          p += p_ac[j] * f;
          q += q_ac[j] * f;
        }
      }

      // Decode A
      if (hasAlpha) {
        for (let cy = 0, j = 0; cy < 5; cy++) {
          for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 5 - cy; cx++, j++) {
            a += a_ac[j] * fx[cx] * fy2;
          }
        }
      }

      // Convert to RGB
      const b = l - 2 / 3 * p;
      const r = (3 * l - b + q) / 2;
      const g = r - q;
      rgba[i] = max(0, 255 * min(1, r));
      rgba[i + 1] = max(0, 255 * min(1, g));
      rgba[i + 2] = max(0, 255 * min(1, b));
      rgba[i + 3] = max(0, 255 * min(1, a));
    }
  }
  return { w, h, rgba };
}

function rgbaToDataURL(w, h, rgba) {
  const row = w * 4 + 1;
  const idat = 6 + h * (5 + row);
  const bytes = [
    137,
    80,
    78,
    71,
    13,
    10,
    26,
    10,
    0,
    0,
    0,
    13,
    73,
    72,
    68,
    82,
    0,
    0,
    w >> 8,
    w & 255,
    0,
    0,
    h >> 8,
    h & 255,
    8,
    6,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    idat >>> 24,
    (idat >> 16) & 255,
    (idat >> 8) & 255,
    idat & 255,
    73,
    68,
    65,
    84,
    120,
    1,
  ];
  const table = [
    0,
    498536548,
    997073096,
    651767980,
    1994146192,
    1802195444,
    1303535960,
    1342533948,
    -306674912,
    -267414716,
    -690576408,
    -882789492,
    -1687895376,
    -2032938284,
    -1609899400,
    -1111625188,
  ];
  let a = 1, b = 0;
  for (let y = 0, i = 0, end = row - 1; y < h; y++, end += row - 1) {
    bytes.push(
      y + 1 < h ? 0 : 1,
      row & 255,
      row >> 8,
      ~row & 255,
      (row >> 8) ^ 255,
      0,
    );
    for (b = (b + a) % 65521; i < end; i++) {
      let u = rgba[i] & 255;
      bytes.push(u);
      a = (a + u) % 65521;
      b = (b + a) % 65521;
    }
  }
  bytes.push(
    b >> 8,
    b & 255,
    a >> 8,
    a & 255,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    73,
    69,
    78,
    68,
    174,
    66,
    96,
    130,
  );
  for (let [start, end] of [[12, 29], [37, 41 + idat]]) {
    let c = ~0;
    for (let i = start; i < end; i++) {
      c ^= bytes[i];
      c = (c >>> 4) ^ table[c & 15];
      c = (c >>> 4) ^ table[c & 15];
    }
    c = ~c;
    bytes[end++] = c >>> 24;
    bytes[end++] = (c >> 16) & 255;
    bytes[end++] = (c >> 8) & 255;
    bytes[end++] = c & 255;
  }
  return "data:image/png;base64," + btoa(String.fromCharCode(...bytes));
}

document.querySelectorAll("[data-thumb]").forEach((entry) => {
  const hash = entry.getAttribute("data-thumb");

  if (!hash) return;

  const decodedString = atob(hash);

  // Create Uint8Array from decoded string
  const buffer = new Uint8Array(decodedString.length);
  for (let i = 0; i < decodedString.length; i++) {
    buffer[i] = decodedString.charCodeAt(i);
  }

  const image = thumbHashToRGBA(buffer);
  const dataURL = rgbaToDataURL(image.w, image.h, image.rgba);

  entry.style.background = `url(${dataURL})`;
  entry.style.backgroundSize = "cover";

  const child = entry.querySelector("img[data-thumb-img]");
  setTimeout(() => {
    const isLoaded = child && child.complete && child.naturalHeight !== 0;
    if (child && !isLoaded) {
      child.style.opacity = 0;
      child.style.filter = "blur(5px)";
      child.addEventListener("load", () => {
        child.style.transition = "opacity 0.3s ease, filter 0.6s ease";
        child.style.opacity = 1;
        child.style.filter = "blur(0px)";
        setTimeout(() => {
          entry.style.background = "";
        }, 400);
      });
    }
  }, 50);
});
