import { isArray } from '../../types';
import type { BinaryHeapLike, Comparator } from './types';

export type { BinaryHeapLike, Comparator } from './types';

export interface BinaryHeapOptions<T> {
  comparator?: Comparator<T>;
}

/**
 * Default min-heap comparator for numeric values
 *
 * @param {number} a First element
 * @param {number} b Second element
 * @returns {number} Negative if a < b, positive if a > b, zero if equal
 */
const defaultComparator: Comparator<number> = (a: number, b: number) => a - b;

/**
 * @name BinaryHeap
 * @category Data Structures
 * @description Binary heap backed by a flat array with configurable comparator
 *
 * @since 0.0.8
 *
 * @template T The type of elements stored in the heap
 */
export class BinaryHeap<T> implements BinaryHeapLike<T> {
  /**
     * The comparator function used to order elements
     *
     * @private
     * @type {Comparator<T>}
     */
  private readonly comparator: Comparator<T>;

  /**
     * Internal flat array backing the heap
     *
     * @private
     * @type {T[]}
     */
  private readonly heap: T[] = [];

  /**
     * Creates an instance of BinaryHeap
     *
     * @param {(T[] | T)} [initialValues] The initial values to heapify
     * @param {BinaryHeapOptions<T>} [options] Heap configuration
     */
  constructor(initialValues?: T[] | T, options?: BinaryHeapOptions<T>) {
    // Numeric default; cast bridges it to the caller's `T` when no comparator is given.
    this.comparator = options?.comparator ?? (defaultComparator as Comparator<T>);

    if (initialValues !== null && initialValues !== undefined) {
      const items = isArray(initialValues) ? initialValues : [initialValues];

      // Avoid push(...items): spreading a large array as arguments overflows the call stack.
      for (const item of items)
        this.heap.push(item);

      this.heapify();
    }
  }

  /**
     * Gets the number of elements in the heap
     * @returns {number} The number of elements in the heap
     */
  public get length(): number {
    return this.heap.length;
  }

  /**
     * Checks if the heap is empty
     * @returns {boolean} `true` if the heap is empty, `false` otherwise
     */
  public get isEmpty(): boolean {
    return this.heap.length === 0;
  }

  /**
     * Pushes an element into the heap
     * @param {T} element The element to insert
     */
  public push(element: T): void {
    this.heap.push(element);
    this.siftUp(this.heap.length - 1);
  }

  /**
     * Removes and returns the root element (min or max depending on comparator)
     * @returns {T | undefined} The root element, or `undefined` if the heap is empty
     */
  public pop(): T | undefined {
    if (this.heap.length === 0) return undefined;

    const root = this.heap[0]!;
    const last = this.heap.pop()!;

    if (this.heap.length > 0) {
      this.heap[0] = last;
      this.siftDown(0);
    }

    return root;
  }

  /**
     * Returns the root element without removing it
     * @returns {T | undefined} The root element, or `undefined` if the heap is empty
     */
  public peek(): T | undefined {
    // Direct index preserves a legitimately stored null/undefined root (length is the empty check).
    return this.heap.length === 0 ? undefined : this.heap[0];
  }

  /**
     * Removes all elements from the heap
     * @returns {this} The heap instance for chaining
     */
  public clear(): this {
    this.heap.length = 0;
    return this;
  }

  /**
     * Returns a shallow copy of the heap elements as an array (heap order, not sorted)
     * @returns {T[]} Array of elements in heap order
     */
  public toArray(): T[] {
    return this.heap.slice();
  }

  /**
     * Returns a string representation of the heap
     * @returns {string} String representation
     */
  public toString(): string {
    return `BinaryHeap(${this.heap.length})`;
  }

  /**
     * Iterator over heap elements in heap order
     */
  public* [Symbol.iterator](): Iterator<T> {
    yield* this.heap;
  }

  /**
     * Async iterator over heap elements in heap order
     */
  public async* [Symbol.asyncIterator](): AsyncIterator<T> {
    for (const element of this.heap)
      yield element;
  }

  /**
     * Restores heap property by sifting an element up
     *
     * @private
     * @param {number} index The index of the element to sift up
     */
  private siftUp(index: number): void {
    const heap = this.heap;
    const cmp = this.comparator;

    while (index > 0) {
      const parent = (index - 1) >> 1;

      if (cmp(heap[index]!, heap[parent]!) >= 0) break;

      const temp = heap[index]!;
      heap[index] = heap[parent]!;
      heap[parent] = temp;

      index = parent;
    }
  }

  /**
     * Restores heap property by sifting an element down
     *
     * @private
     * @param {number} index The index of the element to sift down
     */
  private siftDown(index: number): void {
    const heap = this.heap;
    const cmp = this.comparator;
    const length = heap.length;

    while (true) {
      let smallest = index;
      const left = 2 * index + 1;
      const right = 2 * index + 2;

      if (left < length && cmp(heap[left]!, heap[smallest]!) < 0) {
        smallest = left;
      }

      if (right < length && cmp(heap[right]!, heap[smallest]!) < 0) {
        smallest = right;
      }

      if (smallest === index) break;

      const temp = heap[index]!;
      heap[index] = heap[smallest]!;
      heap[smallest] = temp;

      index = smallest;
    }
  }

  /**
     * Builds heap from unordered array in O(n) using Floyd's algorithm
     *
     * @private
     */
  private heapify(): void {
    for (let i = (this.heap.length >> 1) - 1; i >= 0; i--) {
      this.siftDown(i);
    }
  }
}
