import { ref } from 'vue';
import extract from 'png-chunks-extract';
import encode from 'png-chunks-encode';
import text from 'png-chunk-text';

/**
 * MIT License - Copyright (c) 2021 Kaiido
 *
 * A monkey-patch for Safari's drawImage.
 *
 * This browser doesn't handle well using the cropping abilities of drawImage
 * with out-of-bounds values.
 * (see https://stackoverflow.com/questions/35500999/cropping-with-drawimage-not-working-in-safari)
 * This script takes care of detecting when the monkey-patch is needed,
 * and does redefine the cropping parameters so they fall inside the source's boundaries.
 *
 **/

var patchSafari = () => {
  if (typeof window == 'undefined' || typeof window.CanvasRenderingContext2D == 'undefined') return
  if (!needPoly()) return

  const proto = CanvasRenderingContext2D.prototype;
  const original = proto.drawImage;
  if (!original) {
    console.error('This script requires a basic implementation of drawImage');
    return
  }

  proto.drawImage = function drawImage(source, x, y) {
    // length: 3

    const will_crop = arguments.length === 9;
    if (!will_crop) {
      return original.apply(this, [...arguments])
    }

    const safe_rect = getSafeRect(...arguments);
    if (isEmptyRect(safe_rect)) {
      return
    }
    return original.apply(this, safe_rect)
  };

  function needPoly() {
    const ctx = document.createElement('canvas').getContext('2d');
    ctx.fillRect(0, 0, 40, 40);
    ctx.drawImage(ctx.canvas, -40, -40, 80, 80, 50, 50, 20, 20);

    const img = ctx.getImageData(50, 50, 30, 30); // 10px around expected square
    const data = new Uint32Array(img.data.buffer);
    const colorAt = (x, y) => data[y * img.width + x];

    const transparents = [
      [9, 9],
      [20, 9],
      [9, 20],
      [20, 20],
    ];
    const blacks = [
      [10, 10],
      [19, 10],
      [10, 19],
      [19, 19],
    ];
    return (
      transparents.some(([x, y]) => colorAt(x, y) !== 0x00000000) ||
      blacks.some(([x, y]) => colorAt(x, y) === 0x00000000)
    )
  }

  function getSafeRect(image, sx, sy, sw, sh, dx, dy, dw, dh) {
    const { width, height } = getSourceDimensions(image);

    if (sw < 0) {
      sx += sw;
      sw = Math.abs(sw);
    }
    if (sh < 0) {
      sy += sh;
      sh = Math.abs(sh);
    }
    if (dw < 0) {
      dx += dw;
      dw = Math.abs(dw);
    }
    if (dh < 0) {
      dy += dh;
      dh = Math.abs(dh);
    }
    const x1 = Math.max(sx, 0);
    const x2 = Math.min(sx + sw, width);
    const y1 = Math.max(sy, 0);
    const y2 = Math.min(sy + sh, height);
    const w_ratio = dw / sw;
    const h_ratio = dh / sh;

    return [
      image,
      x1,
      y1,
      x2 - x1,
      y2 - y1,
      sx < 0 ? dx - sx * w_ratio : dx,
      sy < 0 ? dy - sy * h_ratio : dy,
      (x2 - x1) * w_ratio,
      (y2 - y1) * h_ratio,
    ]
  }

  function isEmptyRect(args) {
    // sw, sh, dw, dh
    return [3, 4, 7, 8].some((index) => !args[index])
  }

  function getSourceDimensions(source) {
    const sourceIs = (type) => {
      const constructor = globalThis[type];
      return constructor && source instanceof constructor
    };
    if (sourceIs('HTMLImageElement')) {
      return { width: source.naturalWidth, height: source.naturalHeight }
    } else if (sourceIs('HTMLVideoElement')) {
      return { width: source.videoWidth, height: source.videoHeight }
    } else if (sourceIs('SVGImageElement')) {
      throw new TypeError(
        "SVGImageElement isn't yet supported as source image.",
        'UnsupportedError',
      )
    } else if (sourceIs('HTMLCanvasElement') || sourceIs('ImageBitmap')) {
      return source
    }
  }
};

// https://datatracker.ietf.org/doc/html/rfc4648#section-5
const symbols =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

function fromB64(x) { return x.split("").reduce((s, v) => s * 64 + symbols.indexOf(v), 0) }

function decodeUrlSafeBase64(st) {
  const symbolArray = symbols.split("");
  let arr = [];
  let i = 0;
  for (let letter of st) {
    arr[i++] = symbolArray.indexOf(letter) / 64;
  }
  return arr;
}

