/**
 * 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 { instanceSymbol } from "../../constants.mjs";
import { isArray, isFunction, isObject } from "../../types/is.mjs";
import { Datasource } from "../datasource.mjs";
import { diff } from "../diff.mjs";
import { Pathfinder } from "../pathfinder.mjs";
import { Pipe } from "../pipe.mjs";

export { Server };

/**
 * @private
 * @type {symbol}
 */
const serverVersionSymbol = Symbol("serverVersion");

/**
 * Base class for all server data sources
 *
 * @fragments /fragments/libraries/data/datasource/server/
 *
 * @example /examples/libraries/data/datasource/server/simple/ Transform and prepare server payloads
 * @example /examples/libraries/data/datasource/server/transformer-callbacks/ Combine transformer expressions with mapping callbacks
 * @example /examples/libraries/data/datasource/server/partial-diff/ Reduce write payloads through the partial diff callback
 * @example /examples/libraries/data/datasource/server/sheathing-object/ Wrap outgoing payloads into a server-side envelope
 * @license AGPLv3
 * @since 3.4.0
 * @copyright Volker Schukai
 * @summary A base datasource layer for reading, transforming and writing structured server payloads.
 */
class Server extends Datasource {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/data/datasource/server");
	}

	/**
	 * This prepares the data that comes from the server.
	 * Should not be called directly.
	 *
	 * @private
	 * @param {Object} payload
	 * @return {Object}
	 */
	transformServerPayload(payload) {
		payload = doTransform.call(this, "read", payload);
		this[serverVersionSymbol] = payload;

		const dataPath = this.getOption("read.path");
		if (dataPath) {
			payload = new Pathfinder(payload).getVia(dataPath);
		}

		return payload;
	}

	/**
	 * This prepares the data for writing and should not be called directly.
	 *
	 * @private
	 * @param {Object} payload
	 * @return {Object}
	 */
	prepareServerPayload(payload) {
		payload = doTransform.call(this, "write", payload);
		payload = doDiff.call(this, payload);

		const sheathingObject = this.getOption("write.sheathing.object");
		const sheathingPath = this.getOption("write.sheathing.path");

		if (sheathingObject && sheathingPath) {
			const sub = payload;
			payload = sheathingObject;
			new Pathfinder(payload).setVia(sheathingPath, sub);
		}

		return payload;
	}
}

/**
 *
 * @param obj
 * @return {*}
 */
function doDiff(obj) {
	if (
		this[serverVersionSymbol] === null ||
		this[serverVersionSymbol] === undefined
	) {
		return obj;
	}

	const callback = this.getOption("write.partial.callback");
	if (!isFunction(callback)) {
		return obj;
	}

	const results = diff(this[serverVersionSymbol], obj);
	if (!results) {
		return obj;
	}

	obj = callback(obj, results);
	this[serverVersionSymbol] = obj;

	return obj;
}

/**
 * @private
 * @param {string} type
 * @param {Object} obj
 * @return {Object}
 */
function doTransform(type, obj) {
	const transformation = this.getOption(`${type}.mapping.transformer`);

	if (transformation !== undefined && transformation !== null) {
		const pipe = new Pipe(transformation);
		const callbacks = this.getOption(`${type}.mapping.callbacks`);

		if (isArray(callbacks)) {
			for (const callback of callbacks) {
				if (typeof callback === "function") {
					pipe.setCallback(callback);
				}
			}
		}

		if (isObject(callbacks)) {
			for (const key in callbacks) {
				if (
					callbacks.hasOwnProperty(key) &&
					typeof callbacks[key] === "function"
				) {
					pipe.setCallback(key, callbacks[key]);
				}
			}
		}

		obj = pipe.run(obj);
	}

	return obj;
}
