const TARGET_SELECTOR = '[data-reveal], .reveal-on-scroll, .animate-fade-in';
const DEFAULT_DISTANCE = '24px';
const DEFAULT_DURATION = '0.65s';
const DEFAULT_DELAY = '0s';
const DEFAULT_EASING = 'cubic-bezier(0.22, 1, 0.36, 1)';
const DEFAULT_BLUR = '0.45rem';
const DEFAULT_SCALE = '1';

const directionToOffsets = (direction: string | undefined, distance: string) => {
  const dir = (direction ?? 'up').toLowerCase();
  switch (dir) {
    case 'down':
      return { x: '0', y: `-${distance}` };
    case 'left':
      return { x: distance, y: '0' };
    case 'right':
      return { x: `-${distance}`, y: '0' };
    case 'fade':
    case 'none':
      return { x: '0', y: '0' };
    case 'up':
    default:
      return { x: '0', y: distance };
  }
};

class ScrollRevealController {
  private observer: IntersectionObserver | null = null;
  private mutationObserver: MutationObserver | null = null;
  private targets = new Set<HTMLElement>();
  private prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
  private reduceMotion = this.prefersReducedMotion.matches;

  constructor() {
    document.documentElement.classList.add('reveal-enabled');
    this.setupObserver();
    this.collectTargets(true);
    this.observeMutations();
    this.handlePageTransitions();
    this.prefersReducedMotion.addEventListener('change', (event) => {
      this.reduceMotion = event.matches;
      this.setupObserver();
      this.collectTargets(true);
      if (this.reduceMotion) {
        this.showAllImmediately();
      }
    });
  }

  private setupObserver(): void {
    if (this.observer) {
      this.observer.disconnect();
    }

    if (this.reduceMotion) {
      this.observer = null;
      return;
    }

    this.observer = new IntersectionObserver(
      (entries) => this.handleIntersections(entries),
      {
        root: null,
        threshold: 0.1,
        rootMargin: '0px 0px -12% 0px'
      }
    );
  }

  private collectTargets(force = false): void {
    const currentTargets = document.querySelectorAll<HTMLElement>(TARGET_SELECTOR);

    currentTargets.forEach((element) => {
      if (!force && element.dataset.revealInitialized === 'true') {
        return;
      }

      this.prepareElement(element);
    });
  }

  private prepareElement(element: HTMLElement): void {
    if (element.dataset.revealInitialized === 'true' && !this.reduceMotion) {
      // Reconnect existing elements to observer when needed
      if (this.observer && !element.dataset.revealObserved) {
        this.observer.observe(element);
        element.dataset.revealObserved = 'true';
      }
      return;
    }

    const dataset = element.dataset;
    const direction = dataset.revealDirection || dataset.reveal;
    const distance = dataset.revealDistance || DEFAULT_DISTANCE;
    const duration = dataset.revealDuration || DEFAULT_DURATION;
    const delay = dataset.revealDelay || DEFAULT_DELAY;
    const easing = dataset.revealEasing || DEFAULT_EASING;
    const blur = dataset.revealBlur || DEFAULT_BLUR;
    const scale = dataset.revealScale || DEFAULT_SCALE;
    const once = dataset.revealOnce !== 'false';

    const offsets = directionToOffsets(direction, distance);

    element.style.setProperty('--reveal-delay', delay);
    element.style.setProperty('--reveal-duration', duration);
    element.style.setProperty('--reveal-easing', easing);
    element.style.setProperty('--reveal-translate-x', offsets.x);
    element.style.setProperty('--reveal-translate-y', offsets.y);
    element.style.setProperty('--reveal-blur', blur);
    element.style.setProperty('--reveal-scale', scale);

    if (!element.classList.contains('reveal-on-scroll')) {
      element.classList.add('reveal-on-scroll');
    }

    element.dataset.revealOnce = once ? 'true' : 'false';
    element.dataset.revealInitialized = 'true';
    this.targets.add(element);

    if (this.reduceMotion) {
      this.showElement(element, true);
      return;
    }

    if (this.observer) {
      this.observer.observe(element);
      element.dataset.revealObserved = 'true';
    }
  }

  private handleIntersections(entries: IntersectionObserverEntry[]): void {
    entries.forEach((entry) => {
      const element = entry.target as HTMLElement;
      const once = element.dataset.revealOnce !== 'false';

      if (entry.isIntersecting || entry.intersectionRatio > 0) {
        this.showElement(element);
        if (once && this.observer) {
          this.observer.unobserve(element);
          element.dataset.revealObserved = '';
        }
      } else if (!once) {
        this.hideElement(element);
        if (this.observer && element.dataset.revealObserved !== 'true') {
          this.observer.observe(element);
          element.dataset.revealObserved = 'true';
        }
      }
    });
  }

  private showElement(element: HTMLElement, immediate = false): void {
    if (element.classList.contains('is-visible')) return;

    if (immediate) {
      element.style.setProperty('--reveal-duration', '0s');
      element.style.setProperty('--reveal-delay', '0s');
    }

    requestAnimationFrame(() => {
      element.classList.add('is-visible');
    });
  }

  private hideElement(element: HTMLElement): void {
    element.classList.remove('is-visible');
  }

  private showAllImmediately(): void {
    this.targets.forEach((element) => this.showElement(element, true));
  }

  private observeMutations(): void {
    if (this.mutationObserver) {
      this.mutationObserver.disconnect();
    }

    this.mutationObserver = new MutationObserver((mutations) => {
      let shouldRecollect = false;
      mutations.forEach((mutation) => {
        if (mutation.addedNodes.length > 0) {
          shouldRecollect = true;
        }
      });

      if (shouldRecollect) {
        this.collectTargets();
      }
    });

    this.mutationObserver.observe(document.body, {
      childList: true,
      subtree: true
    });
  }

  private handlePageTransitions(): void {
    document.addEventListener('astro:page-load', () => {
      this.refresh(true);
    });
  }

  public refresh(force = false): void {
    this.collectTargets(force);
    if (this.reduceMotion) {
      this.showAllImmediately();
    }
  }
}

if (typeof window !== 'undefined') {
  if (!(window as any).__scrollRevealController) {
    (window as any).__scrollRevealController = new ScrollRevealController();
  } else {
    const controller = (window as any).__scrollRevealController as ScrollRevealController;
    controller.refresh(true);
  }
}
