/**
 * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact Volker Schukai.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

import { Base } from "./base.mjs";
import { isArray, isElement, isObject, isPrimitive } from "./is.mjs";
import { Observer } from "./observer.mjs";
import { ObserverList } from "./observerlist.mjs";
import { validateObject } from "./validate.mjs";
import { extend } from "../data/extend.mjs";
import { instanceSymbol, proxyInstanceMarker } from "../constants.mjs";
import { clone } from "../util/clone.mjs";
export { ProxyObserver };

/**
 * An observer manages a callback function
 *
 * With the ProxyObserver you can attach observer for observation.
 * With each change at the object to be observed, an update takes place.
 *
 * This also applies to nested objects.
 *
 * @license AGPLv3
 * @since 1.0.0
 * @copyright Volker Schukai
 */
class ProxyObserver extends Base {
	/**
	 *
	 * @param {object} object
	 * @throws {TypeError} value is not a object
	 */
	constructor(object) {
		super();

		this.realSubject = validateObject(object);
		this.subject = new Proxy(object, getHandler.call(this));

		this.objectMap = new WeakMap();
		this.objectMap.set(this.realSubject, this.subject);

		this.proxyMap = new WeakMap();
		this.proxyMap.set(this.subject, this.realSubject);

		this.observers = new ObserverList();
	}

	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 * @since 2.1.0
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/types/proxy-observer");
	}

	/**
	 * @return {object}
	 */
	getSubject() {
		return this.subject;
	}

	/**
	 * @since 1.24.0
	 * @param {Object} obj
	 * @return {ProxyObserver}
	 */
	setSubject(obj) {
		let i;
		const clonedObject = clone(obj);
		const k = Object.keys(this.subject);
		for (i = 0; i < k.length; i++) {
			delete this.subject[k[i]];
		}

		this.subject = extend(this.subject, clonedObject);
		return this;
	}

	/**
	 * Retrieves the real subject associated with the current instance.
	 *
	 * @return {Object} The real subject object.
	 */
	getRealSubject() {
		return this.realSubject;
	}

	/**
	 * attach a new observer
	 *
	 * @param {Observer} observer
	 * @return {ProxyObserver}
	 */
	attachObserver(observer) {
		this.observers.attach(observer);
		return this;
	}

	/**
	 * detach a observer
	 *
	 * @param {Observer} observer
	 * @return {ProxyObserver}
	 */
	detachObserver(observer) {
		this.observers.detach(observer);
		return this;
	}

	/**
	 * notify all observer
	 *
	 * @return {Promise}
	 */
	notifyObservers() {
		return this.observers.notify(this);
	}

	/**
	 * @param {Observer} observer
	 * @return {boolean}
	 */
	containsObserver(observer) {
		return this.observers.contains(observer);
	}
}

/**
 *
 * @return {{defineProperty: (function(*=, *=, *=): *), setPrototypeOf: (function(*, *=): boolean), set: (function(*, *, *, *): boolean), get: ((function(*=, *=, *=): (undefined))|*), deleteProperty: ((function(*, *): (boolean))|*)}}
 * @private
 * @see {@link https://gitlab.schukai.com/-/snippets/49}
 */
function getHandler() {
	const proxy = this;

	// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots
	const handler = {
		// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-get-p-receiver
		get: function (target, key, receiver) {
			// this is an internal hack to identify proxy
			if (key === proxyInstanceMarker) {
				return proxyInstanceMarker;
			}

			const value = Reflect.get(target, key, receiver);

			if (typeof key === "symbol") {
				return value;
			}

			if (isPrimitive(value)) {
				return value;
			}

			if (isElement(value)) {
				return value;
			}

			if (typeof Node !== "undefined" && value instanceof Node) {
				return value;
			}

			// set value as proxy if object or array
			if (isArray(value) || isObject(value)) {
				if (proxy.objectMap.has(value)) {
					return proxy.objectMap.get(value);
				} else if (proxy.proxyMap.has(value)) {
					return value;
				} else {
					const p = new Proxy(value, handler);
					proxy.objectMap.set(value, p);
					proxy.proxyMap.set(p, value);
					return p;
				}
			}

			return value;
		},

		// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-set-p-v-receiver
		set: function (target, key, value, receiver) {
			if (proxy.proxyMap.has(value)) {
				value = proxy.proxyMap.get(value);
			}

			if (proxy.proxyMap.has(target)) {
				target = proxy.proxyMap.get(target);
			}

			let current = Reflect.get(target, key, receiver);
			if (proxy.proxyMap.has(current)) {
				current = proxy.proxyMap.get(current);
			}

			if (current === value) {
				return true;
			}

			const result = Reflect.set(target, key, value, receiver);
			if (result !== true) {
				return result;
			}

			if (typeof key !== "symbol") {
				let next = Reflect.get(target, key, receiver);
				if (proxy.proxyMap.has(next)) {
					next = proxy.proxyMap.get(next);
				}
				if (next !== current) {
					proxy.observers.notify(proxy);
				}
			}

			return result;
		},

		// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-delete-p
		deleteProperty: function (target, key) {
			if (key in target) {
				delete target[key];

				if (typeof key !== "symbol") {
					proxy.observers.notify(proxy);
				}

				return true;
			}
			return false;
		},

		// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-defineownproperty-p-desc
		defineProperty: function (target, key, descriptor) {
			const result = Reflect.defineProperty(target, key, descriptor);
			if (typeof key !== "symbol") {
				proxy.observers.notify(proxy);
			}
			return result;
		},

		// https://262.ecma-international.org/9.0/#sec-proxy-object-internal-methods-and-internal-slots-setprototypeof-v
		setPrototypeOf: function (target, key) {
			const result = Reflect.setPrototypeOf(target, key);

			if (typeof key !== "symbol") {
				proxy.observers.notify(proxy);
			}

			return result;
		},
	};

	return handler;
}