function validatePub(pub) {
    return pub && typeof pub === 'string' && pub.length === 87 && pub.split('.').length === 2;
}

function parsePub(pub) {
    const split = pub.split(".");
    const decoded = split.map(single => decodeUrlSafeBase64(single));
    const finals = decoded.map(d => d[42]);
    const averages = decoded.map(e => e.reduce((acc, d) => acc + d) / e.length);
    const angles = split.map(part => fromB64(part) % 360);
    const colors = split.map((s, i) => `hsl(${angles[i]} ${finals[i] * 100}% ${averages[i] * 100}%)`);
    return { finals, decoded, angles, averages, colors }
}

function chunkIt(list, chunkSize = 3) {
    return [...Array(Math.ceil(list.length / chunkSize))].map(() =>
        list.splice(0, chunkSize)
    );
}

const error = ref(null);

function canvasToBuffer(canvas) {
  try {
    const base64 = canvas.toDataURL('image/png').split(',')[1];
    const bytes = atob(base64);
    const buffer = new Uint8Array(bytes.length);
    for (let i = 0; i < bytes.length; i++) {
      buffer[i] = bytes.charCodeAt(i);
    }
    return buffer
  } catch (e) {
    error.value = 'Failed to convert canvas to buffer: ' + e.message;
    return null
  }
}

function embedInSvg(svgString, data) {
  try {
    const metadata = `<metadata>
      <gun-data>${JSON.stringify(data)}</gun-data>
    </metadata>`;
    return svgString.replace('</svg>', `${metadata}</svg>`)
  } catch (e) {
    error.value = 'Failed to embed data in SVG: ' + e.message;
    return null
  }
}

function extractFromSvg(svgString) {
  console.log(svgString);
  try {
    const metadataMatch = svgString.match(/<metadata>\s*<gun-data>(.*?)<\/gun-data>\s*<\/metadata>/s);
    if (!metadataMatch || !metadataMatch[1]) {
      error.value = 'No embedded data found in SVG';
      return null
    }

    return JSON.parse(metadataMatch[1])
  } catch (e) {
    error.value = 'Failed to extract data from SVG: ' + e.message;
    return null
  }
}

function embedInPNG(canvas, data) {
  try {
    const buffer = canvasToBuffer(canvas);
    if (!buffer) return null
    const chunks = extract(buffer);
    chunks.splice(-1, 0, text.encode('message', JSON.stringify(data)));
    return encode(chunks)
  } catch (e) {
    error.value = 'Failed to embed data: ' + e.message;
    return null
  }
}




function extractFromBuffer(buffer, type = 'image/png') {
  if (type === 'image/svg+xml') {
    const text = new TextDecoder().decode(buffer);
    return extractFromSvg(text)
  }
  // Default PNG handling
  try {
    const chunks = extract(buffer);
    const textChunks = chunks
      .filter(chunk => chunk.name === 'tEXt')
      .map(chunk => text.decode(chunk.data));

    const messageChunk = textChunks.find(chunk => chunk.keyword === 'message');
    return messageChunk ? JSON.parse(messageChunk.text) : null
  } catch (e) {
    error.value = e.message;
    return null
  }
}

async function extractFromFile(file) {
  try {
    if (!(file instanceof File)) {
      throw new Error('Input must be a File object')
    }
    const arrayBuffer = await file.arrayBuffer();
    const buffer = new Uint8Array(arrayBuffer);
    return await extractFromBuffer(buffer, file.type)
  } catch (e) {
    error.value = e.message;
    return null
  }
}

