const instances = new WeakMap();

function readNumber(value, fallback) {
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : fallback;
}

function random(x) {
  return (Math.sin(x * 12.9898) * 43758.5453) % 1;
}

function noise2D(x, y) {
  const i = Math.floor(x);
  const j = Math.floor(y);
  const fx = x - i;
  const fy = y - j;

  const a = random(i + j * 57);
  const b = random(i + 1 + j * 57);
  const c = random(i + (j + 1) * 57);
  const d = random(i + 1 + (j + 1) * 57);

  const ux = fx * fx * (3 - 2 * fx);
  const uy = fy * fy * (3 - 2 * fy);

  return a * (1 - ux) * (1 - uy) + b * ux * (1 - uy) + c * (1 - ux) * uy + d * ux * uy;
}

function octavedNoise(x, octaves, lacunarity, gain, baseAmplitude, baseFrequency, time, seed, baseFlatness) {
  let y = 0;
  let amplitude = baseAmplitude;
  let frequency = baseFrequency;

  for (let i = 0; i < octaves; i++) {
    let octaveAmplitude = amplitude;
    if (i === 0) octaveAmplitude *= baseFlatness;
    y += octaveAmplitude * noise2D(frequency * x + seed * 100, time * frequency * 0.3);
    frequency *= lacunarity;
    amplitude *= gain;
  }

  return y;
}

function getCornerPoint(centerX, centerY, radius, startAngle, arcLength, progress) {
  const angle = startAngle + progress * arcLength;
  return {
    x: centerX + radius * Math.cos(angle),
    y: centerY + radius * Math.sin(angle)
  };
}

function getRoundedRectPoint(t, left, top, width, height, radius) {
  const straightWidth = Math.max(width - 2 * radius, 0);
  const straightHeight = Math.max(height - 2 * radius, 0);
  const cornerArc = (Math.PI * radius) / 2;
  const totalPerimeter = 2 * straightWidth + 2 * straightHeight + 4 * cornerArc;
  const distance = t * totalPerimeter;

  let accumulated = 0;

  if (distance <= accumulated + straightWidth) {
    const progress = straightWidth === 0 ? 0 : (distance - accumulated) / straightWidth;
    return { x: left + radius + progress * straightWidth, y: top };
  }
  accumulated += straightWidth;

  if (distance <= accumulated + cornerArc) {
    const progress = cornerArc === 0 ? 0 : (distance - accumulated) / cornerArc;
    return getCornerPoint(left + width - radius, top + radius, radius, -Math.PI / 2, Math.PI / 2, progress);
  }
  accumulated += cornerArc;

  if (distance <= accumulated + straightHeight) {
    const progress = straightHeight === 0 ? 0 : (distance - accumulated) / straightHeight;
    return { x: left + width, y: top + radius + progress * straightHeight };
  }
  accumulated += straightHeight;

  if (distance <= accumulated + cornerArc) {
    const progress = cornerArc === 0 ? 0 : (distance - accumulated) / cornerArc;
    return getCornerPoint(left + width - radius, top + height - radius, radius, 0, Math.PI / 2, progress);
  }
  accumulated += cornerArc;

  if (distance <= accumulated + straightWidth) {
    const progress = straightWidth === 0 ? 0 : (distance - accumulated) / straightWidth;
    return { x: left + width - radius - progress * straightWidth, y: top + height };
  }
  accumulated += straightWidth;

  if (distance <= accumulated + cornerArc) {
    const progress = cornerArc === 0 ? 0 : (distance - accumulated) / cornerArc;
    return getCornerPoint(left + radius, top + height - radius, radius, Math.PI / 2, Math.PI / 2, progress);
  }
  accumulated += cornerArc;

  if (distance <= accumulated + straightHeight) {
    const progress = straightHeight === 0 ? 0 : (distance - accumulated) / straightHeight;
    return { x: left, y: top + height - radius - progress * straightHeight };
  }
  accumulated += straightHeight;

  const progress = cornerArc === 0 ? 0 : (distance - accumulated) / cornerArc;
  return getCornerPoint(left + radius, top + radius, radius, Math.PI, Math.PI / 2, progress);
}

