/**
 * 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 { isIterable, isString } from "./is.mjs";
import { validateFunction, validateString } from "./validate.mjs";
import { Base } from "./base.mjs";

export { TokenList };

/**
 * A `TokenList` allows you to manage tokens (individual character strings such as css classes in an attribute string).
 *
 * The `TokenList` offers various functions to manipulate values. For example, you can add, remove or replace a class in a CSS list.
 *
 * This class implements the [iteration protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols).
 *
 * @license AGPLv3
 * @since 1.2.0
 * @copyright Volker Schukai
 */
class TokenList extends Base {
	/**
	 *
	 * @param {array|string|iteratable} init
	 */
	constructor(init) {
		super();
		this.tokens = new Set();

		if (typeof init !== "undefined") {
			this.add(init);
		}
	}

	/**
	 * Iterator protocol
	 *
	 * @return {Symbol.iterator}
	 */
	getIterator() {
		return this[Symbol.iterator]();
	}

	/**
	 * Iterator
	 *
	 * @return {{next: ((function(): ({value: *, done: boolean}))|*)}}
	 */
	[Symbol.iterator]() {
		// Use a new index for each iterator. This makes multiple
		// iterations over the iterable safe for non-trivial cases,
		// such as use of break or nested looping over the same iterable.
		let index = 0;
		const entries = this.entries();

		return {
			next: () => {
				if (index < entries.length) {
					return { value: entries?.[index++], done: false };
				} else {
					return { done: true };
				}
			},
		};
	}

	/**
	 * Returns true if it contains token, otherwise false
	 *
	 * @externalExample ../../example/types/tokenlist-2.mjs
	 * @param {array|string|iteratable} value
	 * @return {boolean}
	 */
	contains(value) {
		if (isString(value)) {
			value = value.trim();
			let counter = 0;
			value.split(" ").forEach((token) => {
				if (this.tokens.has(token.trim()) === false) return false;
				counter++;
			});
			return counter > 0 ? true : false;
		}

		if (isIterable(value)) {
			let counter = 0;
			for (const token of value) {
				validateString(token);
				if (this.tokens.has(token.trim()) === false) return false;
				counter++;
			}
			return counter > 0 ? true : false;
		}

		return false;
	}

	/**
	 * Add tokens
	 *
	 * @externalExample ../../example/types/tokenlist-3.mjs
	 * @param {array|string|iteratable} value
	 * @return {TokenList}
	 * @throws {TypeError} unsupported value
	 */
	add(value) {
		if (isString(value)) {
			value.split(" ").forEach((token) => {
				this.tokens.add(token.trim());
			});
		} else if (isIterable(value)) {
			for (const token of value) {
				validateString(token);
				this.tokens.add(token.trim());
			}
		} else if (typeof value !== "undefined") {
			throw new TypeError("unsupported value");
		}

		return this;
	}

	/**
	 * remove all tokens
	 *
	 * @return {TokenList}
	 */
	clear() {
		this.tokens.clear();
		return this;
	}

	/**
	 * Removes token
	 *
	 * @externalExample ../../example/types/tokenlist-4.mjs
	 * @param {array|string|iteratable} value
	 * @return {TokenList}
	 * @throws {TypeError} unsupported value
	 */
	remove(value) {
		if (isString(value)) {
			value.split(" ").forEach((token) => {
				this.tokens.delete(token.trim());
			});
		} else if (isIterable(value)) {
			for (const token of value) {
				validateString(token);
				this.tokens.delete(token.trim());
			}
		} else if (typeof value !== "undefined") {
			throw new TypeError("unsupported value", "types/tokenlist.mjs");
		}

		return this;
	}

	/**
	 * this method replaces a token with a new token.
	 *
	 * if the passed token exists, it is replaced with newToken and TokenList is returned.
	 * if the token does not exist, newToken is not set and TokenList is returned.
	 *
	 * @param {string} token
	 * @param {string} newToken
	 * @return {TokenList}
	 */
	replace(token, newToken) {
		validateString(token);
		validateString(newToken);
		if (!this.contains(token)) {
			return this;
		}

		const a = Array.from(this.tokens);
		const i = a.indexOf(token);
		if (i === -1) return this;

		a.splice(i, 1, newToken);
		this.tokens = new Set();
		this.add(a);

		return this;
	}

	/**
	 * Removes token from string. If token doesn't exist it's added.
	 *
	 * @externalExample ../../example/types/tokenlist-5.mjs
	 * @param {array|string|iteratable} value
	 * @return {boolean}
	 * @throws {TypeError} unsupported value
	 */
	toggle(value) {
		if (isString(value)) {
			value.split(" ").forEach((token) => {
				toggleValue.call(this, token);
			});
		} else if (isIterable(value)) {
			for (const token of value) {
				toggleValue.call(this, token);
			}
		} else if (typeof value !== "undefined") {
			throw new TypeError("unsupported value", "types/tokenlist.mjs");
		}

		return this;
	}

	/**
	 * returns an array with all tokens
	 *
	 * @return {array}
	 */
	entries() {
		return Array.from(this.tokens);
	}

	/**
	 * executes the provided function with each value of the set
	 *
	 * @param {function} callback
	 * @return {TokenList}
	 */
	forEach(callback) {
		validateFunction(callback);
		this.tokens.forEach(callback);
		return this;
	}

	/**
	 * returns the individual tokens separated by a blank character
	 *
	 * @return {string}
	 */
	toString() {
		return this.entries().join(" ");
	}
}

/**
 * @private
 * @param token
 * @return {toggleValue}
 * @throws {Error} must be called with TokenList.call
 */
function toggleValue(token) {
	if (!(this instanceof TokenList))
		throw Error("must be called with TokenList.call");
	validateString(token);
	token = token.trim();
	if (this.contains(token)) {
		this.remove(token);
		return this;
	}
	this.add(token);
	return this;
}