function renderCanvasAvatar({ pub, size, dark, draw, reflect, round, embed, p3 }) {


  const canvas = document.createElement("canvas");
  canvas.width = canvas.height = size;
  const ctx = canvas.getContext("2d");

  const { decoded, finals } = parsePub(pub);

  drawGradient({ ctx, top: finals[0], bottom: finals[1], size, dark });

  if (draw == "squares") {
    ctx.filter = "blur(20px)";
    drawSquares(decoded[0], ctx, size, p3);
    ctx.filter = "blur(0px)";
    ctx.globalCompositeOperation = "color-burn";
    drawSquares(decoded[1], ctx, size, p3);
  } else {
    drawCircles(decoded[0], ctx, size, 0.42 * size, p3);
    ctx.globalCompositeOperation = "multiply";
    drawCircles(decoded[1], ctx, size, 0.125 * size, p3);
  }

  if (reflect) {
    ctx.globalCompositeOperation = "source-over";
    ctx.scale(-1, 1);
    ctx.translate(-size / 2, 0);
    ctx.drawImage(canvas, size / 2, 0, size, size, 0, 0, size, size);
    // Reset transformation matrix after reflection
    ctx.setTransform(1, 0, 0, 1, 0, 0);
  }

  if (round) {
    // Store the current canvas content
    const imageData = ctx.getImageData(0, 0, size, size);
    ctx.clearRect(0, 0, size, size);

    // Fill with background color first
    ctx.fillStyle = dark ? '#cccccc' : '#ffffff';
    ctx.fillRect(0, 0, size, size);

    // Draw original image back
    ctx.putImageData(imageData, 0, 0);

    // Create circular mask
    ctx.globalCompositeOperation = 'destination-in';
    ctx.beginPath();
    ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
    ctx.closePath();
    ctx.fill();
  }

  let image = canvas.toDataURL("image/png");

  if (embed) {
    const embedData = {
      pub,
      content: embed
    };
    const embedBuffer = embedInPNG(canvas, embedData);
    if (embedBuffer) {
      const blob = new Blob([embedBuffer], { type: 'image/png' });
      image = URL.createObjectURL(blob);
    }
  }

  return image;
}


function drawGradient({ ctx, top = 0, bottom = 150, size = 200, dark = false }) {
  const gradient = ctx.createLinearGradient(0, 0, 0, size);
  const offset = dark ? 0 : 70;
  gradient.addColorStop(0, `hsl(0,0%,${offset + top * 30}%)`);
  gradient.addColorStop(1, `hsl(0,0%,${offset + bottom * 30}%)`);
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, size, size);
}

function drawSquares(data, ctx, size, p3) {
  chunkIt(data, 14).forEach(chunk => {
    if (chunk.length === 14) {
      let [x, y, rRaw, h1, s1, l1, a1, x1, h2, s2, l2, a2, x2, angle] = chunk;
      let r = size / 8 + rRaw * size * (7 / 8);
      const gradient = ctx.createLinearGradient(
        x * size + r * x1, 0,
        x * size + r * x2, size
      );
      gradient.addColorStop(0, p3 ? `color(display-p3 ${h1} ${s1} ${l1} / ${a1})` : `rgba(${h1 * 255}, ${s1 * 255}, ${l1 * 255}, ${a1})`);
      gradient.addColorStop(1, p3 ? `color(display-p3 ${h2} ${s2} ${l2} / ${a2})` : `rgba(${h2 * 255}, ${s2 * 255}, ${l2 * 255}, ${a2})`);
      ctx.fillStyle = gradient;
      ctx.translate(x * size, y * size);
      ctx.rotate(angle * Math.PI);
      ctx.fillRect(-r / 2, -r / 2, r, r);
      ctx.setTransform(1, 0, 0, 1, 0, 0);
    }
  });
}

function drawCircles(data, ctx, size, radius, p3) {
  chunkIt(data, 7).forEach(chunk => {
    if (chunk.length === 7) {
      let [x, y, r, h, s, l, a] = chunk;
      ctx.beginPath();
      ctx.arc(
        size / 2 + (x * size) / 2,
        y * size,
        r * radius,
        0,
        2 * Math.PI
      );
      ctx.fillStyle = p3 ? `color(display-p3 ${h} ${s} ${l} / ${a})` : `rgba(${h * 255}, ${s * 255}, ${l * 255}, ${a})`;
      ctx.closePath();
      ctx.fill();
    }
  });
}

