/**
 * Optimized scroll management using Intersection Observer
 * Reduces expensive operations during scroll events
 */
export class OptimizedScrollManager {
  /**
   * @param {*} overlayManager
   * @param {*} stateManager
   * @param {{ onScrollFrame?: () => void }} [hooks] - optional live scroll hooks (e.g. highlight refresh)
   */
  constructor(overlayManager, stateManager, hooks = {}) {
    this.overlayManager = overlayManager;
    this.stateManager = stateManager;
    this.onScrollFrame = typeof hooks?.onScrollFrame === 'function' ? hooks.onScrollFrame : null;
    
    // Scroll state tracking
    this.isScrolling = false;
    this.scrollTimeout = null;
    this.scrollStartTime = 0;
    
    // Intersection observer for scroll-sensitive elements
    this.scrollObserver = null;
    
    // Elements that need position updates during scroll
    this.scrollSensitiveElements = new Set();
    
    // Throttled scroll handler
    this.throttledScrollHandler = this.throttle(this.handleScrollThrottled.bind(this), 16); // ~60fps
    
    // Bound event handlers (so cleanup can reliably remove listeners)
    this.boundScrollHandler = null;
    this.boundWheelHandler = null;

    // Performance tracking
    this.scrollMetrics = {
      scrollEvents: 0,
      overlayUpdates: 0,
      throttledCalls: 0,
      averageScrollDuration: 0
    };
  }

  init() {
    this.setupScrollObserver();
    this.setupScrollListeners();
    this.setupStateSubscription();
    this.observeCurrentStateElements();
  }

  observeCurrentStateElements() {
    // Ensure we start tracking the currently focused elements even if state was
    // already populated before OptimizedScrollManager initialized.
    const currentState = this.stateManager?.getState?.();
    if (!currentState) return;

    this.observeElementForScroll(currentState.focusEl);
    this.observeElementForScroll(currentState.inspectorEl || currentState.deleteEl);
    this.observeElementForScroll(currentState.focusedTextElement);
  }

