/**
 * Copyright © schukai GmbH 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 schukai GmbH.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

import { diff } from "../../../data/diff.mjs";
import { addAttributeToken } from "../../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs";
import { isArray } from "../../../types/is.mjs";
import { Datasource, dataSourceSymbol } from "../datasource.mjs";
import { DatasourceStyleSheet } from "../stylesheet/datasource.mjs";
import { instanceSymbol } from "../../../constants.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../../dom/customelement.mjs";
import { RestAPI } from "../../../data/datasource/server/restapi.mjs";
import { Formatter } from "../../../text/formatter.mjs";
import { clone } from "../../../util/clone.mjs";
import { validateBoolean } from "../../../types/validate.mjs";
import { findElementWithIdUpwards } from "../../../dom/util.mjs";
import { Observer } from "../../../types/observer.mjs";
import { Pathfinder } from "../../../data/pathfinder.mjs";
import { fireCustomEvent } from "../../../dom/events.mjs";

export { Rest };

/**
 * @private
 * @type {symbol}
 */
const intersectionObserverHandlerSymbol = Symbol("intersectionObserverHandler");

/**
 * @private
 * Original at source/components/datatable/datasource/rest.mjs
 * @type {symbol}
 */
const rawDataSymbol = Symbol.for(
	"@schukai/monster/data/datasource/server/restapi/rawdata",
);

/**
 * @private
 * @type {symbol}
 */
const intersectionObserverObserverSymbol = Symbol(
	"intersectionObserverObserver",
);

/**
 * @private
 * @type {symbol}
 */
const filterObserverSymbol = Symbol("filterObserver");

/**
 * The Datasource component is a basic class for the datatable component.
 *
 * <img src="./images/rest.png">
 *
 * Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library
 *
 * @startuml rest.png
 * skinparam monochrome true
 * skinparam shadowing false
 * HTMLElement <|-- CustomElement
 * CustomElement <|-- Datasource
 * Datasource <|-- Rest
 * @enduml
 *
 * @copyright schukai GmbH
 * @memberOf Monster.Components.Datatable.Datasource
 * @summary A rest api datasource
 */
class Rest extends Datasource {
	/**
	 * the constructor of the class
	 */
	constructor() {
		super();
		this[dataSourceSymbol] = new RestAPI();
	}

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

	/**
     * To set the options via the html tag the attribute `data-monster-options` must be used.
     * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
     *
     * The individual configuration values can be found in the table.
     *
     * @property {Object} templates Template definitions
     * @property {string} templates.main Main template
     * @property {Object} features Feature definitions
     * @property {boolean} features.autoInit If true, the component is initialized automatically
     * @property {boolean} features.filter If true, the component is initialized automatically
     * @property {Object} autoInit Auto init definitions
     * @property {boolean} autoInit.intersectionObserver If true, the intersection observer is initialized automatically
     * @property {boolean} autoInit.oneTime If true, the intersection observer is initialized only once
     * @property {Object} filter Filter definitions
     * @property {string} filter.id The id of the filter control
     * @property {Object} datatable Datatable definitions
     * @property {string} datatable.id The id of the datatable control
     * @property {Object} response Response definitions
     * @property {Object} response.path Path definitions (changed in 3.56.0)
     * @property {string} response.path.message Path to the message (changed in 3.56.0)
     * @property {Object} read Read configuration
     * @property {string} read.url The url of the rest api
     * @property {string} read.method The method of the rest api
     * @property {Object} read.parameters The parameters of the rest api
     * @property {Object} read.parameters.filter The filter of the rest api
     * @property {Object} read.parameters.orderBy The order by of the rest api
     * @property {Object} read.parameters.page The page of the rest api
     * @property {Object} write Write configuration

     */
	get defaults() {
		const restOptions = new RestAPI().defaults;

		restOptions.read.parameters = {
			filter: undefined,
			oderBy: undefined,
			page: "1",
		};

		return Object.assign({}, super.defaults, restOptions, {
			templates: {
				main: getTemplate(),
			},

			features: {
				autoInit: false,
				filter: false,
			},

			autoInit: {
				intersectionObserver: false,
				oneTime: true,
			},

			filter: {
				id: undefined,
			},

			datatable: {
				id: undefined,
			},

			response: {
				path: {
					message: "sys.message",
					code: "sys.code",
				},
			},
		});
	}

