import { Component } from '@theme/component';
import { debounce } from '@theme/utilities';

const ANIMATION_OPTIONS = { duration: 500 };

class MarqueeComponent extends Component {
  requiredRefs = ['wrapper', 'content', 'marqueeItems'];

  async connectedCallback() {
    super.connectedCallback();

    const { marqueeItems } = this.refs;
    if (!marqueeItems.length) return;

    const { numberOfCopies } = await this.#queryNumberOfCopies();
    const speed = this.#calculateSpeed(numberOfCopies);

    this.#resetRepeatedItems();
    this.#addRepeatedItems(numberOfCopies);
    this.#duplicateContent();
    this.#setSpeed(speed);

    window.addEventListener('resize', this.#handleResize);

    // Hover desktop
    this.addEventListener('pointerenter', this.#slowDown);
    this.addEventListener('pointerleave', this.#speedUp);

    // Drag desktop + mobile
    this.addEventListener('pointerdown', this.#onDragStart);
    this.addEventListener('pointermove', this.#onDragMove);
    this.addEventListener('pointerup', this.#onDragEnd);
    this.addEventListener('pointercancel', this.#onDragEnd);
    this.addEventListener('pointerleave', this.#onDragEnd);
  }

  disconnectedCallback() {
    super.disconnectedCallback();

    window.removeEventListener('resize', this.#handleResize);

    this.removeEventListener('pointerenter', this.#slowDown);
    this.removeEventListener('pointerleave', this.#speedUp);

    this.removeEventListener('pointerdown', this.#onDragStart);
    this.removeEventListener('pointermove', this.#onDragMove);
    this.removeEventListener('pointerup', this.#onDragEnd);
    this.removeEventListener('pointercancel', this.#onDragEnd);
    this.removeEventListener('pointerleave', this.#onDragEnd);
  }

  #animation = null;
  #marqueeWidth = null;

  // ===== DRAG + MOMENTUM =====
  #isDragging = false;
  #startX = 0;
  #startTime = 0;
  #lastX = 0;
  #velocity = 0;
  #momentumRAF = null;

  #onDragStart = (e) => {
    const animation = this.refs.wrapper.getAnimations()[0];
    if (!animation) return;

    e.preventDefault();
    cancelAnimationFrame(this.#momentumRAF);

    this.#isDragging = true;
    this.#startX = this.#lastX = e.clientX;
    this.#startTime = animation.currentTime ?? 0;
    this.#velocity = 0;

    this.setPointerCapture(e.pointerId);
    this.#slowDown.cancel();

    animation.pause();
  };

  #onDragMove = (e) => {
    if (!this.#isDragging) return;

    const animation = this.refs.wrapper.getAnimations()[0];
    if (!animation) return;

    const deltaX = this.#startX - e.clientX;
    const velocity = this.#lastX - e.clientX;
    this.#velocity = velocity;

    const scrubFactor = 5;
    animation.currentTime = this.#startTime + deltaX * scrubFactor;

    this.#lastX = e.clientX;
  };

  #onDragEnd = (e) => {
    if (!this.#isDragging) return;

    const animation = this.refs.wrapper.getAnimations()[0];
    if (!animation) return;

    this.#isDragging = false;

    try { this.releasePointerCapture(e.pointerId); } catch {}

    let momentum = this.#velocity * 20;
    const friction = 0.95;

    const step = () => {
      momentum *= friction;
      animation.currentTime += momentum;

      if (Math.abs(momentum) > 0.1) {
        this.#momentumRAF = requestAnimationFrame(step);
      } else {
        animation.play();
      }
    };

    step();
  };

  #slowDown = debounce(() => {
    if (this.#animation) return;
    const animation = this.refs.wrapper.getAnimations()[0];
    if (!animation) return;

    this.#animation = animateValue({
      ...ANIMATION_OPTIONS,
      from: 1,
      to: 0,
      onUpdate: (v) => animation.updatePlaybackRate(v),
      onComplete: () => (this.#animation = null),
    });
  }, ANIMATION_OPTIONS.duration);

  #speedUp() {
    this.#slowDown.cancel();
    const animation = this.refs.wrapper.getAnimations()[0];
    if (!animation || animation.playbackRate === 1) return;

    const from = this.#animation?.current ?? 0;
    this.#animation?.cancel();

    this.#animation = animateValue({
      ...ANIMATION_OPTIONS,
      from,
      to: 1,
      onUpdate: (v) => animation.updatePlaybackRate(v),
      onComplete: () => (this.#animation = null),
    });
  }

  get clonedContent() {
    const { content, wrapper } = this.refs;
    const lastChild = wrapper.lastElementChild;
    return content !== lastChild ? lastChild : null;
  }

  #setSpeed(v) {
    this.style.setProperty('--marquee-speed', `${v}s`);
  }

  async #queryNumberOfCopies() {
    const { marqueeItems } = this.refs;

    return new Promise((resolve) => {
      if (!marqueeItems[0])
        return resolve({ numberOfCopies: 1, isHorizontalResize: true });

      const observer = new IntersectionObserver((entries) => {
        const entry = entries[0];
        if (!entry) return;

        observer.disconnect();

        const { width: marqueeWidth } = entry.rootBounds ?? { width: 0 };
        const { width: itemsWidth } = entry.boundingClientRect;

        const isHorizontalResize = this.#marqueeWidth !== marqueeWidth;
        this.#marqueeWidth = marqueeWidth;

        resolve({
          numberOfCopies:
            itemsWidth === 0 ? 1 : Math.ceil(marqueeWidth / itemsWidth) + 1,
          isHorizontalResize,
        });
      }, { root: this });

      observer.observe(marqueeItems[0]);
    });
  }

  #calculateSpeed(n) {
    const f = Number(this.getAttribute('data-speed-factor'));
    return Math.sqrt(n) * f;
  }

  #handleResize = debounce(async () => {
    const { numberOfCopies, isHorizontalResize } =
      await this.#queryNumberOfCopies();
    if (!isHorizontalResize) return;

    const speed = this.#calculateSpeed(numberOfCopies);

    this.#resetRepeatedItems();
    this.#addRepeatedItems(numberOfCopies);
    this.#duplicateContent();

    this.#setSpeed(speed);
    this.#restartAnimation();
  }, 250);

  #restartAnimation() {
    const animations = this.refs.wrapper.getAnimations();
    requestAnimationFrame(() => {
      animations.forEach((a) => (a.currentTime = 0));
    });
  }

  #duplicateContent() {
    this.clonedContent?.remove();

    const clone = this.refs.content.cloneNode(true);
    clone.setAttribute('aria-hidden', 'true');
    clone.removeAttribute('ref');

    this.refs.wrapper.appendChild(clone);
  }

  #resetRepeatedItems() {
    const { content } = this.refs;
    const first = content.firstElementChild;
    content.innerHTML = '';
    if (first) content.appendChild(first);
  }

  #addRepeatedItems(n) {
    const { content, marqueeItems } = this.refs;
    if (!marqueeItems[0]) return;

    for (let i = 0; i < n - 1; i++) {
      content.appendChild(marqueeItems[0].cloneNode(true));
    }
  }
}

function animateValue({ from, to, duration, onUpdate, easing = (t)=>t*t*(3-2*t), onComplete }) {
  const start = performance.now();
  let cancelled = false, current = from;

  function frame(now) {
    if (cancelled) return;
    const p = Math.min((now - start) / duration, 1);
    const e = easing(p);
    current = from + (to - from) * e;
    onUpdate(current);
    if (p < 1) requestAnimationFrame(frame);
    else onComplete?.();
  }
  requestAnimationFrame(frame);

  return { get current(){ return current }, cancel(){ cancelled = true } };
}

if (!customElements.get('marquee-component')) {
  customElements.define('marquee-component', MarqueeComponent);
}