'use strict';
import { Iterables } from './iterable';
import { Strings } from './string';

// Code stolen from https://github.com/Microsoft/vscode/blob/b3e6d5bb039a4a9362b52a2c8726267ca68cf64e/src/vs/base/common/map.ts#L352

const FIN = { done: true, value: undefined };

// eslint-disable-next-line @typescript-eslint/interface-name-prefix
export interface IKeyIterator {
	reset(key: string): this;
	next(): this;

	hasNext(): boolean;
	cmp(a: string): number;
	value(): string;
}

export class StringIterator implements IKeyIterator {
	private _value: string = '';
	private _pos: number = 0;

	reset(key: string): this {
		this._value = key;
		this._pos = 0;
		return this;
	}

	next(): this {
		this._pos += 1;
		return this;
	}

	hasNext(): boolean {
		return this._pos < this._value.length - 1;
	}

	cmp(a: string): number {
		const aCode = a.charCodeAt(0);
		const thisCode = this._value.charCodeAt(this._pos);
		return aCode - thisCode;
	}

	value(): string {
		return this._value[this._pos];
	}
}

export class PathIterator implements IKeyIterator {
	private _value!: string;
	private _from!: number;
	private _to!: number;

	reset(key: string): this {
		this._value = key.replace(/\\$|\/$/, '');
		this._from = 0;
		this._to = 0;
		return this.next();
	}

	hasNext(): boolean {
		return this._to < this._value.length;
	}

	next(): this {
		// this._data = key.split(/[\\/]/).filter(s => !!s);
		this._from = this._to;
		let justSeps = true;
		for (; this._to < this._value.length; this._to++) {
			const ch = this._value.charCodeAt(this._to);
			if (ch === Strings.CharCode.Slash || ch === Strings.CharCode.Backslash) {
				if (justSeps) {
					this._from++;
				} else {
					break;
				}
			} else {
				justSeps = false;
			}
		}
		return this;
	}

	cmp(a: string): number {
		let aPos = 0;
		const aLen = a.length;
		let thisPos = this._from;

		while (aPos < aLen && thisPos < this._to) {
			const cmp = a.charCodeAt(aPos) - this._value.charCodeAt(thisPos);
			if (cmp !== 0) {
				return cmp;
			}

			aPos += 1;
			thisPos += 1;
		}

		if (aLen === this._to - this._from) {
			return 0;
		}

		if (aPos < aLen) {
			return -1;
		}

		return 1;
	}

	value(): string {
		return this._value.substring(this._from, this._to);
	}
}

class TernarySearchTreeNode<E> {
	segment!: string;
	value: E | undefined;
	key!: string;
	left: TernarySearchTreeNode<E> | undefined;
	mid: TernarySearchTreeNode<E> | undefined;
	right: TernarySearchTreeNode<E> | undefined;

	isEmpty(): boolean {
		return !this.left && !this.mid && !this.right && !this.value;
	}
}

export class TernarySearchTree<E> {
	static forPaths<E>(): TernarySearchTree<E> {
		return new TernarySearchTree<E>(new PathIterator());
	}

	static forStrings<E>(): TernarySearchTree<E> {
		return new TernarySearchTree<E>(new StringIterator());
	}

	private _iter: IKeyIterator;
	private _root: TernarySearchTreeNode<E> | undefined;

	constructor(segments: IKeyIterator) {
		this._iter = segments;
	}

	clear(): void {
		this._root = undefined;
	}

	set(key: string, element: E): E | undefined {
		const iter = this._iter.reset(key);
		let node: TernarySearchTreeNode<E>;

		if (!this._root) {
			this._root = new TernarySearchTreeNode<E>();
			this._root.segment = iter.value();
		}

		node = this._root;
		while (true) {
			const val = iter.cmp(node.segment);
			if (val > 0) {
				// left
				if (!node.left) {
					node.left = new TernarySearchTreeNode<E>();
					node.left.segment = iter.value();
				}
				node = node.left;
			} else if (val < 0) {
				// right
				if (!node.right) {
					node.right = new TernarySearchTreeNode<E>();
					node.right.segment = iter.value();
				}
				node = node.right;
			} else if (iter.hasNext()) {
				// mid
				iter.next();
				if (!node.mid) {
					node.mid = new TernarySearchTreeNode<E>();
					node.mid.segment = iter.value();
				}
				node = node.mid;
			} else {
				break;
			}
		}
		const oldElement = node.value;
		node.value = element;
		node.key = key;
		return oldElement;
	}

	get(key: string): E | undefined {
		const iter = this._iter.reset(key);
		let node = this._root;
		while (node) {
			const val = iter.cmp(node.segment);
			if (val > 0) {
				// left
				node = node.left;
			} else if (val < 0) {
				// right
				node = node.right;
			} else if (iter.hasNext()) {
				// mid
				iter.next();
				node = node.mid;
			} else {
				break;
			}
		}
		return node ? node.value : undefined;
	}