function interactiveScriptGen({ size = 300, reflect = true, follow = true, finals = [0.5, 0.5], averages = [0.5, 0.5], breathVert = 0.03 } = {}) {
  return `
    <script type="text/javascript"><![CDATA[
      // Compact state & precomputed constants
      const s = {
        e: [], // elements
        p: { x: null, y: null, tx: null, ty: null, a: false, lt: 0, lit: 0 }, // pointer state
        r: null, // rect
        a: false, // pulse active
        pt: 0, // pulse time
        pv: 0, // pulse value
        t: 0 // time
      },
      c = {
        sz: ${size},
        cx: ${size / 2},
        cy: ${size / 2},
        pz: ${size} * 1.2,
        bz: ${size} * 0.4 * ${finals[1]},
        pr: ${size} * (0.5 + ${finals[0]}/4),
        mv: (0.1 + 0.05*${averages[0]}) * Math.max(0.8, Math.min(4.4, 200/${size})),
        bs: 0.1 * Math.max(0.8, Math.min(1.4, 200/${size})),
        bv: ${breathVert} * Math.max(0.8, Math.min(1.4, 200/${size})),
        bd: 4500 + ${averages[0]}*1000,
        pa: 0.6 * Math.max(0.8, Math.min(1.4, 200/${size})),
        pr: 200,
        pf: 700,
        rf: ${follow},
        d: 900,
        td: 1500000,
        pp: 0.5+ 0.5*${averages[1]}
      };

      // Init using IIFE
      (() => {
        const svg = document.currentScript?.closest('svg');
        if (!svg) return;
        
        // Update rect and get element collections
        const ur = () => s.r = svg.getBoundingClientRect();
        ur();
        
        // Cache elements for better performance
        s.e = [
          ...Array.from(svg.querySelectorAll('.interactive-circle')).map(el => {
            const x = +el.getAttribute('data-cx');
            const y = +el.getAttribute('data-cy');
            const r = +el.getAttribute('r');
            const o = +el.getAttribute('data-opacity');
            const m = Math.pow(r / (c.sz * 0.05), 0.7) * o;
            const mr = el.nextElementSibling && 
                      el.nextElementSibling.getAttribute('cx') === (c.sz - x).toString() ?
                      el.nextElementSibling : null;
            
            return {
              el, t: 'c',
              x, y, r,
              cx: x, cy: y,
              m, mr,
              o: Math.random() * Math.PI * 2
            };
          }),
          ...Array.from(svg.querySelectorAll('.interactive-square')).map(el => {
            const x = +el.getAttribute('data-cx');
            const y = +el.getAttribute('data-cy');
            const r = +el.getAttribute('data-r');
            const o = +el.getAttribute('data-opacity');
            const a = +el.getAttribute('data-angle') + (Math.random() * 20 - 10);
            
            return {
              el, t: 's',
              x, y, z: c.bz, r: a,
              cx: x, cy: y, cz: c.bz, cr: a, cs: 1,
              m: Math.pow((r / c.sz), 1.5) * o,
              o: Math.random() * Math.PI * 2
            };
          })
        ];
        
        // Initial transform for squares
        s.e.filter(el => el.t === 's').forEach(setTransform);
        
        // Helper functions for event handling
        const gc = e => ({ x: (e.touches?.[0] || e.changedTouches?.[0] || e).clientX, 
                           y: (e.touches?.[0] || e.changedTouches?.[0] || e).clientY });
        
        const inBounds = (x, y) => {
          const { left, top, width, height } = s.r || {};
          return x >= left && x <= left + width && y >= top && y <= top + height;
        };
        
        // Event handlers
        const up = e => {
          if (!s.r) ur();
          const { left, top, width, height } = s.r;
          const { x, y } = gc(e);
          
          if (!inBounds(x, y) && !e.type.startsWith('touch')) {
            if (s.p.a) pl(false);
            return;
          }
          
          // Calculate relative position
          const tx = ((x - left) / width) * c.sz;
          const ty = ((y - top) / height) * c.sz;
          
          // Init/update target position
          if (s.p.tx === null) {
            s.p.tx = tx;
            s.p.ty = ty;
          } else {
            s.p.tx = tx;
            s.p.ty = ty;
            
            // Handle reappearance after inactivity
            if (!s.p.a && performance.now() - s.p.lt > 100) {
              s.p.x = tx;
              s.p.y = ty;
            }
          }
          
          // Initialize actual position if first interaction
          if (s.p.x === null) {
            s.p.x = s.p.tx;
            s.p.y = s.p.ty;
          }
          
          s.p.a = true;
          s.p.lt = performance.now();
        };
        
        const pd = e => {
          up(e);
          s.a = true;
          s.pt = performance.now();
          if (e.type === 'touchstart') e.preventDefault();
        };
        
        const pu = e => {
          s.a = false;
          s.pt = performance.now();
          
          if (e.type === 'touchend' || e.type === 'touchcancel') {
            pl(true);
          }
        };
        
        const pl = (immediate = false) => {
          s.p.a = false;
          s.p.lit = performance.now();
          s.a = false;
          s.pt = s.p.lit;
          
          if (immediate) {
            s.p.tx = s.p.x = null;
            s.p.ty = s.p.y = null;
          }
        };
        
        // Event listeners
        window.addEventListener('resize', ur, { passive: true });
        svg.addEventListener('pointermove', up, { passive: true });
        svg.addEventListener('pointerenter', up, { passive: true });
        svg.addEventListener('pointerleave', pl, { passive: true });
        svg.addEventListener('pointerdown', pd, { passive: false });
        window.addEventListener('pointerup', pu, { passive: true });
        window.addEventListener('pointercancel', pu, { passive: true });
        
        // Start animation
        requestAnimationFrame(animate);
        
        // Setup auto-cleanup
        if (document.currentScript?.parentNode) {
          const cleanup = () => {
            window.removeEventListener('resize', ur);
            svg.removeEventListener('pointermove', up);
            svg.removeEventListener('pointerenter', up);
            svg.removeEventListener('pointerleave', pl);
            svg.removeEventListener('pointerdown', pd);
            window.removeEventListener('pointerup', pu);
            window.removeEventListener('pointercancel', pu);
            s.e = [];
          };
          
          new MutationObserver(mutations => {
            mutations.forEach(m => {
              if (m.removedNodes) {
                m.removedNodes.forEach(n => {
                  if (n === document.currentScript) {
                    cleanup();
                    observer.disconnect();
                  }
                });
              }
            });
          }).observe(document.currentScript.parentNode, { childList: true });
        }
      })();
      
      // Apply 3D transform to square elements
      function setTransform(el) {
        const ps = c.pz / (c.pz + el.cz);
        const px = c.cx + (el.cx - c.cx) * ps;
        const py = c.cy + (el.cy - c.cy) * ps;
        el.el.setAttribute('transform', 
          'translate('+ px+' '+ py+') rotate('+ el.cr+ ') scale(' + ps * el.cs + ')'
        );
      }
      
      // Calculate effects based on time and pulse
      function fx(el, t, pv) {
        const θ = (t % c.bd) / c.bd * Math.PI * 2 + el.o;
        const w = Math.sin(θ);
        const mf = 1 - Math.min(0.8, el.m);
        return {
          s: (1 + w * c.bs * mf) * (1 + pv * c.pa * Math.pow(mf, 2)),
          y: w * c.sz * c.bv * mf,
          p: pv * c.pa * Math.pow(mf, 2)
        };
      }
      
      // Animation loop
      function animate(time) {
        const dt = s.t ? time - s.t : 0;
        s.t = time;
        
        // Handle pointer inactivity
        if (!s.p.a && s.p.x !== null && time - s.p.lit > c.td) {
          s.p.x = s.p.y = s.p.tx = s.p.ty = null;
          s.e.forEach(el => { if (el.ractive) el.ractive = false; });
        }
        
        // Smooth pointer movement
        if (s.p.a && s.p.tx !== null && s.p.x !== null) {
          const dx = s.p.tx - s.p.x;
          const dy = s.p.ty - s.p.y;
          const dist = Math.sqrt(dx*dx + dy*dy);
          const factor = dist > c.sz * 0.3 ? 0.2 : c.pp;
          
          s.p.x += dx * factor;
          s.p.y += dy * factor;
        }
        
        // Update pulse
        s.pv = s.a ? 
          s.pv + (1 - s.pv) * Math.min(1, (time - s.pt) / c.pr) : 
          s.pv * Math.max(0, 1 - (time - s.pt) / c.pf);
        
        if (s.pv < 0.001) s.pv = 0;
        
        // Process elements
        const ps = Math.max(0.6, Math.min(1, c.sz / 150));
        const active = s.p.x !== null;
        
        s.e.forEach(el => {
          const sm = Math.min(dt / c.d, 1) * (el.t === 's' ? 0.3 : 0.1);
          const effect = fx(el, time, s.pv);
          
          if (el.t === 'c') { // Circle
            let tx = el.x;
            let ty = el.y + effect.y;
            
            if (active) {
              const dx = s.p.x - el.x;
              const dy = s.p.y - el.y;
              const d = Math.sqrt(dx * dx + dy * dy);
              const mv = c.sz * c.mv * (1 - Math.min(0.8, el.m));
              const m = Math.min(d, mv) * ps;
              
              if (d > 0.1) {
                tx += m * (dx / d);
                ty += m * (dy / d);
              }
            }
            
            // Smooth interpolation
            const cx_diff = tx - el.cx;
            const cy_diff = ty - el.cy;
            const c_dist = Math.sqrt(cx_diff*cx_diff + cy_diff*cy_diff);
            const c_factor = c_dist > 20 ? 0.15 : 0.1;
            
            el.cx += cx_diff * c_factor;
            el.cy += cy_diff * c_factor;
            
            // Apply changes
            el.el.setAttribute('cx', el.cx);
            el.el.setAttribute('cy', el.cy);
            el.el.setAttribute('r', el.r * effect.s);
            
            // Handle reflection
            if (el.mr && ${reflect}) {
              // Initialize reflection properties if needed
              const reflectX = c.sz - el.x;
              if (!el.rcx) {
                el.rcx = reflectX;
                el.rcy = el.y;
                el.rtx = reflectX;
                el.rty = el.y;
                el.ractive = false;
                el.rest = { x: reflectX, y: el.y };
              }
              
              // Update reflection activity state
              if (active && !el.ractive && s.p.lt - s.p.lit > 100) {
                el.ractive = true;
              } else if (!active && el.ractive && s.p.lit - s.p.lt > 50) {
                el.ractive = false;
              }
              
              // Set target position based on active state and mode
              if (!active || !${follow}) {
                el.rtx = c.sz - el.cx;
                el.rty = el.cy;
              } else {
                const rdx = s.p.x - reflectX;
                const rdy = s.p.y - el.y;
                const rd = Math.sqrt(rdx * rdx + rdy * rdy);
                const rmv = c.sz * c.mv * (1 - Math.min(0.8, el.m));
                const rm = Math.min(rd, rmv) * ps;
                
                if (rd > 0.1) {
                  el.rtx = reflectX + rm * (rdx / rd);
                  el.rty = el.y + effect.y + rm * (rdy / rd);
                } else {
                  el.rtx = reflectX;
                  el.rty = el.y + effect.y;
                }
              }
              
              // Return to rest position when inactive
              if (!active && !s.p.x) {
                el.rtx = el.rest.x;
                el.rty = el.rest.y + effect.y;
              }
              
              // Smooth interpolation for reflection
              const rcx_diff = el.rtx - el.rcx;
              const rcy_diff = el.rty - el.rcy;
              const rc_dist = Math.sqrt(rcx_diff*rcx_diff + rcy_diff*rcy_diff);
              const rc_factor = rc_dist > 30 ? 0.05 : (rc_dist > 10 ? 0.07 : 0.09);
              
              el.rcx += rcx_diff * rc_factor;
              el.rcy += rcy_diff * rc_factor;
              
              // Apply reflection position
              el.mr.setAttribute('cx', el.rcx);
              el.mr.setAttribute('cy', el.rcy);
              el.mr.setAttribute('r', el.r * effect.s);
            }
          } else if (el.t === 's') { // Square
            let tx = el.x, ty = el.y + effect.y, tz = c.bz, tr = el.r, ts = 1;
            
            if (active) {
              const dx = s.p.x - el.x;
              const dy = s.p.y - el.y;
              const d = Math.sqrt(dx * dx + dy * dy);
              const mf = 1 - Math.min(0.8, el.m);
              const mv = c.sz * 0.1 * mf;
              const m = Math.min(d, mv) * ps;
              
              if (d > 0.1) {
                tx += m * (dx / d);
                ty += m * (dy / d);
              }
              
              // Apply depth & scale changes
              const n = Math.min(d / c.pr, 1);
              const zf = 1 - n * n;
              tz -= c.bz * zf * mf;
              ts += zf * 0.3 * mf;
              tr += Math.sin(d / 100) * 5 * mf;
            } else {
              // Idle rotation
              tr += Math.sin(time / 1500 + el.o) * 3 * (1 - Math.min(0.8, el.m));
            }
            
            // Apply pulse to Z depth
            tz -= effect.p * c.bz * 0.7 * (1 - Math.min(0.8, el.m));
            ts *= effect.s;
            
            // Smoothing based on distance
            const sx_diff = tx - el.cx;
            const sy_diff = ty - el.cy;
            const s_dist = Math.sqrt(sx_diff*sx_diff + sy_diff*sy_diff);
            const s_factor = Math.min(1, s_dist > 20 ? sm * 1.5 : sm);
            
            // Apply interpolation
            el.cx += sx_diff * s_factor;
            el.cy += sy_diff * s_factor;
            el.cz += (tz - el.cz) * sm;
            el.cr += (tr - el.cr) * sm;
            el.cs += (ts - el.cs) * sm;
            
            setTransform(el);
          }
        });
        
        requestAnimationFrame(animate);
      }
    ]]></script>
  `;
}

