/**
 * Node utility.
 */
export default class ClassMethodBinder {
	private target: object;
	private classes: any[];
	private cache = new Map<string | symbol, boolean>();

	/**
	 * Constructor.
	 *
	 * @param target Target.
	 * @param classes Classes.
	 */
	constructor(target: object, classes: any[]) {
		this.target = target;
		this.classes = classes;
	}

	/**
	 * Binds method, getters and setters to a target.
	 *
	 * @param name Method name.
	 */
	public bind(name: string | symbol): void {
		// We should never bind the Symbol.iterator method as it can cause problems with Array.from()
		if (this.cache.has(name) || name === Symbol.iterator || name === 'constructor') {
			return;
		}

		this.cache.set(name, true);

		const target = this.target;

		if (!(name in target)) {
			return;
		}

		for (const _class of this.classes) {
			const descriptor = Object.getOwnPropertyDescriptor(_class.prototype, name);
			if (descriptor) {
				if (typeof descriptor.value === 'function') {
					if (descriptor.value.toString().startsWith('class ')) {
						// Do not bind classes
						return;
					}
					Object.defineProperty(target, name, {
						...descriptor,
						value: descriptor.value.bind(target)
					});
				} else if (descriptor.get !== undefined) {
					Object.defineProperty(target, name, {
						...descriptor,
						get: descriptor.get?.bind(target),
						set: descriptor.set?.bind(target)
					});
				}
				return;
			}
		}
	}

	/**
	 * Prevents a method, getter or setter from being bound.
	 *
	 * @param name Method name.
	 */
	public preventBinding(name: string | symbol): void {
		this.cache.set(name, true);
	}
}