function createElectricBorder(card, canvas) {
  const ctx = canvas.getContext('2d');
  if (!ctx) return null;

  const color = card.dataset.electricColor || '#7df9ff';
  const speed = readNumber(card.dataset.electricSpeed, 1);
  const chaos = readNumber(card.dataset.electricChaos, 0.12);
  const thickness = readNumber(card.dataset.electricThickness, 2);
  const borderRadius = readNumber(card.dataset.electricRadius, 16);
  const borderOffset = 28;
  const displacement = 28;
  const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
  const config = {
    octaves: 10,
    lacunarity: 1.6,
    gain: 0.7,
    amplitude: chaos,
    frequency: 10,
    baseFlatness: 0
  };

  let width = 0;
  let height = 0;
  let dpr = 1;
  let time = 0;
  let lastFrameTime = 0;
  let frameId = 0;
  let stopped = false;

  card.style.setProperty('--electric-border-color', color);
  canvas.dataset.electricBorderReady = 'false';

  function updateSize() {
    const rect = card.getBoundingClientRect();
    width = Math.max(rect.width + borderOffset * 2, 1);
    height = Math.max(rect.height + borderOffset * 2, 1);
    dpr = Math.min(window.devicePixelRatio || 1, 2);

    canvas.width = Math.round(width * dpr);
    canvas.height = Math.round(height * dpr);
    canvas.style.width = `${width}px`;
    canvas.style.height = `${height}px`;
  }

  const TARGET_FPS = 30;
  const FRAME_INTERVAL = 1000 / TARGET_FPS;
  let lastThrottleTime = 0;

  function draw(currentTime = 0) {
    if (stopped || !card.isConnected) {
      stop();
      return;
    }

    // 页面不可见时暂停动画
    if (document.hidden) {
      lastFrameTime = 0;
      lastThrottleTime = 0;
      frameId = window.requestAnimationFrame(draw);
      return;
    }

    // 帧率节流：限制为 ~30fps
    if (currentTime - lastThrottleTime < FRAME_INTERVAL) {
      frameId = window.requestAnimationFrame(draw);
      return;
    }
    lastThrottleTime = currentTime;

    const nextDpr = Math.min(window.devicePixelRatio || 1, 2);
    if (nextDpr !== dpr) updateSize();

    const deltaTime = lastFrameTime ? (currentTime - lastFrameTime) / 1000 : 0;
    time += deltaTime * speed;
    lastFrameTime = currentTime;

    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.scale(dpr, dpr);

    ctx.strokeStyle = color;
    ctx.lineWidth = thickness;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.shadowColor = color;
    ctx.shadowBlur = 16;

    const left = borderOffset;
    const top = borderOffset;
    const borderWidth = width - 2 * borderOffset;
    const borderHeight = height - 2 * borderOffset;
    const maxRadius = Math.min(borderWidth, borderHeight) / 2;
    const radius = Math.min(borderRadius, maxRadius);
    const approximatePerimeter = 2 * (borderWidth + borderHeight) + 2 * Math.PI * radius;
    const sampleCount = Math.max(80, Math.min(260, Math.floor(approximatePerimeter / 3)));

    ctx.beginPath();
    for (let i = 0; i <= sampleCount; i++) {
      const progress = i / sampleCount;
      const point = getRoundedRectPoint(progress, left, top, borderWidth, borderHeight, radius);
      const xNoise = octavedNoise(progress * 8, config.octaves, config.lacunarity, config.gain, config.amplitude, config.frequency, time, 0, config.baseFlatness);
      const yNoise = octavedNoise(progress * 8, config.octaves, config.lacunarity, config.gain, config.amplitude, config.frequency, time, 1, config.baseFlatness);
      const displacedX = point.x + xNoise * displacement;
      const displacedY = point.y + yNoise * displacement;

      if (i === 0) {
        ctx.moveTo(displacedX, displacedY);
      } else {
        ctx.lineTo(displacedX, displacedY);
      }
    }

    ctx.closePath();
    ctx.save();
    ctx.globalAlpha = 0.32;
    ctx.lineWidth = thickness + 5;
    ctx.shadowBlur = 22;
    ctx.stroke();
    ctx.restore();
    ctx.stroke();
    canvas.dataset.electricBorderReady = 'true';

    if (!reducedMotion) {
      frameId = window.requestAnimationFrame(draw);
    }
  }

  function stop() {
    stopped = true;
    if (frameId) window.cancelAnimationFrame(frameId);
    resizeObserver?.disconnect();
    instances.delete(card);
    delete card.dataset.electricBorderActive;
  }

  updateSize();
  const resizeObserver = window.ResizeObserver
    ? new ResizeObserver(() => {
        updateSize();
        if (reducedMotion) draw(lastFrameTime);
      })
    : null;

  resizeObserver?.observe(card);
  frameId = window.requestAnimationFrame(draw);

  return { stop };
}

export function cleanupElectricBorders(root = document) {
  root.querySelectorAll?.('.electric-border').forEach((card) => {
    const instance = instances.get(card);
    if (instance) instance.stop();
  });
}

export function initElectricBorders(root = document) {
  root.querySelectorAll?.('.electric-border').forEach((card) => {
    if (instances.has(card)) return;
    const canvas = card.querySelector('.eb-canvas');
    if (!canvas) return;
    const instance = createElectricBorder(card, canvas);
    if (instance) {
      instances.set(card, instance);
      card.dataset.electricBorderActive = 'true';
    }
  });
}
