/**
 * A combination queue/set where the elements are ordered (in ascending order, according to the given priority function)
 * and unique (according to the given elementMatcher).
 *
 * If an element is added to the set and an equivalent element already exists, the lower-priority one is discarded.
 */
export class PriorityQueueSet {
	constructor(elementMatcher, priorityFunction) {
		this.first = null;
		this.elementMatcher = elementMatcher;
		this.priorityFunction = priorityFunction;
	}

	pushWithPriority(value) {
		const newNode = {value, priority: this.priorityFunction(value), next: null};

		// If the queue is currently empty, we can just set this new node as the first and we're done
		if (!this.first) {
			this.first = newNode;
			return;
		}

		let inserted = false;
		let previous;
		let current = this.first;

		// Loop through the existing elements
		while (current) {
			if (this.elementMatcher(current.value, value)) {
				// We've found an equivalent element before one with a lower priority. This one has at least
				// the same priority as the new one, so don't bother inserting
				return;
			} else if (newNode.priority <= current.priority) {
				// We've found some element with lower priority than the new one, so insert the new one just before it
				newNode.next = current;
				if (previous) {
					previous.next = newNode;
				} else {
					this.first = newNode;
				}
				inserted = true;

				previous = current;
				current = current.next;
				break;
			}
			previous = current;
			current = current.next;
		}

		if (inserted) {
			// Go through the rest of the list and try to find an equivalent element to the new one.
			// We know it has higher priority than the new one, so remove it.
			while (current) {
				if (this.elementMatcher(current.value, value)) {
					if (previous) {
						previous.next = current.next;
					} else {
						this.first = current.next;
					}
					return;
				}
				previous = current;
				current = current.next;
			}
		} else {
			// We reached the end of the queue without finding a lower-priority or existing element, so
			// insert the new one at the end
			previous.next = newNode;
		}
	}

	hasNext() {
		return !!this.first;
	}

	pop() {
		const first = this.first;
		this.first = first?.next;
		return first?.value;
	}
}

/**
 * Queue that will only ever accept elements with a given value once. Elements must have a "value" field, the
 * JSON representation of which will be used as the key to match
 */
export class ProcessOnceQueue {
	constructor() {
		this.first = null;
		this.last = null;
		this.previouslyQueued = new Set();
	}

	/**
	 * Remove everything from the queue and forget all the previously-queued items
	 */
	reset() {
		this.first = null;
		this.last = null;
		this.previouslyQueued.clear();
	}

	push(element) {
		if (this.previouslyQueued.has(element)) {
			return;
		}
		this.previouslyQueued.add(element);

		const newNode = {
			value: element,
			next: null,
			previous: null,
		};

		if (!this.first) {
			this.first = newNode;
			this.last = newNode;
		} else {
			this.last.next = newNode;
			newNode.previous = this.last;
			this.last = newNode;
		}
	}

	pop() {
		const node = this.first;
		this.first = node?.next;
		if (!node?.next) {
			this.last = null;
		}
		return node?.value;
	}

	hasNext() {
		return !!this.first;
	}
}
