/**
 * 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 { internalSymbol } from "../constants.mjs";
import { extend } from "../data/extend.mjs";
import { Pathfinder } from "../data/pathfinder.mjs";
import { parseDataURL } from "./dataurl.mjs";
import { isString } from "./is.mjs";
import { Observer } from "./observer.mjs";
import { ProxyObserver } from "./proxyobserver.mjs";
import { validateObject } from "./validate.mjs";
import { isObject } from "./is.mjs";

export { equipWithInternal };

/**
 * @private
 * @type {string}
 */
const propertyName = "internalDefaults";

/**
 * This function extends the given object with the following methods:
 *
 * - attachInternalObserver
 * - detachInternalObserver
 * - containsInternalObserver
 * - setInternal
 * - setInternals
 * - getInternal
 *
 * @license AGPLv3
 * @since 3.15.0
 * @copyright Volker Schukai
 */
function equipWithInternal() {
	validateObject(this);

	if (!hasGetter(this, propertyName)) {
		Object.defineProperty(this, propertyName, {
			get: function () {
				return {};
			},
		});
	}

	const defaults = extend({}, this[propertyName] || {});
	this[internalSymbol] = new ProxyObserver(defaults);

	/**
	 * Attach a new observer
	 *
	 * @param {Observer} observer
	 * @return {ProxyObserver}
	 */
	this["attachInternalObserver"] = (observer) => {
		this[internalSymbol].attachObserver(observer);
		return this;
	};

	/**
	 * Detach a observer
	 *
	 * @param {Observer} observer
	 * @return {ProxyObserver}
	 */
	this["detachInternalObserver"] = (observer) => {
		this[internalSymbol].detachObserver(observer);
		return this;
	};

	/**
	 * Check if a observer is attached
	 *
	 * @param {Observer} observer
	 * @return {boolean}
	 */
	this["containsInternalObserver"] = (observer) => {
		return this[internalSymbol].containsObserver(observer);
	};

	/**
	 * Set an internal value, nested internals can be specified by path `a.b.c`
	 *
	 * @param {string} path
	 * @param {*} value
	 * @return {Datasource}
	 */
	this["setInternal"] = (path, value) => {
		new Pathfinder(this[internalSymbol].getSubject()).setVia(path, value);
		return this;
	};

	/**
	 * set multiple internals at once
	 *
	 * @param {string|object} options
	 * @return {Datasource}
	 * @throws {Error} the options does not contain a valid json definition
	 */
	this["setInternals"] = (options) => {
		if (isString(options)) {
			options = parseOptionsJSON(options);
		}

		extend(this[internalSymbol].getSubject(), defaults, options);
		return this;
	};

	/**
	 * path 'a.b.c can specify nested internals`
	 *
	 * @param {string} path
	 * @param {*} defaultValue
	 * @return {*}
	 */
	this["getInternal"] = (path, defaultValue) => {
		let value;

		try {
			value = new Pathfinder(this[internalSymbol].getRealSubject()).getVia(
				path,
			);
		} catch (e) {}

		if (value === undefined) return defaultValue;
		return value;
	};
}

/**
 * @private
 * @param obj
 * @param prop
 * @return {boolean}
 */
function hasGetter(obj, prop) {
	while (isObject(obj)) {
		if (Object.getOwnPropertyDescriptor(obj, prop)?.["get"]) {
			return true;
		}
		obj = Object.getPrototypeOf(obj);
	}

	return false;
}

/**
 * @private
 * @param data
 * @return {Object}
 */
function parseOptionsJSON(data) {
	let obj = {};

	if (!isString(data)) {
		return obj;
	}

	// the configuration can be specified as a data url.
	try {
		const dataUrl = parseDataURL(data);
		data = dataUrl.content;
	} catch (e) {}

	try {
		obj = JSON.parse(data);
	} catch (e) {
		throw e;
	}

	return validateObject(obj);
}
