// https://www.geeksforgeeks.org/find-top-k-or-most-frequent-numbers-in-a-stream/

/* 
Time Complexity - O(n + klog(k))
Space Complexity - O(k) from set + O(k) from heap

1. Create a set and a min heap (max size of heap and set will be k). 
2. Add first k unique elements to heap and set
3. Heapify
4. For the rest of the numbers (or for the numbers incoming from stream)
   check if it is greater than the top of the heap and if it already exists
   in the set
5. If these conditions are met, get the min of the heap, delete the min from the set,
   swap the num with the top of the heap (heap min) and sink it down.
   Also add the num to the set
*/

function topK(arr, k) {
	const len = arr.length

	if (len < k) return [new Set(arr)]

	const heap = new MinBinaryHeap(),
		set = new Set()

	let i = 0

	while (i < len && heap.size < k) {
		const curr = arr[i]

		if (!set.has(curr)) {
			heap.insert(curr)
			set.add(curr)
		}
		i++
	}

	heap.heapify()

	while (i < len) {
		const curr = arr[i],
			min = heap.top

		if (curr > min && !set.has(curr)) {
			heap.swapTopAndSinkDown(curr)
			set.delete(min)
			set.add(curr)
		}
		i++
	}

	return heap.heapArray
}

// Min Binary Heap
class MinBinaryHeap {
	constructor() {
		this.heapArr = []
	}
	// getters
	get size() {
		return this.heapArr.length
	}

	get heapArray() {
		return this.heapArr
	}

	get top() {
		return this.heapArr[0]
	}
	// Class methods
	swap(i, j) {
		const temp = this.heapArr[i]
		this.heapArr[i] = this.heapArr[j]
		this.heapArr[j] = temp
	}

	insert(val) {
		this.heapArr[this.size] = val
	}

	swapTopAndSinkDown(newVal) {
		this.heapArr[0] = newVal
		this.sinkDown(0)
	}

	heapify() {
		const lastIndex = this.size - 1
		for (let i = lastIndex; i >= 0; i--) {
			this.sinkDown(i)
		}
	}

	sinkDown(p) {
		const element = this.heapArr[p],
			lastIndex = this.heapArr.length - 1

		while (p < lastIndex) {
			const r = 2 * p + 2,
				l = r - 1,
				left = this.heapArr[l],
				right = this.heapArr[r]

			let swapIndex = null,
				min = element

			if (l <= lastIndex && left < min) {
				swapIndex = l
				min = left
			}

			if (r <= lastIndex && right < min) {
				swapIndex = r
			}

			if (swapIndex == null) break

			this.swap(p, swapIndex)

			p = swapIndex
		}
	}
}

const arr = [8, 1, 3, 4, 1, 5, 9, 7, 10, 7, 3, 10]
console.log(topK(arr, 10))
console.log(topK(arr, 5))

const arr1 = [4, 2, 1, 6, 2, 10, 4, 3, 10, 6, 5, 6, 7, 2, 10, 10, 4, 6, 5, 8]
console.log(topK(arr1, 7))