const Organisation = require('./Organisation');

class Organisations {

	#values = {}

	#overlord

	static entries (orgs) {
		return orgs.names().map(name => [name, orgs.get(name)]);
	}

	constructor (data, overlord) {
		if (data instanceof Organisations) {
			for (const [name, value] of Organisations.entries(data)) {
				this.set(name, new Organisation(value));
			}
		} else if (data instanceof Object) {
			for (const [name, value] of Object.entries(data)) {
				this.set(name, new Organisation(value));
			}
		} else if (data instanceof String) {
			this.set(data, new Organisation());
		} else if (typeof data !== "undefined") {
			throw new Error("Invalid constructor argument");
		}

		if (overlord) {
			this.#overlord = overlord;
		}
	}

	get values () {
		return this.#values;
	}

	get length () {
		return this.names().length;
	}

	get overlord () {
		return this.#overlord;
	}

	set overlord (v) {
		this.#overlord = new Organisations(v);
	}

	/** Get the operations for `name`
	 */
	get (name) {
		const key = Object.keys(this.values).find( k => k.toLowerCase() == name.toLowerCase() ) ?? name;
		return this.values[key];
	}

	/** Set the operations for `name` to `value`
	 *
	 * If we have an overlord, ensure we cannot:
	 *
	 * 1. Add new organisations which the overlord
	 *    is not a member of
	 * 2. Access operations that the overlord is not
	 *    allowed to access
	 */
	set (name, value) {
		name = String(name).trim();
		const key = Object.keys(this.values).find( k => k.toLowerCase() == name.toLowerCase() ) ?? name;
		const org = new Organisation(value);

		if (this.overlord) {
			const parent = this.overlord.get(key) ?? this.overlord.get("*");
			if (parent) {
				this.values[key] = parent.filter(org);
			}
		} else {
			this.values[key] = new Organisation(value);
		}

		return this;
	}

	/** Enable the operation `op` in all organisations
	 */
	enableOperation (op) {
		if (this.overlord) {
			Object.keys(this.#values)
			.filter( key => (this.overlord.get(key) ?? this.overlord.get("*"))?.[op] )
			.forEach( key => this.#values[key][op] = true );
		} else {
			Object.values(this.#values).forEach( org => org[op] = true );
		}

		return this;
	}

	/** Disable the operation `op` in all organisations
	 */
	disableOperation (op) {
		Object.values(this.#values).forEach( org => org[op] = false );

		return this;
	}

	/** Create a new organisation object limited by the caller's rights
	 *
	 * The spawned Organisations instance will have the same organisations
	 * and rights as the caller minus the applied `mask`. With the default
	 * mask, the spawned object will inherit all rights except for `edit`
	 * rights.
	 *
	 * The "*" organisation must be explicitly assigned. It is not inherited.
	 */
	spawn (mask = {read: true, write: true, edit: false}) {

		const parent = new Organisations();
		const wildcard = this.get("*").edit; // If true, we can spawn everywhere

		this.entries().forEach( ([k, v]) => {
			// if (k != "*") { // This organisation is not inherited
				if (v.edit || wildcard) { // We have the right to spawn in this organisation
					const o = new Organisation({
						read: v.read && mask.read,
						write: v.write && mask.write,
						edit: v.edit && mask.edit
					});
					parent.set(k, o);
				}
			// }
		});

		return new Organisations({}, parent);
	}

	remove (name) {
		const key = Object.keys(this.values).find( k => k.toLowerCase() == name.toLowerCase() ) ?? name;
		delete this.values[key];
	}

	/** Return the list of organisation names
	 */
	names () {
		return Object.keys(this.values);
	}

	/** Same as this.get(name)
	 */
	value (name) {
		return this.values[name];
	}

	/** Same as Object.prototype.entries
	 */
	entries () {
		return this.names().map( name => [ name, this.value(name) ] );
	}

	/** Return true if the named organisation is present
	 */
	has (name) {
		return Boolean(this.value(name));
	}

	/** Return only those of our organisations
	 * and operations present in `other`
	 */
	filter (other) {
		const filteredOrganisations = new Organisations();

		const wildcard = other.value("*");

		for (const [name, org] of this.entries()) {
			const ownOrg = other.value(name) ?? wildcard;
			if (ownOrg) {
				filteredOrganisations.set(name, org.filter(ownOrg))
			}
		}

		return filteredOrganisations;
	}

	/** Return only those organisations
	 * that have access to the required
	 * operation
	 */
	accessToOperation (op) {
		const filteredOrganisations = new Organisations();

		for (const [name, org] of this.entries()) {
			if (org[op]) {
				filteredOrganisations.set(name, org);
			}
		}

		return filteredOrganisations;
	}

	toJSON () {
		const obj = {};
		for (const key in this.values) {
			obj[key] = this.values[key].toJSON();
		}
		return obj;
	}

	toString (replacer, space) {
		return JSON.stringify(this.toJSON(), replacer, space);
	}

	*[Symbol.iterator] () {
		for (const [name, operations] of this.entries()) {
			yield {name, operations};
		}
	}

}


if (typeof module !== 'undefined' && module.exports) {
    module.exports = Organisations; // CJS export
}

// ESM export
if (typeof exports !== 'undefined' && !exports.default) {
    exports.default = Organisations; // ESM export
}