function renderSVGAvatar({ pub, size = 200, dark = false, draw = "circles", reflect = true, round = true, embed = true, svg } = {}) {
  const { decoded, finals, averages } = parsePub(pub);

  // Create gradient background
  const bgGradient = `
      <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
        <stop offset="0%" stop-color="hsla(0,0%,${(dark ? 0 : 70) + finals[0] * 30}%)"/>
        <stop offset="100%" stop-color="hsla(0,0%,${(dark ? 0 : 70) + finals[1] * 30}%)"/>
      </linearGradient>
    `;

  // Create squares for both layers (non-interactive)
  const createSquares = (data, isSecond = false) => {
    return chunkIt(data, 14).map(chunk => {
      if (chunk.length !== 14) return '';
      const [x, y, rRaw, h1, s1, l1, a1, x1, h2, s2, l2, a2, x2, angle] = chunk;
      const r = size / 8 + rRaw * size * (7 / 8);
      const gradientId = `gradient-${x}-${y}-${isSecond ? '2' : '1'}`;
      const centerX = x * size;
      const centerY = y * size;

      const squareAttrs = svg === 'interactive'
        ? `class="interactive-square" data-cx="${centerX}" data-cy="${centerY}" data-r="${r}" data-angle="${angle * 180}" data-opacity="${(a1 + a2) / 2}"`
        : '';

      return `
          <defs>
            <linearGradient id="${gradientId}" x1="${x1}" y1="0" x2="${x2}" y2="1">
              <stop offset="0%" stop-color="color(display-p3 ${h1} ${s1} ${l1} / ${a1})"/>
              <stop offset="100%" stop-color="color(display-p3 ${h2} ${s2} ${l2} / ${a2})"/>
            </linearGradient>
          </defs>
          <g ${squareAttrs} transform="translate(${centerX} ${centerY}) rotate(${angle * 180})">
            <rect 
              x="${-r / 2}" y="${-r / 2}" 
              width="${r}" height="${r}"
              fill="url(#${gradientId})"
              style="${isSecond ? 'mix-blend-mode:color-burn;' : 'filter:blur(20px);'}"
            />
          </g>
        `;
    }).join('');
  };


  // Generate circles for both layers with interactive attributes
  const createCircles = (data, radius, isSecond = false) => {
    return chunkIt(data, 7).map(chunk => {
      if (chunk.length !== 7) return '';
      const [x, y, radi, r, g, b, a] = chunk;
      const cx = size / 2 + (x * size) / 2;
      const cy = y * size;
      const rad = radi * radius;

      const circleAttrs = svg === 'interactive'
        ? `class="interactive-circle" data-cx="${cx}" data-cy="${cy}" data-opacity="${a}"`
        : '';

      return `
          <circle 
            ${circleAttrs}
            cx="${cx}" cy="${cy}" r="${rad}"
            fill="color(display-p3 ${r} ${g} ${b} / ${a})"

            style="${isSecond ? 'mix-blend-mode:multiply;' : ''}"
          />
          ${reflect ? `
          <circle 
            cx="${size - cx}" cy="${cy}" r="${rad}"
            fill="color(display-p3 ${r} ${g} ${b} / ${a})"

            style="${isSecond ? 'mix-blend-mode:multiply;' : ''}"
          />` : ''}
        `;
    }).join('');
  };

  const clipPath = round ? `
      <defs>
        <clipPath id="circle-mask">
          <circle cx="${size / 2}" cy="${size / 2}" r="${size / 2}" />
        </clipPath>
      </defs>
    ` : '';

  // Interactive mode script for mouse tracking
  const interactiveScript = svg === 'interactive' ? interactiveScriptGen({ size, reflect, finals, averages }) : '';

  let svg_content = `
      <svg 
        width="${size}" height="${size}" 
        viewBox="0 0 ${size} ${size}" 
        xmlns="http://www.w3.org/2000/svg"
        style="overflow: visible;"
        >
        <defs>${bgGradient}</defs>
        ${clipPath}
        <g ${round ? 'clip-path="url(#circle-mask)"' : ''}>
          <rect x="${-size}" width="${3 * size}" y="${-size}" height="${3 * size}" fill="url(#bg)"/>
          ${draw === "squares" ?
      `${createSquares(decoded[0], false)}
             ${createSquares(decoded[1], true)}` :
      `${createCircles(decoded[0], 0.42 * size)}
             ${createCircles(decoded[1], 0.125 * size, true)}`
    }
        </g>
        ${interactiveScript}
      </svg>
    `;

  if (svg === 'interactive') {
    // For interactive mode, return encoded SVG without base64
    return `data:image/svg+xml,${encodeURIComponent(svg_content.trim())}`;
  }

  if (embed) {
    const embedData = { pub };
    if (embed) { embedData.content = embed; }
    svg_content = embedInSvg(svg_content, embedData);
  }

  const svgBase64 = typeof btoa === 'function'
    ? btoa(svg_content)
    : Buffer.from(svg_content).toString('base64');
  let finalData = `data:image/svg+xml;base64,${svgBase64}`;

  return finalData
}

