/**
 * A binary heap implementation for efficient priority queue operations
 * Supports O(log n) insert and extract operations
 */
export class BinaryHeap<T> {
  private heap: T[] = [];
  private keyMap?: Map<string, number>; // For efficient key-based lookups

  constructor(
    private compareFn: (a: T, b: T) => number,
    private extractKey?: (item: T) => string
  ) {
    if (extractKey) {
      this.keyMap = new Map();
    }
  }

  /**
   * Get the current size of the heap
   */
  public get size(): number {
    return this.heap.length;
  }

  /**
   * Check if the heap is empty
   */
  public isEmpty(): boolean {
    return this.heap.length === 0;
  }

  /**
   * Peek at the top element without removing it
   */
  public peek(): T | undefined {
    return this.heap[0];
  }

  /**
   * Insert a new item into the heap
   * O(log n) time complexity
   */
  public insert(item: T): void {
    const index = this.heap.length;
    this.heap.push(item);
    
    if (this.keyMap && this.extractKey) {
      const key = this.extractKey(item);
      this.keyMap.set(key, index);
    }
    
    this.bubbleUp(index);
  }

  /**
   * Extract the top element from the heap
   * O(log n) time complexity
   */
  public extract(): T | undefined {
    if (this.heap.length === 0) return undefined;
    if (this.heap.length === 1) {
      const item = this.heap.pop()!;
      if (this.keyMap && this.extractKey) {
        this.keyMap.delete(this.extractKey(item));
      }
      return item;
    }
    
    const result = this.heap[0];
    const lastItem = this.heap.pop()!;
    this.heap[0] = lastItem;
    
    if (this.keyMap && this.extractKey) {
      this.keyMap.delete(this.extractKey(result));
      this.keyMap.set(this.extractKey(lastItem), 0);
    }
    
    this.bubbleDown(0);
    return result;
  }

  /**
   * Extract an element that matches the predicate
   * O(n) time complexity for search, O(log n) for extraction
   */
  public extractIf(predicate: (item: T) => boolean): T | undefined {
    const index = this.heap.findIndex(predicate);
    if (index === -1) return undefined;
    
    return this.extractAt(index);
  }

  /**
   * Extract an element by its key (if extractKey was provided)
   * O(log n) time complexity
   */
  public extractByKey(key: string): T | undefined {
    if (!this.keyMap || !this.extractKey) {
      throw new Error('extractKey function must be provided to use key-based extraction');
    }
    
    const index = this.keyMap.get(key);
    if (index === undefined) return undefined;
    
    return this.extractAt(index);
  }

  /**
   * Check if a key exists in the heap
   * O(1) time complexity
   */
  public hasKey(key: string): boolean {
    if (!this.keyMap) return false;
    return this.keyMap.has(key);
  }

  /**
   * Get all elements as an array (does not modify heap)
   * O(n) time complexity
   */
  public toArray(): T[] {
    return [...this.heap];
  }

  /**
   * Clear the heap
   */
  public clear(): void {
    this.heap = [];
    if (this.keyMap) {
      this.keyMap.clear();
    }
  }

  /**
   * Extract element at specific index
   */
  private extractAt(index: number): T {
    const item = this.heap[index];
    
    if (this.keyMap && this.extractKey) {
      this.keyMap.delete(this.extractKey(item));
    }
    
    if (index === this.heap.length - 1) {
      this.heap.pop();
      return item;
    }
    
    const lastItem = this.heap.pop()!;
    this.heap[index] = lastItem;
    
    if (this.keyMap && this.extractKey) {
      this.keyMap.set(this.extractKey(lastItem), index);
    }
    
    // Try bubbling up first
    const parentIndex = Math.floor((index - 1) / 2);
    if (parentIndex >= 0 && this.compareFn(this.heap[index], this.heap[parentIndex]) < 0) {
      this.bubbleUp(index);
    } else {
      this.bubbleDown(index);
    }
    
    return item;
  }

  /**
   * Bubble up element at given index to maintain heap property
   */
  private bubbleUp(index: number): void {
    while (index > 0) {
      const parentIndex = Math.floor((index - 1) / 2);
      
      if (this.compareFn(this.heap[index], this.heap[parentIndex]) >= 0) {
        break;
      }
      
      this.swap(index, parentIndex);
      index = parentIndex;
    }
  }

  /**
   * Bubble down element at given index to maintain heap property
   */
  private bubbleDown(index: number): void {
    const length = this.heap.length;
    
    while (true) {
      const leftChild = 2 * index + 1;
      const rightChild = 2 * index + 2;
      let smallest = index;
      
      if (leftChild < length && 
          this.compareFn(this.heap[leftChild], this.heap[smallest]) < 0) {
        smallest = leftChild;
      }
      
      if (rightChild < length && 
          this.compareFn(this.heap[rightChild], this.heap[smallest]) < 0) {
        smallest = rightChild;
      }
      
      if (smallest === index) break;
      
      this.swap(index, smallest);
      index = smallest;
    }
  }

  /**
   * Swap two elements in the heap
   */
  private swap(i: number, j: number): void {
    const temp = this.heap[i];
    this.heap[i] = this.heap[j];
    this.heap[j] = temp;
    
    if (this.keyMap && this.extractKey) {
      this.keyMap.set(this.extractKey(this.heap[i]), i);
      this.keyMap.set(this.extractKey(this.heap[j]), j);
    }
  }
}