/**
 * 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 { instanceSymbol } from "../constants.mjs";
import { Base } from "../types/base.mjs";
import { parseDataURL } from "../types/dataurl.mjs";
import { isString } from "../types/is.mjs";
import { ProxyObserver } from "../types/proxyobserver.mjs";
import { validateObject } from "../types/validate.mjs";
import { diff } from "./diff.mjs";
import { extend } from "./extend.mjs";
import { Pathfinder } from "./pathfinder.mjs";

export { Datasource };

/**
 * This callback can be passed to a datasource and is used to adapt data structures.
 *
 * @callback Monster.Data.Datasource~exampleCallback
 * @param {*} value Value
 * @param {string} key  Key
 * @see Monster.Data.Datasource
 */

/**
 * @private
 * @type {symbol}
 * @license AGPLv3
 * @since 1.24.0
 */
const internalDataSymbol = Symbol.for(
	"@schukai/monster/data/datasource/@@data",
);

/**
 * The datasource class is the basis for dealing with different data sources.
 * It provides a unified interface for accessing data
 * @externalExample ../../example/data/datasource.mjs
 * @fragments /fragments/libraries/data/datasource/
 * @example /examples/libraries/data/datasource/basic-read-write/ Implement a small in memory datasource with read and write
 * @example /examples/libraries/data/datasource/observer-log/ Attach observers and react to subject changes
 * @example /examples/libraries/data/datasource/skip-noop-events/ Compare skipNoopEvents with repeated writes
 * @example /examples/libraries/data/datasource/render-list/ Drive a small UI list from datasource updates
 * @example /examples/libraries/data/datasource/async-options/ Use datasource options to control async reads
 * @license AGPLv3
 * @since 1.22.0
 * @copyright Volker Schukai
 * @summary A datasource abstraction for reading, observing and writing structured application data.
 */
class Datasource extends Base {
	/**
	 * creates a new datasource
	 *
	 */
	constructor() {
		super();

		this[internalSymbol] = new ProxyObserver({
			options: extend({}, this.defaults),
		});

		this[internalDataSymbol] = new ProxyObserver({});
	}

	/**
	 * attach a new observer
	 *
	 * @param {Observer} observer
	 * @return {Datasource}
	 */
	attachObserver(observer) {
		this[internalDataSymbol].attachObserver(observer);
		return this;
	}

	/**
	 * detach a observer
	 *
	 * @param {Observer} observer
	 * @return {Datasource}
	 */
	detachObserver(observer) {
		this[internalDataSymbol].detachObserver(observer);
		return this;
	}

	/**
	 * @param {Observer} observer
	 * @return {boolean}
	 */
	containsObserver(observer) {
		return this[internalDataSymbol].containsObserver(observer);
	}

	/**
	 * Derived classes can override and extend this method as follows.
	 *
	 * ```
	 * get defaults() {
	 *    return Object.assign({}, super.defaults, {
	 *        myValue:true
	 *    });
	 * }
	 * ```
	 */
	get defaults() {
		return {
			features: {
				// Feature flag: default-on no-op event suppression, can be rolled back.
				skipNoopEvents: true,
			},
		};
	}

	/**
	 * Set option
	 *
	 * @param {string} path
	 * @param {*} value
	 * @return {Datasource}
	 */
	setOption(path, value) {
		new Pathfinder(this[internalSymbol].getSubject()["options"]).setVia(
			path,
			value,
		);
		return this;
	}

	/**
	 * @param {string|object} options
	 * @return {Datasource}
	 * @throws {Error} the options does not contain a valid json definition
	 */
	setOptions(options) {
		if (isString(options)) {
			options = parseOptionsJSON(options);
		}
		extend(
			this[internalSymbol].getSubject()["options"],
			this.defaults,
			options,
		);

		return this;
	}

	/**
	 * nested options can be specified by path `a.b.c`
	 *
	 * @param {string} path
	 * @param {*} defaultValue
	 * @return {*}
	 */
	getOption(path, defaultValue) {
		let value;

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

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

	/**
	 * @throws {Error} this method must be implemented by derived classes.
	 * @return {Promise}
	 */
	read() {
		throw new Error("this method must be implemented by derived classes");
	}

	/**
	 * @throws {Error} this method must be implemented by derived classes.
	 * @return {Promise}
	 */
	write() {
		throw new Error("this method must be implemented by derived classes");
	}

	/**
	 * Returns real object
	 *
	 * @return {Object|Array}
	 */
	get() {
		return this[internalDataSymbol].getRealSubject();
	}

	/**
	 * @param {Object|Array} data
	 * @return {Datasource}
	 */
	set(data) {
		if (this.getOption("features.skipNoopEvents") === true) {
			const current = this[internalDataSymbol].getRealSubject();
			if (diff(current, data).length === 0) {
				return this;
			}
		}
		this[internalDataSymbol].setSubject(data);
		return this;
	}

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

/**
 * @private
 * @param {String} data
 * @return {Object}
 * @throws {Error} the options does not contain a valid json definition
 */
function parseOptionsJSON(data) {
	if (isString(data)) {
		// the configuration can be specified as a data url.
		try {
			const dataUrl = parseDataURL(data);
			data = dataUrl.content;
		} catch (e) {}

		try {
			const obj = JSON.parse(data);
			validateObject(obj);
			return obj;
		} catch (e) {
			throw new Error(
				`the options does not contain a valid json definition (actual: ${data}).`,
			);
		}
	}

	return {};
}
