/**
 * 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 { extend } from "../data/extend.mjs";
import { Base } from "../types/base.mjs";
import { getGlobalObject } from "../types/global.mjs";
import { equipWithInternal } from "../types/internal.mjs";
import { isArray } from "../types/is.mjs";
import { ATTRIBUTE_HREF, ATTRIBUTE_SRC } from "./constants.mjs";
import { Resource } from "./resource.mjs";
import { Data } from "./resource/data.mjs";
import { Stylesheet } from "./resource/link/stylesheet.mjs";
import { Script } from "./resource/script.mjs";

export { ResourceManager };

/**
 * The ResourceManager is a singleton that manages all resources.
 *
 * @fragments /fragments/libraries/dom/resourcemanager/
 *
 * @example /examples/libraries/dom/resourcemanager/simple/ Register and connect multiple resources
 * @example /examples/libraries/dom/resourcemanager/data-url-bundle/ Connect script stylesheet and data resources from inline urls
 * @example /examples/libraries/dom/resourcemanager/separate-registration/ Register resources in separate steps and inspect the internal groups
 * @example /examples/libraries/dom/resourcemanager/availability-check/ Wait for registered resources to report availability
 * @license AGPLv3
 * @since 1.25.0
 * @copyright Volker Schukai
 * @summary A resource coordinator for registering and connecting scripts, stylesheets and data assets.
 */
class ResourceManager extends Base {
	/**
	 *
	 * @param {Object} options
	 * throw {Error} unsupported document type
	 */
	constructor(options) {
		super(options);
		equipWithInternal.call(this);

		if (!(this.getOption("document") instanceof Document)) {
			throw new Error("unsupported document type");
		}
	}

	/**
	 * @deprecated since 3.15.0 use getInternal instead
	 * @property {string} baseurl
	 */
	getOption(key) {
		return this.getInternal(key);
	}

	/**
	 * @property {string} baseurl
	 */
	getBaseURL() {
		this.getOption("document")?.baseURL;
	}

	/**
	 * @property {string} baseurl
	 * @deprecated since 3.15.0 use internalDefaults instead
	 */
	get defaults() {
		return this.internalDefaults;
	}

	/**
	 *
	 * @property {HTMLDocument} document=document Document
	 * @property {Object} resources
	 * @property {Array} resources.scripts=[] array with {@link Monster.DOM.Resource.Script} objects
	 * @property {Array} resources.stylesheets=[] array with {@link Monster.DOM.Resource.Link.Stylesheet} objects
	 * @property {Array} resources.data=[] array with {@link Monster.DOM.Resource.Data} objects
	 */
	get internalDefaults() {
		return Object.assign(
			{},
			{
				document: getGlobalObject("document"),
				resources: {
					scripts: [],
					stylesheets: [],
					data: [],
				},
			},
		);
	}

	/**
	 * Append Tags to DOM
	 *
	 * @return {Monster.DOM.ResourceManager}
	 * @throws {Error} unsupported resource definition
	 */
	connect() {
		runResourceMethod.call(this, "connect");
		return this;
	}

	/**
	 * Check if available
	 *
	 * @return {Promise}
	 * @throws {Error} unsupported resource definition
	 */
	available() {
		return Promise.all(runResourceMethod.call(this, "available"));
	}

	/**
	 * Add a script
	 *
	 * @param {string|URL} url
	 * @param [Object|undefined} options
	 * @return {Monster.DOM.ResourceManager}
	 * @see Monster.DOM.Resource.Script
	 */
	addScript(url, options) {
		return addResource.call(this, "scripts", url, options);
	}

	/**
	 * Add Stylesheet
	 *
	 * @param {string|URL} url
	 * @param [Object|undefined} options
	 * @return {Monster.DOM.ResourceManager}
	 * @see Monster.DOM.Resource.Link.Stylesheet
	 */
	addStylesheet(url, options) {
		return addResource.call(this, "stylesheets", url, options);
	}

	/**
	 * Add Data Tag
	 *
	 * @param {string|URL} url
	 * @param [Object|undefined} options
	 * @return {Monster.DOM.ResourceManager}
	 * @see Monster.DOM.Resource.Data
	 */
	addData(url, options) {
		return addResource.call(this, "data", url, options);
	}
}

/**
 * @private
 * @param {string} method
 * @return {Array}
 */
function runResourceMethod(method) {
	const result = [];

	for (const type of ["scripts", "stylesheets", "data"]) {
		const resources = this.getOption(`resources.${type}`);
		if (!isArray(resources)) {
			continue;
		}

		for (const resource of resources) {
			if (!(resource instanceof Resource)) {
				throw new Error("unsupported resource definition");
			}

			result.push(resource[method]());
		}
	}

	return result;
}

/**
 *
 * @param {string} type
 * @param {string|URL} url
 * @param [Object|undefined} options
 * @return {Monster.DOM.ResourceManager}
 * @private
 */
function addResource(type, url, options) {
	if (url instanceof URL) {
		url = url.toString();
	}

	options = options || {};

	let resource;
	switch (type) {
		case "scripts":
			resource = new Script(extend({}, options, { [ATTRIBUTE_SRC]: url }));
			break;
		case "stylesheets":
			resource = new Stylesheet(extend({}, options, { [ATTRIBUTE_HREF]: url }));
			break;
		case "data":
			resource = new Data(extend({}, options, { [ATTRIBUTE_SRC]: url }));
			break;
		default:
			throw new Error(`unsupported type ${type}`);
	}

	this.getOption("resources")?.[type].push(resource);
	return this;
}