  setupScrollObserver() {
    // Observer to track when elements move in/out of view during scroll
    this.scrollObserver = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          const element = entry.target;
          
          if (entry.isIntersecting) {
            // Element is visible, may need overlay updates
            this.scrollSensitiveElements.add(element);
          } else {
            // Element is out of view, can skip overlay updates
            this.scrollSensitiveElements.delete(element);
            
            // Hide any overlays for out-of-view elements
            this.hideOverlaysForElement(element);
          }
        });
      },
      {
        rootMargin: '20px', // Small margin to catch elements just entering/leaving
        threshold: [0, 1.0]
      }
    );
  }

  setupScrollListeners() {
    // Use passive listener for better performance
    this.boundScrollHandler = this.handleScroll.bind(this);
    this.boundWheelHandler = this.handleWheel.bind(this);

    document.addEventListener('scroll', this.boundScrollHandler, { 
      passive: true,
      capture: true 
    });
    
    // Also listen for wheel events to predict scroll direction
    document.addEventListener('wheel', this.boundWheelHandler, { 
      passive: true 
    });
  }

  setupStateSubscription() {
    // Subscribe to state changes to track new elements
    this.stateUnsubscribe = this.stateManager.subscribe((newState, prevState) => {
      // Start observing new elements
      if (newState.focusEl !== prevState.focusEl) {
        if (prevState.focusEl) {
          this.unobserveElementForScroll(prevState.focusEl);
        }
        if (newState.focusEl) {
          this.observeElementForScroll(newState.focusEl);
        }
      }
      
      // Shared inspector pick target (Delete / Cols / future kinds)
      const prevInsp = prevState.inspectorEl || prevState.deleteEl || prevState.colsEl;
      const nextInsp = newState.inspectorEl || newState.deleteEl || newState.colsEl;
      if (nextInsp !== prevInsp) {
        if (prevInsp) {
          this.unobserveElementForScroll(prevInsp);
        }
        if (nextInsp) {
          this.observeElementForScroll(nextInsp);
        }
      }
      
      if (newState.focusedTextElement !== prevState.focusedTextElement) {
        if (prevState.focusedTextElement) {
          this.unobserveElementForScroll(prevState.focusedTextElement);
        }
        if (newState.focusedTextElement) {
          this.observeElementForScroll(newState.focusedTextElement);
        }
      }
    });
  }

  handleScroll(event) {
    this.scrollMetrics.scrollEvents++;

    // Mark scroll start
    if (!this.isScrolling) {
      this.isScrolling = true;
      this.scrollStartTime = performance.now();

      // Hide focus overlay immediately when scrolling starts (for R-Tree mode)
      // This prevents the green rectangle from staying visible during scroll
      this.overlayManager?.hideFocusOverlay?.();
    }

    // Use throttled handler for performance
    this.throttledScrollHandler(event);

    // Clear existing timeout and set new one
    if (this.scrollTimeout) {
      clearTimeout(this.scrollTimeout);
    }

    // Detect scroll end
    this.scrollTimeout = setTimeout(() => {
      this.handleScrollEnd();
    }, 100);
  }

  handleScrollThrottled(event) {
    this.scrollMetrics.throttledCalls++;
    
    const currentState = this.stateManager.getState();
    
    // Always update overlay positions during scroll so they track the page movement,
    // even if IntersectionObserver tracking hasn't been established for the element yet.
    // (This fixes the "hover/click rectangle sticks during scroll" issue.)
    if (currentState.focusEl && currentState.focusEl.isConnected) {
      this.updateOverlayPosition(currentState.focusEl, 'focus');
    }

    const inspectorEl = currentState.inspectorEl || currentState.deleteEl || currentState.colsEl;
    if (inspectorEl && inspectorEl.isConnected) {
      this.updateOverlayPosition(inspectorEl, 'inspector');
    }

    // Update focused text element overlays (both focused text overlay and active text input frame)
    if (currentState.focusedTextElement && currentState.focusedTextElement.isConnected) {
      this.updateOverlayPosition(currentState.focusedTextElement, 'focusedText');
    }

    // Text / rectangle select: keep dashed selection rect + carets live with scroll
    // (document-anchored origin). Without this, overlays only jumped on scroll-end.
    if (currentState.mode === 'highlight' && this.onScrollFrame) {
      try {
        this.onScrollFrame();
      } catch (e) {
        // Non-fatal — selection refresh should never break scroll handling
      }
    }
    
    this.scrollMetrics.overlayUpdates++;
  }

  handleWheel(event) {
    // Predict scroll direction and prepare for smooth updates
    const direction = event.deltaY > 0 ? 'down' : 'up';
    
    // Pre-emptively update overlay positions based on predicted scroll
    this.prepareForScroll(direction);
  }

  prepareForScroll(direction) {
    // Pre-calculate positions for smooth scrolling
    const currentState = this.stateManager.getState();
    
    if (currentState.focusEl) {
      this.observeElementForScroll(currentState.focusEl);
    }
    
    const inspectorEl = currentState.inspectorEl || currentState.deleteEl || currentState.colsEl;
    if (inspectorEl) {
      this.observeElementForScroll(inspectorEl);
    }
    
    if (currentState.focusedTextElement) {
      this.observeElementForScroll(currentState.focusedTextElement);
    }
  }

  observeElementForScroll(element) {
    if (element && this.scrollObserver) {
      this.scrollObserver.observe(element);
      this.scrollSensitiveElements.add(element);
    }
  }

  unobserveElementForScroll(element) {
    if (element && this.scrollObserver) {
      this.scrollObserver.unobserve(element);
      this.scrollSensitiveElements.delete(element);
    }
  }

  updateOverlayPosition(element, type) {
    if (!element) return;
    
    // Animation removed - update overlays immediately
    if (type === 'focus') {
      this.overlayManager.updateFocusOverlay(element);
    } else if (type === 'inspector' || type === 'delete' || type === 'cols') {
      const kind = this.stateManager.getState()?.inspectorKind
        || (type === 'cols' ? 'cols' : type === 'delete' ? 'delete' : null);
      if (typeof this.overlayManager.updateInspectorOverlay === 'function') {
        this.overlayManager.updateInspectorOverlay(element, kind);
      } else {
        this.overlayManager.updateDeleteOverlay?.(element);
      }
    } else if (type === 'focusedText') {
      // Update both the focused text overlay and active text input frame
      this.overlayManager.updateFocusedTextOverlay(element);
      this.overlayManager.updateActiveTextInputFrame(element);
    }
  }

  hideOverlaysForElement(element) {
    const currentState = this.stateManager.getState();
    
    // Hide overlays if they're for elements that are out of view
    if (currentState.focusEl === element) {
      this.overlayManager.hideFocusOverlay();
    }

    const inspectorEl = currentState.inspectorEl || currentState.deleteEl || currentState.colsEl;
    if (inspectorEl === element) {
      if (typeof this.overlayManager.hideInspectorOverlay === 'function') {
        this.overlayManager.hideInspectorOverlay();
      } else {
        this.overlayManager.hideDeleteOverlay?.();
      }
    }
    
    if (currentState.focusedTextElement === element) {
      this.overlayManager.hideFocusedTextOverlay();
      this.overlayManager.hideActiveTextInputFrame();
    }
  }

  handleScrollEnd() {
    const scrollDuration = performance.now() - this.scrollStartTime;
    
    // Update average scroll duration
    this.scrollMetrics.averageScrollDuration = 
      (this.scrollMetrics.averageScrollDuration + scrollDuration) / 2;
    
    this.isScrolling = false;
    
    // Force a complete overlay update after scroll ends
    const currentState = this.stateManager.getState();
    
    // Re-query elements at current mouse position for accuracy
    if (currentState.lastMouse.x >= 0 && currentState.lastMouse.y >= 0) {
      // Dispatch a custom event to trigger element re-query
      document.dispatchEvent(new CustomEvent('keypilot:scroll-end', {
        detail: {
          mouseX: currentState.lastMouse.x,
          mouseY: currentState.lastMouse.y
        }
      }));
    }
    
    // Clean up observers for elements that are no longer relevant
    this.cleanupScrollObservers();
  }

  cleanupScrollObservers() {
    // Remove observers for elements that are no longer in the DOM.
    // Use isConnected (not document.contains) so open-shadow nodes are kept.
    for (const element of this.scrollSensitiveElements) {
      if (!element || !element.isConnected) {
        this.unobserveElementForScroll(element);
      }
    }
  }

  // Throttle utility function
  throttle(func, limit) {
    let inThrottle;
    return function(...args) {
      if (!inThrottle) {
        func.apply(this, args);
        inThrottle = true;
        setTimeout(() => inThrottle = false, limit);
      }
    };
  }

  // Get scroll performance metrics
  getScrollMetrics() {
    const throttleRatio = this.scrollMetrics.scrollEvents > 0 ? 
      (this.scrollMetrics.throttledCalls / this.scrollMetrics.scrollEvents * 100).toFixed(1) : 0;
    
    return {
      ...this.scrollMetrics,
      throttleRatio: `${throttleRatio}%`,
      activeSensitiveElements: this.scrollSensitiveElements.size,
      isCurrentlyScrolling: this.isScrolling
    };
  }

  // Cleanup method
  cleanup() {
    if (this.scrollObserver) {
      this.scrollObserver.disconnect();
      this.scrollObserver = null;
    }
    
    if (this.scrollTimeout) {
      clearTimeout(this.scrollTimeout);
      this.scrollTimeout = null;
    }
    
    if (this.stateUnsubscribe) {
      this.stateUnsubscribe();
      this.stateUnsubscribe = null;
    }
    
    this.scrollSensitiveElements.clear();
    
    // Remove event listeners
    if (this.boundScrollHandler) {
      document.removeEventListener('scroll', this.boundScrollHandler, { capture: true });
      this.boundScrollHandler = null;
    }
    if (this.boundWheelHandler) {
      document.removeEventListener('wheel', this.boundWheelHandler);
      this.boundWheelHandler = null;
    }
  }
}