/**
 * Utilities for handling selection across Shadow DOM boundaries
 */

export interface SelectionInfo {
  startContainer: Node;
  startOffset: number;
  endContainer: Node;
  endOffset: number;
  collapsed: boolean;
}

// Type for the extended caretPositionFromPoint with Shadow DOM support
type CaretPositionFromPointExtended = (x: number, y: number, ...shadowRoots: ShadowRoot[]) => CaretPosition | null;

export class WysiwygSelection {
  /**
   * Gets selection info that works across Shadow DOM boundaries
   * @param shadowRoots - Shadow roots to include in the selection search
   */
  static getSelectionInfo(...shadowRoots: ShadowRoot[]): SelectionInfo | null {
    const selection = window.getSelection();
    console.log('WysiwygSelection.getSelectionInfo - selection:', selection, 'rangeCount:', selection?.rangeCount);
    if (!selection) return null;

    // Try using getComposedRanges if available (better Shadow DOM support)
    if ('getComposedRanges' in selection && typeof selection.getComposedRanges === 'function') {
      console.log('Using getComposedRanges with', shadowRoots.length, 'shadow roots');
      try {
        // Pass shadow roots in the correct format as per MDN
        const ranges = selection.getComposedRanges({ shadowRoots });
        console.log('getComposedRanges returned', ranges.length, 'ranges');
        if (ranges.length > 0) {
          const range = ranges[0];
          return {
            startContainer: range.startContainer,
            startOffset: range.startOffset,
            endContainer: range.endContainer,
            endOffset: range.endOffset,
            collapsed: range.collapsed
          };
        }
      } catch (error) {
        console.warn('getComposedRanges failed, falling back to getRangeAt:', error);
      }
    } else {
      console.log('getComposedRanges not available, using fallback');
    }

    // Fallback to traditional selection API
    if (selection.rangeCount > 0) {
      const range = selection.getRangeAt(0);
      return {
        startContainer: range.startContainer,
        startOffset: range.startOffset,
        endContainer: range.endContainer,
        endOffset: range.endOffset,
        collapsed: range.collapsed
      };
    }

    return null;
  }

  /**
   * Checks if a selection is within a specific element (considering Shadow DOM)
   */
  static isSelectionInElement(element: Element, shadowRoot?: ShadowRoot): boolean {
    const selectionInfo = shadowRoot 
      ? this.getSelectionInfo(shadowRoot)
      : this.getSelectionInfo();
    
    if (!selectionInfo) return false;

    // Check if the selection's common ancestor is within the element
    return element.contains(selectionInfo.startContainer) || 
           element.contains(selectionInfo.endContainer);
  }

  /**
   * Gets the selected text across Shadow DOM boundaries
   */
  static getSelectedText(): string {
    const selection = window.getSelection();
    return selection ? selection.toString() : '';
  }

  /**
   * Creates a range from selection info
   */
  static createRangeFromInfo(info: SelectionInfo): Range {
    const range = document.createRange();
    range.setStart(info.startContainer, info.startOffset);
    range.setEnd(info.endContainer, info.endOffset);
    return range;
  }

  /**
   * Sets selection from a range (works with Shadow DOM)
   */
  static setSelectionFromRange(range: Range): void {
    const selection = window.getSelection();
    if (selection) {
      selection.removeAllRanges();
      selection.addRange(range);
    }
  }

  /**
   * Gets cursor position relative to a specific element
   */
  static getCursorPositionInElement(element: Element, ...shadowRoots: ShadowRoot[]): number | null {
    const selectionInfo = shadowRoots.length > 0
      ? this.getSelectionInfo(...shadowRoots)
      : this.getSelectionInfo();
    
    if (!selectionInfo || !selectionInfo.collapsed) return null;
    
    // Create a range from start of element to cursor position
    try {
      const range = document.createRange();
      range.selectNodeContents(element);
      
      // Handle case where selection is in a text node that's a child of the element
      // Use our Shadow DOM-aware contains method
      const isContained = this.containsAcrossShadowDOM(element, selectionInfo.startContainer);
      
      if (isContained) {
        range.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
        const position = range.toString().length;
        return position;
      } else {
        // Selection might be in shadow DOM or different context
        // Try to find the equivalent position in the element
        const text = element.textContent || '';
        const selectionText = selectionInfo.startContainer.textContent || '';
        
        // If the selection is at the beginning or end, handle those cases
        if (selectionInfo.startOffset === 0) {
          return 0;
        } else if (selectionInfo.startOffset === selectionText.length) {
          return text.length;
        }
        
        // For other cases, try to match based on text content
        console.warn('Selection container not within element, using text matching fallback');
        return selectionInfo.startOffset;
      }
    } catch (error) {
      console.warn('Failed to get cursor position:', error);
      return null;
    }
  }