	/**
	 *
	 * @param {string} page
	 * @param {string} query
	 * @param {string} orderBy
	 * @returns {Monster.Components.Datatable.Datasource.Rest}
	 */
	setParameters({ page, query, orderBy }) {
		const parameters = this.getOption("read.parameters");
		if (query !== undefined) {
			parameters.query = `${query}`;
			parameters.page = "1";
		}

		// after a query the page is set to 1, so if the page is not set, it is set to 1
		if (page !== undefined) parameters.page = `${page}`;
		if (orderBy !== undefined) parameters.order = `${orderBy}`;
		this.setOption("read.parameters", parameters);
		return this;
	}

	/**
	 * @return {void}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

		initEventHandler.call(this);
		initAutoInit.call(this);
	}

	/**
	 * @deprecated 2023-06-25
	 * @returns {Promise<never>|*}
	 */
	reload() {
		return this.fetch();
	}

	/**
	 * Fetches the data from the rest api
	 * @returns {Promise<never>|*}
	 */
	fetch() {
		const opt = clone(this.getOption("read"));
		this[dataSourceSymbol].setOption("read", opt);

		let url = this.getOption("read.url");
		const formatter = new Formatter(this.getOption("read.parameters"));

		if (!url) {
			return Promise.reject(new Error("No url defined"));
		}

		url = formatter.format(url);

		this[dataSourceSymbol].setOption("read.url", url);

		return new Promise((resolve, reject) => {
			fireCustomEvent(this, "monster-datasource-fetch", {
				datasource: this,
			});

			setTimeout(() => {
				this[dataSourceSymbol]
					.read()
					.then((response) => {
						fireCustomEvent(this, "monster-datasource-fetched", {
							datasource: this,
						});

						resolve(response);
					})
					.catch((error) => {
						fireCustomEvent(this, "monster-datasource-error", {
							error: error,
						});

						addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
						reject(error);
					});
			}, 0);
		});
	}

	/**
	 *
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [DatasourceStyleSheet];
	}

	/**
	 * @private
	 * @return {string}
	 */
	static getTag() {
		return "monster-datasource-rest";
	}

	/**
	 * This method activates the intersection observer manually.
	 * For this purpose, the option `autoInit.intersectionObserver` must be set to `false`.
	 *
	 * @returns {Monster.Components.Datatable.Datasource.Rest}
	 */
	initIntersectionObserver() {
		initIntersectionObserver.call(this);
		return this;
	}

	/**
	 * @private
	 */
	connectedCallback() {
		super.connectedCallback();

		setTimeout(() => {
			if (this.getOption("features.filter", false) === true) {
				initFilter.call(this);
			}
		}, 0);
	}

	/**
	 * @private
	 */
	disconnectedCallback() {
		super.disconnectedCallback();
		removeFilter.call(this);
	}

	/**
	 * @returns {Promise<never>|*}
	 */
	read() {
		return this.fetch();
	}

	/**
	 * Fetches the data from the rest api
	 * @returns {Promise<never>|*}
	 */
	write() {
		const opt = clone(this.getOption("write"));
		this[dataSourceSymbol].setOption("write", opt);

		let url = this.getOption("write.url");
		const formatter = new Formatter(this.getOption("write.parameters"));

		if (!url) {
			return Promise.reject(new Error("No url defined"));
		}

		url = formatter.format(url);

		this[dataSourceSymbol].setOption("write.url", url);

		return new Promise((resolve, reject) => {
			fireCustomEvent(this, "monster-datasource-fetch", {
				datasource: this,
			});

			setTimeout(() => {
				this[dataSourceSymbol]
					.write()
					.then((response) => {
						fireCustomEvent(this, "monster-datasource-fetched", {
							datasource: this,
						});

						resolve(response);
					})
					.catch((error) => {
						fireCustomEvent(this, "monster-datasource-error", {
							error: error,
						});

						addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
						reject(error);
					});
			}, 0);
		});
	}
}

/**
 * @private
 */