const cache = {};

function gunAvatar({
  pub, // public key of a user
  size = 200, // square pixel dimensions
  dark = false, // Light mode, enable to have more dim backgrounds
  draw = "circles", // useful for people and agents avatars. Also "squares" - for backgrounds and document covers.
  reflect = true, // used for avatars symmetry. Disable for squares.
  round = true, // Cut the image with round transparency mask. Disable to get raw square image.
  embed = true, // Embed the "pub" key into the image (both PNG and SVG). You can put any serializable content here - an encrypted keypair is a nice example of use for this
  svg = true, // Scalabe vector graphics format. Disable to get Canvas PNG render available only in browsers.
  p3 = true, // Extended P3 color palette utilizes full capacity of modern displays. Disable to have more backwards compatible RGBA color palette.
} = {}) {

  const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';

  if (!validatePub(pub)) return '';

  if (svg || !isBrowser) return renderSVGAvatar({ pub, size, dark, draw, reflect, round, embed, svg});

  const key = JSON.stringify(arguments[0]);
  if (cache?.[key]) return cache[key]

  const image = renderCanvasAvatar({ pub, size, dark, draw, reflect, round, embed, p3 });

  cache[key] = image;
  return image;
}

function mountClass(elClass = "gun-avatar") {
  document.addEventListener("DOMContentLoaded", () => {
    let avatars = document.getElementsByClassName(elClass);
    for (let i in avatars) {
      const img = avatars[i];
      if (img.dataset.round !== "false") {
        img.style.borderRadius = "100%";
      }

      let embed = img.dataset.embed;
      if (img.dataset.embed !== 'true') {
        try {
          embed = JSON.parse(img.dataset.embed);
        } catch (e) {
          console.warn('Invalid content JSON in data-embed attribute');
        }
      }

      img.src = gunAvatar({
        pub: img.dataset.pub,
        size: Number(img.dataset.size),
        dark: Boolean(img.dataset.dark),
        draw: img.dataset.draw,
        reflect: img.dataset.reflect !== "false",
        svg: img.dataset.svg,
        round: Boolean(img.dataset.round),
        embed: embed === "false" ? false : embed || true
      });
    }
  });
}

