/**
 * EdgeScrollManager - Auto-scroll when dragging near viewport edges
 *
 * 2-zone system:
 * - Inner zone (0-50px): Fast scroll (640 px/sec)
 * - Outer zone (50-100px): Slow scroll (140 px/sec)
 */

import { IEventBus } from '../types/CalendarTypes';
import { CoreEvents } from '../constants/CoreEvents';

export class EdgeScrollManager {
  private scrollableContent: HTMLElement | null = null;
  private timeGrid: HTMLElement | null = null;
  private draggedElement: HTMLElement | null = null;
  private scrollRAF: number | null = null;
  private mouseY = 0;
  private isDragging = false;
  private isScrolling = false;
  private lastTs = 0;
  private rect: DOMRect | null = null;
  private initialScrollTop = 0;

  private readonly OUTER_ZONE = 100;
  private readonly INNER_ZONE = 50;
  private readonly SLOW_SPEED = 140;
  private readonly FAST_SPEED = 640;

  constructor(private eventBus: IEventBus) {
    this.subscribeToEvents();
    document.addEventListener('pointermove', this.trackMouse);
  }

  init(scrollableContent: HTMLElement): void {
    this.scrollableContent = scrollableContent;
    this.timeGrid = scrollableContent.querySelector('swp-time-grid');
    this.scrollableContent.style.scrollBehavior = 'auto';
  }

  private trackMouse = (e: PointerEvent): void => {
    if (this.isDragging) {
      this.mouseY = e.clientY;
    }
  };

  private subscribeToEvents(): void {
    this.eventBus.on(CoreEvents.EVENT_DRAG_START, (event: Event) => {
      const payload = (event as CustomEvent).detail;
      this.draggedElement = payload.element;
      this.startDrag();
    });

    this.eventBus.on(CoreEvents.EVENT_DRAG_END, () => this.stopDrag());
    this.eventBus.on(CoreEvents.EVENT_DRAG_CANCEL, () => this.stopDrag());
  }

  private startDrag(): void {
    this.isDragging = true;
    this.isScrolling = false;
    this.lastTs = 0;
    this.initialScrollTop = this.scrollableContent?.scrollTop ?? 0;

    if (this.scrollRAF === null) {
      this.scrollRAF = requestAnimationFrame(this.scrollTick);
    }
  }

  private stopDrag(): void {
    this.isDragging = false;
    this.setScrollingState(false);

    if (this.scrollRAF !== null) {
      cancelAnimationFrame(this.scrollRAF);
      this.scrollRAF = null;
    }

    this.rect = null;
    this.lastTs = 0;
    this.initialScrollTop = 0;
  }

  private calculateVelocity(): number {
    if (!this.rect) return 0;

    const distTop = this.mouseY - this.rect.top;
    const distBot = this.rect.bottom - this.mouseY;

    if (distTop < this.INNER_ZONE) return -this.FAST_SPEED;
    if (distTop < this.OUTER_ZONE) return -this.SLOW_SPEED;
    if (distBot < this.INNER_ZONE) return this.FAST_SPEED;
    if (distBot < this.OUTER_ZONE) return this.SLOW_SPEED;

    return 0;
  }

  private isAtBoundary(velocity: number): boolean {
    if (!this.scrollableContent || !this.timeGrid || !this.draggedElement) return false;

    const atTop = this.scrollableContent.scrollTop <= 0 && velocity < 0;
    const atBottom = velocity > 0 &&
      this.draggedElement.getBoundingClientRect().bottom >=
      this.timeGrid.getBoundingClientRect().bottom;

    return atTop || atBottom;
  }

  private setScrollingState(scrolling: boolean): void {
    if (this.isScrolling === scrolling) return;

    this.isScrolling = scrolling;
    if (scrolling) {
      this.eventBus.emit(CoreEvents.EDGE_SCROLL_STARTED, {});
    } else {
      this.initialScrollTop = this.scrollableContent?.scrollTop ?? 0;
      this.eventBus.emit(CoreEvents.EDGE_SCROLL_STOPPED, {});
    }
  }

  private scrollTick = (ts: number): void => {
    if (!this.isDragging || !this.scrollableContent) return;

    const dt = this.lastTs ? (ts - this.lastTs) / 1000 : 0;
    this.lastTs = ts;
    this.rect ??= this.scrollableContent.getBoundingClientRect();

    const velocity = this.calculateVelocity();

    if (velocity !== 0 && !this.isAtBoundary(velocity)) {
      const scrollDelta = velocity * dt;
      this.scrollableContent.scrollTop += scrollDelta;
      this.rect = null;
      this.eventBus.emit(CoreEvents.EDGE_SCROLL_TICK, { scrollDelta });
      this.setScrollingState(true);
    } else {
      this.setScrollingState(false);
    }

    this.scrollRAF = requestAnimationFrame(this.scrollTick);
  };
}