function removeFilter() {
	const filterID = this.getOption("filter.id", undefined);
	if (!filterID) return;

	const filterControl = findElementWithIdUpwards(this, filterID);

	if (filterControl && this[filterObserverSymbol]) {
		filterControl?.detachObserver(this[filterObserverSymbol]);
	}
}

/**
 * @private
 */
function initFilter() {
	const filterID = this.getOption("filter.id", undefined);

	if (!filterID)
		throw new Error("filter feature is enabled but no filter id is defined");

	const filterControl = findElementWithIdUpwards(this, filterID);
	if (!filterControl)
		throw new Error(
			"filter feature is enabled but no filter control with id " +
				filterID +
				" is found",
		);

	this[filterObserverSymbol] = new Observer(() => {
		const query = filterControl.getOption("query");
		if (query === undefined) {
			return;
		}
		this.setParameters({ query: query });
		this.fetch()
			.then((response) => {
				if (!(response instanceof Response)) {
					throw new Error("Response is not an instance of Response");
				}

				if (response?.ok === true) {
					this.dispatchEvent(new CustomEvent("reload", { bubbles: true }));
					filterControl?.showSuccess();
				}

				if (response.bodyUsed === true) {
					return handleIntersectionObserver.call(
						this,
						response[rawDataSymbol],
						response,
						filterControl,
					);
				}

				response
					.text()
					.then((jsonAsText) => {
						let json;
						try {
							json = JSON.parse(jsonAsText);
						} catch (e) {
							const message = e instanceof Error ? e.message : `${e}`;
							filterControl?.showFailureMessage(message);
							return Promise.reject(e);
						}

						return handleIntersectionObserver.call(
							this,
							json,
							response,
							filterControl,
						);
					})
					.catch((e) => {
						filterControl?.showFailureMessage(e.message);
					});
			})
			.catch((e) => {
				this.dispatchEvent(
					new CustomEvent("error", { bubbles: true, detail: e }),
				);

				if (!(e instanceof Error)) {
					e = new Error(e);
				}

				filterControl?.showFailureMessage(e.message);
				return Promise.reject(e);
			});
	});

	filterControl.attachObserver(this[filterObserverSymbol]);
}

function handleIntersectionObserver(json, response, filterControl) {
	const path = new Pathfinder(json);

	const codePath = this.getOption("response.path.code");

	if (path.exists(codePath)) {
		const code = `${path.getVia(codePath)}`;
		if (code && code === "200") {
			filterControl?.showSuccess();
			return Promise.resolve(response);
		}

		const messagePath = this.getOption("response.path.message");
		if (path.exists(messagePath)) {
			const message = path.getVia(messagePath);
			filterControl?.showFailureMessage(message);
			return Promise.reject(new Error(message));
		}

		return Promise.reject(new Error("Response code is not 200"));
	}
}

/**
 * @private
 */
function initAutoInit() {
	const autoInit = this.getOption("features.autoInit");
	validateBoolean(autoInit);

	if (autoInit !== true) return;

	if (this.getOption("autoInit.intersectionObserver") === true) {
		initIntersectionObserver.call(this);
		return;
	}

	setTimeout(() => {
		this.fetch().catch(() => {});
	}, 0);
}

function initEventHandler() {
	this[intersectionObserverHandlerSymbol] = (entries) => {
		entries.forEach((entry) => {
			if (entry.isIntersecting) {
				if (entry.intersectionRatio > 0) {
					this.fetch();
				}

				// only load once
				if (
					this.getOption("autoInit.oneTime") === true &&
					this[intersectionObserverObserverSymbol] !== undefined
				) {
					this[intersectionObserverObserverSymbol].unobserve(this);
				}
			}
		});
	};
}

function initIntersectionObserver() {
	this.classList.add("intersection-observer");

	const options = {
		root: null,
		rootMargin: "0px",
		threshold: 0.1,
	};

	this[intersectionObserverObserverSymbol] = new IntersectionObserver(
		this[intersectionObserverHandlerSymbol],
		options,
	);
	this[intersectionObserverObserverSymbol].observe(this);
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <slot></slot>`;
}

registerCustomElement(Rest);