import { __esDecorate, __runInitializers } from "tslib";
import { DomainModelKind, DomainNamespaceKind } from '../models/kinds.js';
import { DomainElement } from './DomainElement.js';
import { nanoid } from '../nanoid.js';
import { Thing } from '../models/Thing.js';
import { observed, retargetChange } from '../decorators/observed.js';
import { DomainModel } from './DomainModel.js';
import { RemoveNamespaceException } from '../exceptions/remove_namespace_exception.js';
import { RemoveModelException } from '../exceptions/remove_model_exception.js';
import { removeGraphNode } from './GraphUtils.js';
import { DetachException } from '../exceptions/detach_exception.js';
import { AttachException } from '../exceptions/attach_exception.js';
/**
 * The domain namespace is a way to create a logical structure around data models.
 * It groups data models that represent a whole schema, like a Product data namespace
 * can have data models that describe: the product data model, price history data model,
 * product location, etc.
 *
 * It is optional to have a namespace, but it is recommended to have one for complex
 * data domains.
 *
 * A data domain can be a child of another data domain. It can also contain multiple
 * data domains and data models.
 */
let DomainNamespace = (() => {
    let _classSuper = DomainElement;
    let _info_decorators;
    let _info_initializers = [];
    let _info_extraInitializers = [];
    let _deprecated_decorators;
    let _deprecated_initializers = [];
    let _deprecated_extraInitializers = [];
    return class DomainNamespace extends _classSuper {
        static {
            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
            _info_decorators = [retargetChange()];
            _deprecated_decorators = [observed()];
            __esDecorate(this, null, _info_decorators, { kind: "accessor", name: "info", static: false, private: false, access: { has: obj => "info" in obj, get: obj => obj.info, set: (obj, value) => { obj.info = value; } }, metadata: _metadata }, _info_initializers, _info_extraInitializers);
            __esDecorate(this, null, _deprecated_decorators, { kind: "accessor", name: "deprecated", static: false, private: false, access: { has: obj => "deprecated" in obj, get: obj => obj.deprecated, set: (obj, value) => { obj.deprecated = value; } }, metadata: _metadata }, _deprecated_initializers, _deprecated_extraInitializers);
            if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
        }
        kind;
        #info_accessor_storage = __runInitializers(this, _info_initializers, void 0);
        /**
         * The description of the domain namespace.
         */
        get info() { return this.#info_accessor_storage; }
        set info(value) { this.#info_accessor_storage = value; }
        #fields_accessor_storage = __runInitializers(this, _info_extraInitializers);
        /**
         * The ordered list of fields (namespace and models) in the schema.
         * These only keep references to define the order of these properties
         * in the schema as graph won't do it.
         */
        get fields() { return this.#fields_accessor_storage; }
        set fields(value) { this.#fields_accessor_storage = value; }
        #deprecated_accessor_storage = __runInitializers(this, _deprecated_initializers, void 0);
        /**
         * Whether this namespace is deprecated.
         */
        get deprecated() { return this.#deprecated_accessor_storage; }
        set deprecated(value) { this.#deprecated_accessor_storage = value; }
        /**
         * Creates a full namespace schema with defaults.
         * @param input The partial namespace schema.
         * @returns The namespace schema.
         */
        static createSchema(input = {}) {
            const { key = nanoid(), fields, deprecated } = input;
            const info = Thing.fromJSON(input.info, { name: 'New namespace' }).toJSON();
            const result = {
                kind: DomainNamespaceKind,
                key,
                info,
            };
            if (Array.isArray(fields)) {
                result.fields = [...fields];
            }
            if (typeof deprecated === 'boolean') {
                result.deprecated = deprecated;
            }
            return result;
        }
        /**
         * Creates a new namespace instance.
         * It does not add it to the graph.
         * You need to call the `addNamespace` method of the graph to add it.
         *
         * @param root A reference to the root DataDomain instance.
         * @param input The partial namespace schema.
         * @returns Created namespace instance.
         */
        constructor(root, input) {
            const init = DomainNamespace.createSchema(input);
            super(root, init.key);
            __runInitializers(this, _deprecated_extraInitializers);
            this.kind = DomainNamespaceKind;
            this.info = new Thing(init.info);
            if (Array.isArray(init.fields)) {
                this.fields = [...init.fields];
            }
            else {
                this.fields = [];
            }
            if (typeof init.deprecated === 'boolean') {
                this.deprecated = init.deprecated;
            }
            else {
                this.deprecated = undefined;
            }
        }
        toJSON() {
            const result = {
                kind: this.kind,
                key: this.key,
                info: this.info.toJSON(),
            };
            if (Array.isArray(this.fields) && this.fields.length) {
                result.fields = [...this.fields];
            }
            if (typeof this.deprecated === 'boolean') {
                result.deprecated = this.deprecated;
            }
            return result;
        }
        /**
         * Adds a child namespace to the current namespace.
         *
         * @param value The partial namespace schema.
         * @returns The created namespace instance.
         * @example
         * ```typescript
         * const property = namespace.addNamespace({
         *   key: 'e-commerce'
         * })
         * ```
         */
        addNamespace(value) {
            if (value && value.key && this.root.graph.hasNode(value.key)) {
                throw new Error(`Trying to add a namespace to ${this.key}, but it already exists`);
            }
            const instance = new DomainNamespace(this.root, value);
            this.root.graph.setNode(instance.key, instance);
            this.root.graph.setParent(instance.key, this.key);
            this.fields.push({
                type: 'namespace',
                key: instance.key,
            });
            this.root.notifyChange();
            return instance;
        }
        /**
         * Removes a namespace from the current namespace.
         *
         * @param key The key of the namespace to remove.
         * @throws {RemoveNamespaceException} If the namespace does not exist.
         * @example
         * ```typescript
         * namespace.removeNamespace('e-commerce')
         * ```
         */
        removeNamespace(key) {
            if (!this.root.graph.hasNode(key)) {
                throw new RemoveNamespaceException(`Trying to remove the ${key} namespace, but it doesn't exist`);
            }
            const node = this.root.graph.node(key);
            if (!node || node.kind !== DomainNamespaceKind) {
                throw new RemoveNamespaceException(`Trying to remove the ${key} namespace, but it is not a namespace`);
            }
            removeGraphNode(this.root.graph, key);
            this.removeField(key);
            this.root.notifyChange();
            return this;
        }
        /**
         * Attaches a namespace to the current namespace. It changes the parent of the namespace
         * to the current namespace in the graph.
         *
         * The attached namespace must be already a child of another namespace.
         *
         * @param key The key of the namespace to attach.
         * @throws {Error} If the namespace does not exist.
         * @example
         * ```typescript
         * namespace.attachNamespace('e-commerce')
         * ```
         */
        attachNamespace(key) {
            if (!this.root.graph.hasNode(key)) {
                throw new Error(`Trying to attach the ${key} namespace, but it doesn't exist`);
            }
            const node = this.root.graph.node(key);
            if (!node || node.kind !== DomainNamespaceKind) {
                throw new Error(`Trying to attach the ${key} namespace, but it is not a namespace`);
            }
            if (node.domain.key !== this.root.key) {
                throw new Error(`Trying to attach the ${key} namespace, but it is not in the same domain`);
            }
            const parent = node.getParentInstance();
            if (parent === this) {
                throw new AttachException(`Trying to attach the ${key} entity, but it is already a child of this model`);
            }
            if (this.root !== parent) {
                ;
                parent.detachNamespace(key);
            }
            this.fields.push({
                type: 'namespace',
                key: key,
            });
            this.root.graph.setParent(key, this.key);
            this.root.notifyChange();
            return this;
        }
        /**
         * Detaches a namespace from the current namespace. It changes the parent of the namespace
         * to the root graph.
         * Unlike remove, it doesn't remove the namespace from the graph nor notifies the change.
         * @param key The key of the namespace to detach.
         * @throws {RemoveNamespaceException} If the namespace does not exist.
         */
        detachNamespace(key) {
            const index = this.fields.findIndex((item) => item.key === key);
            if (index === -1) {
                throw new RemoveNamespaceException(`Trying to detach the ${key} namespace, but it doesn't exist`);
            }
            const node = this.root.graph.node(key);
            if (!node || node.kind !== DomainNamespaceKind) {
                throw new RemoveNamespaceException(`Trying to detach the ${key} namespace, but it is not a namespace`);
            }
            this.fields.splice(index, 1);
            this.root.graph.setParent(key);
            return this;
        }
        /**
         * Lists all namespaces of this namespaces.
         *
         * Note, it accounts for the order of the namespaces as
         * defined in the `fields` array.
         *
         * @returns A generator that yields each `DomainNamespace`.
         * @example
         * ```typescript
         * for (const ns of namespace.listNamespaces()) {
         *   console.log(ns.key);
         * }
         * ```
         */
        *listNamespaces() {
            for (const { key, type } of this.fields) {
                if (type !== 'namespace') {
                    continue;
                }
                const node = this.root.graph.node(key);
                if (!node || node.kind !== DomainNamespaceKind) {
                    continue;
                }
                yield node;
            }
        }
        /**
         * Checks if this namespace has any namespaces.
         *
         * @returns True if the namespace has namespaces.
         * @example
         * ```typescript
         * if (namespace.hasNamespaces()) {
         *   // ...
         * }
         * ```
         */
        hasNamespaces() {
            return this.fields.some((item) => item.type === 'namespace');
        }
        /**
         * Adds a data model to the current namespace.
         * @param value The partial data model schema.
         * @returns The created data model instance.
         * @example
         * ```typescript
         * const property = namespace.addModel({
         *   key: 'inventory'
         * })
         * ```
         */
        addModel(input) {
            if (input && input.key && this.root.graph.hasNode(input.key)) {
                throw new Error(`Trying to add a model to ${this.key}, but it already exists`);
            }
            const item = new DomainModel(this.root, input);
            this.root.graph.setNode(item.key, item);
            this.root.graph.setParent(item.key, this.key);
            this.fields.push({
                type: 'model',
                key: item.key,
            });
            this.root.notifyChange();
            return item;
        }
        /**
         * Removes a data model from the current namespace.
         *
         * @param key The key of the data model to remove.
         * @throws {RemoveModelException} If the data model does not exist.
         * @example
         * ```typescript
         * namespace.removeModel('inventory')
         * ```
         */
        removeModel(key) {
            if (!this.root.graph.hasNode(key)) {
                throw new RemoveModelException(`Trying to remove the ${key} model, but it doesn't exist`);
            }
            const node = this.root.graph.node(key);
            if (!node || node.kind !== DomainModelKind) {
                throw new RemoveModelException(`Trying to remove the ${key} model, but it is not a model`);
            }
            removeGraphNode(this.root.graph, key);
            this.removeField(key);
            this.root.notifyChange();
            return this;
        }
        /**
         * Attaches a model to the current namespace and sets its parent to the current namespace.
         *
         * The attached model is already a child of another namespace.
         * This method only adds this model to its internal list of fields.
         *
         * Unlike `addModel`, it doesn't create a new model or add it to the graph.
         * It only adds the model to the list of fields.
         *
         * @param key The key of the model to attach.
         * @throws {AttachException} If the model does not exist.
         * @throws {AttachException} If the model is not a model.
         * @throws {AttachException} If the model is not in the same domain.
         * @throws {AttachException} If the model is already a child of this namespace.
         * @example
         * ```typescript
         * namespace.attachModel('inventory')
         * ```
         */
        attachModel(key) {
            if (!this.root.graph.hasNode(key)) {
                throw new AttachException(`Trying to attach the ${key} model, but it doesn't exist`);
            }
            const node = this.root.graph.node(key);
            if (!node || node.kind !== DomainModelKind) {
                throw new AttachException(`Trying to attach the ${key} model, but it is not a model`);
            }
            if (node.domain.key !== this.root.key) {
                throw new AttachException(`Trying to attach the ${key} model, but it is not in the same domain`);
            }
            const parent = node.getParentInstance();
            if (parent === this) {
                throw new AttachException(`Trying to attach the ${key} model, but it is already a child of this namespace`);
            }
            if (this.root !== parent) {
                ;
                parent.detachModel(key);
            }
            this.fields.push({
                type: 'model',
                key: key,
            });
            this.root.graph.setParent(key, this.key);
            this.root.notifyChange();
            return this;
        }
        /**
         * Detaches a model from the current namespace and sets its parent to the root graph.
         * Unlike remove, it doesn't remove the model from the graph nor notifies the change.
         * @param key The key of the model to detach.
         * @throws {DetachException} If the model does not exist.
         * @throws {DetachException} If the model is not a model.
         * @throws {DetachException} If the model is not a child of this namespace.
         */
        detachModel(key) {
            const index = this.fields.findIndex((item) => item.key === key);
            if (index === -1) {
                throw new DetachException(`Trying to detach the ${key} model, but it doesn't exist`);
            }
            const node = this.root.graph.node(key);
            if (!node || node.kind !== DomainModelKind) {
                throw new DetachException(`Trying to detach the ${key} model, but it is not a model`);
            }
            if (node.getParentInstance() !== this) {
                throw new DetachException(`Trying to detach the ${key} model, but it is not a child of this namespace`);
            }
            this.fields.splice(index, 1);
            this.root.graph.setParent(key);
            return this;
        }
        /**
         * Lists all models of this namespaces.
         *
         * Note, it accounts for the order of the models as
         * defined in the `fields` array.
         *
         * @returns A generator that yields each `DomainModel`.
         * @example
         * ```typescript
         * for (const ns of namespace.listModels()) {
         *   console.log(ns.key);
         * }
         * ```
         */
        *listModels() {
            for (const { key, type } of this.fields) {
                if (type !== 'model') {
                    continue;
                }
                const node = this.root.graph.node(key);
                if (!node || node.kind !== DomainModelKind) {
                    continue;
                }
                yield node;
            }
        }
        /**
         * Checks if this namespace has any models.
         *
         * @returns True if the namespace has models.
         * @example
         * ```typescript
         * if (namespace.hasModels()) {
         *   // ...
         * }
         * ```
         */
        hasModels() {
            return this.fields.some((item) => item.type === 'model');
        }
        /**
         * Returns a parent data namespace or the data domain where this data namespace exist.
         */
        getParentInstance() {
            let parentKey;
            for (const parent of this.root.graph.parents(this.key)) {
                parentKey = parent;
                break;
            }
            if (!parentKey) {
                return this.root;
            }
            return this.root.graph.node(parentKey);
        }
        /**
         * Lists all fields (namespace and models) of this namespace.
         *
         * @returns A generator that yields each `DomainAssociation` or `DomainProperty`.
         * @example
         * ```typescript
         * for (const field of namespace.listFields()) {
         *   console.log(field.key);
         * }
         * ```
         */
        *listFields() {
            for (const { key } of this.fields) {
                const node = this.root.graph.node(key);
                if (!node) {
                    continue;
                }
                if (node.kind === DomainNamespaceKind || node.kind === DomainModelKind) {
                    yield node;
                }
            }
        }
        /**
         * Checks if this namespace has any fields (namespace and models).
         *
         * @returns True if the namespace has fields.
         * @example
         * ```typescript
         * if (namespace.hasFields()) {
         *   // ...
         * }
         * ```
         */
        hasFields() {
            return this.fields.length > 0;
        }
        removeField(key) {
            this.fields = this.fields.filter((item) => item.key !== key);
        }
        /**
         * Checks whether the namespace is a child of the given namespace.
         * The relationship doesn't have to be direct, as long as the namespace is in the hierarchy it will return true.
         *
         * @param key The key of the parent namespace to check.
         * @returns True if this namespace is a child of the given namespace.
         */
        isChildOf(key) {
            if (this.key === key) {
                return false;
            }
            const parent = this.getParentInstance();
            if (!parent) {
                return false;
            }
            if (parent.key === key) {
                return true;
            }
            if (parent === this.root) {
                return false;
            }
            return parent.isChildOf(key);
        }
    };
})();
export { DomainNamespace };
//# sourceMappingURL=DomainNamespace.js.map