  /**
   * Gets cursor position from mouse coordinates with Shadow DOM support
   */
  static getCursorPositionFromPoint(x: number, y: number, container: HTMLElement, ...shadowRoots: ShadowRoot[]): number | null {
    // Try modern API with shadow root support
    if ('caretPositionFromPoint' in document && document.caretPositionFromPoint) {
      let caretPos: CaretPosition | null = null;
      
      // Try with shadow roots first (newer API)
      try {
        caretPos = (document.caretPositionFromPoint as any)(x, y, ...shadowRoots);
      } catch (e) {
        // Fallback to standard API without shadow roots
        caretPos = document.caretPositionFromPoint(x, y);
      }
      
      if (caretPos && container.contains(caretPos.offsetNode)) {
        // Calculate total offset within the container
        return this.getOffsetInElement(caretPos.offsetNode, caretPos.offset, container);
      }
    }
    
    // Safari/WebKit fallback
    if ('caretRangeFromPoint' in document) {
      const range = (document as any).caretRangeFromPoint(x, y);
      if (range && container.contains(range.startContainer)) {
        return this.getOffsetInElement(range.startContainer, range.startOffset, container);
      }
    }
    
    return null;
  }
  
  /**
   * Helper to get the total character offset of a position within an element
   */
  private static getOffsetInElement(node: Node, offset: number, container: HTMLElement): number {
    let totalOffset = 0;
    let found = false;
    
    const walker = document.createTreeWalker(
      container,
      NodeFilter.SHOW_TEXT,
      null
    );
    
    let textNode: Node | null;
    while (textNode = walker.nextNode()) {
      if (textNode === node) {
        totalOffset += offset;
        found = true;
        break;
      } else {
        totalOffset += textNode.textContent?.length || 0;
      }
    }
    
    return found ? totalOffset : 0;
  }

  /**
   * Sets cursor position in an element
   */
  static setCursorPosition(element: Element, position: number): void {
    const walker = document.createTreeWalker(
      element,
      NodeFilter.SHOW_TEXT,
      null
    );

    let currentPosition = 0;
    let targetNode: Text | null = null;
    let targetOffset = 0;

    while (walker.nextNode()) {
      const node = walker.currentNode as Text;
      const nodeLength = node.textContent?.length || 0;
      
      if (currentPosition + nodeLength >= position) {
        targetNode = node;
        targetOffset = position - currentPosition;
        break;
      }
      
      currentPosition += nodeLength;
    }

    if (targetNode) {
      const range = document.createRange();
      range.setStart(targetNode, targetOffset);
      range.collapse(true);
      this.setSelectionFromRange(range);
    }
  }

  /**
   * Check if a node is contained within an element across Shadow DOM boundaries
   * This is needed because element.contains() doesn't work across Shadow DOM
   */
  static containsAcrossShadowDOM(container: Node, node: Node): boolean {
    if (!container || !node) return false;
    
    // Start with the node and traverse up
    let current: Node | null = node;
    
    while (current) {
      // Direct match
      if (current === container) {
        return true;
      }
      
      // If we're at a shadow root, check its host
      if (current.nodeType === Node.DOCUMENT_FRAGMENT_NODE && (current as any).host) {
        const shadowRoot = current as ShadowRoot;
        // Check if the container is within this shadow root
        if (shadowRoot.contains(container)) {
          return false; // Container is in a child shadow DOM
        }
        // Move to the host element
        current = shadowRoot.host;
      } else {
        // Regular DOM traversal
        current = current.parentNode;
      }
    }
    
    return false;
  }
}