	delete(key: string): void {
		const iter = this._iter.reset(key);
		const stack: [-1 | 0 | 1, TernarySearchTreeNode<E>][] = [];
		let node = this._root;

		// find and unset node
		while (node) {
			const val = iter.cmp(node.segment);
			if (val > 0) {
				// left
				stack.push([1, node]);
				node = node.left;
			} else if (val < 0) {
				// right
				stack.push([-1, node]);
				node = node.right;
			} else if (iter.hasNext()) {
				// mid
				iter.next();
				stack.push([0, node]);
				node = node.mid;
			} else {
				// remove element
				node.value = undefined;

				// clean up empty nodes
				while (stack.length > 0 && node.isEmpty()) {
					const [dir, parent] = stack.pop()!;
					switch (dir) {
						case 1:
							parent.left = undefined;
							break;
						case 0:
							parent.mid = undefined;
							break;
						case -1:
							parent.right = undefined;
							break;
					}
					node = parent;
				}
				break;
			}
		}
	}

	findSubstr(key: string): E | undefined {
		const iter = this._iter.reset(key);
		let node = this._root;
		let candidate: E | undefined;
		while (node) {
			const val = iter.cmp(node.segment);
			if (val > 0) {
				// left
				node = node.left;
			} else if (val < 0) {
				// right
				node = node.right;
			} else if (iter.hasNext()) {
				// mid
				iter.next();
				candidate = node.value || candidate;
				node = node.mid;
			} else {
				break;
			}
		}
		return (node && node.value) || candidate;
	}

	findSuperstr(key: string, limit: boolean = false): Iterable<E> | undefined {
		const iter = this._iter.reset(key);
		let node = this._root;
		while (node) {
			const val = iter.cmp(node.segment);
			if (val > 0) {
				// left
				node = node.left;
			} else if (val < 0) {
				// right
				node = node.right;
			} else if (iter.hasNext()) {
				// mid
				iter.next();
				node = node.mid;
			} else {
				// collect
				if (!node.mid) {
					return undefined;
				}

				node = node.mid;
				return {
					// eslint-disable-next-line no-loop-func
					[Symbol.iterator]: () => this._nodeIterator(node!, limit)
				};
			}
		}
		return undefined;
	}

	private _nodeIterator(node: TernarySearchTreeNode<E>, limit: boolean = false): Iterator<E> {
		let res: { done: false; value: E };
		let idx: number;
		let data: E[];
		const next = (): IteratorResult<E> => {
			if (!data) {
				// lazy till first invocation
				data = [];
				idx = 0;
				this._forEach(node, value => data.push(value), limit);
			}
			if (idx >= data.length) {
				return (FIN as unknown) as IteratorResult<E>;
			}

			if (!res) {
				res = { done: false, value: data[idx++] };
			} else {
				res.value = data[idx++];
			}
			return res;
		};
		return { next: next };
	}

	forEach(callback: (value: E, index: string) => any) {
		this._forEach(this._root, callback);
	}

	private _forEach(
		node: TernarySearchTreeNode<E> | undefined,
		callback: (value: E, index: string) => any,
		limit: boolean = false
	) {
		if (node === undefined) return;

		// left
		this._forEach(node.left, callback, limit);

		// node
		if (node.value) {
			callback(node.value, node.key);
		}

		if (!limit) {
			// mid
			this._forEach(node.mid, callback, limit);
		}

		// right
		this._forEach(node.right, callback, limit);
	}

	any(): boolean {
		return this._root !== undefined && !this._root.isEmpty();
	}

	count(predicate?: (entry: E) => boolean): number {
		if (this._root === undefined || this._root.isEmpty()) return 0;

		return Iterables.count(this.entries(), predicate === undefined ? undefined : ([e]) => predicate(e));
	}

	entries(): Iterable<[E, string]> {
		return this._iterator(this._root);
	}

	values(): Iterable<E> {
		return Iterables.map(this.entries(), ([e]) => e);
	}

	highlander(): [E, string] | undefined {
		if (this._root === undefined || this._root.isEmpty()) return undefined;

		const entries = this.entries() as IterableIterator<[E, string]>;

		let count = 0;
		let next: IteratorResult<[E, string]>;
		let value: [E, string] | undefined;

		while (true) {
			next = entries.next();
			if (next.done) break;

			value = next.value;

			count++;
			if (count > 1) return undefined;
		}

		return value;
	}

	some(predicate: (entry: E) => boolean): boolean {
		if (this._root === undefined || this._root.isEmpty()) return false;

		return Iterables.some(this.entries(), ([e]) => predicate(e));
	}

	private *_iterator(node: TernarySearchTreeNode<E> | undefined): IterableIterator<[E, string]> {
		if (node !== undefined) {
			// left
			yield* this._iterator(node.left);

			// node
			if (node.value) {
				yield [node.value, node.key];
			}

			// mid
			yield* this._iterator(node.mid);

			// right
			yield* this._iterator(node.right);
		}
	}
}