function mountElement(elName = "gun-avatar") {
  let initiated = false;
  if (initiated) return;

  class Avatar extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open" });
      /** @type {HTMLImageElement} */
      this.img = document.createElement("img");
      this.shadowRoot.append(this.img);
    }

    render() {
      this.pub = this.getAttribute("pub") || "1234123455Ute2tFhdjDQgzR-1234lfSlZxgEZKuquI.2F-j1234434U1234Asj-5lxnECG5TDyuPD8gEiuI123";
      this.size = this.hasAttribute("size") ? Number(this.getAttribute("size")) : 400;
      this.draw = this.getAttribute("draw") || "circles";
      this.reflect = this.hasAttribute("reflect") ? this.getAttribute("reflect") !== "false" : true;
      this.round = this.hasAttribute("round") || this.getAttribute("round") === "";
      this.dark = this.hasAttribute("dark") ? this.getAttribute("dark") != "" : false;
      this.embed = this.hasAttribute("embed") ? this.getAttribute("embed") !== "false" : true;

      this.svg = this.hasAttribute("dark") && this.getAttribute("dark");

      this.p3 = this.hasAttribute("p3") ? this.getAttribute("p3") !== "false" : true;

      let embed = this.getAttribute("embed");
      if (this.getAttribute("embed")) {
        try {
          embed = JSON.parse(this.getAttribute("embed"));
        } catch (e) {
          console.warn('Invalid content JSON in embed attribute');
        }
      }

      this.img.style.borderRadius = this.round ? "100%" : "0%";

      this.img.src = gunAvatar({
        pub: this.pub,
        size: this.size,
        dark: this.dark,
        draw: this.draw,
        reflect: this.reflect,
        round: this.round,
        svg: this.svg,
        embed: embed === "false" ? false : embed || true,
        p3: this.p3
      });
    }

    connectedCallback() {
      this.render();
    }

    static get observedAttributes() {
      return ["pub", "round", "size", "dark", "draw", "reflect", "embed", "p3"];
    }

    attributeChangedCallback() {
      this.render();
    }
  }

  customElements.define(elName, Avatar);
  initiated = true;
}

patchSafari();

export { error, extractFromFile, gunAvatar, mountClass, mountElement, parsePub };
