import { __esDecorate, __runInitializers } from "tslib";
import { DomainEntityKind, DomainModelKind } 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 { DomainEntity } from './DomainEntity.js';
import { RemoveEntityException } from '../exceptions/remove_entity_exception.js';
import { removeGraphNode } from './GraphUtils.js';
import { AttachException } from '../exceptions/attach_exception.js';
import { DetachException } from '../exceptions/detach_exception.js';
/**
 * Represents a data model within a data domain.
 *
 * A `DomainModel` groups related `DomainEntity` instances, providing a logical structure for organizing
 * data. It acts as a container for entities that share a common purpose or context.
 *
 * **Key Features:**
 *
 * - **Entity Container:** Holds a collection of `DomainEntity` instances.
 * - **Logical Grouping:** Organizes entities into meaningful groups.
 * - **Metadata:** Supports a description for the data model.
 * - **Namespace Integration:**  Belongs to a `DomainNamespace`, creating a hierarchical structure.
 *
 * **Usage:**
 *
 * The preferred way to create a `DomainModel` is through
 * the `DomainNamespace.addModel()` or
 * `DataDomain.addModel()` method.
 *
 * **Example:**
 *
 * ```typescript
 * const dataDomain = new DataDomain();
 * const ns = dataDomain.addNamespace({
 *   key: 'myNamespace',
 * });
 * const userModel = ns.addModel({
 *   key: 'userModel',
 *   info: { name: 'User Model' },
 * });
 * ```
 */
let DomainModel = (() => {
    let _classSuper = DomainElement;
    let _info_decorators;
    let _info_initializers = [];
    let _info_extraInitializers = [];
    let _deprecated_decorators;
    let _deprecated_initializers = [];
    let _deprecated_extraInitializers = [];
    return class DomainModel 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 model.
         */
        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 entities in the schema.
         */
        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 model is deprecated.
         */
        get deprecated() { return this.#deprecated_accessor_storage; }
        set deprecated(value) { this.#deprecated_accessor_storage = value; }
        /**
         * Creates a full data model schema with defaults.
         * @param input The partial data model schema.
         * @returns The data model schema.
         */
        static createSchema(input = {}) {
            const { key = nanoid(), fields, deprecated } = input;
            const info = Thing.fromJSON(input.info, { name: 'New model' }).toJSON();
            const result = {
                kind: DomainModelKind,
                key,
                info,
            };
            if (Array.isArray(fields)) {
                result.fields = [...fields];
            }
            if (typeof deprecated === 'boolean') {
                result.deprecated = deprecated;
            }
            return result;
        }
        /**
         * Creates a new model instance.
         *
         * You should use the `DomainNamespace.addModel()` or
         * `DataDomain.addModel()` method instead.
         *
         * @param root A reference to the root DataDomain instance.
         * @param input The partial model schema.
         */
        constructor(root, input) {
            const init = DomainModel.createSchema(input);
            super(root, init.key);
            __runInitializers(this, _deprecated_extraInitializers);
            this.kind = DomainModelKind;
            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;
            }
        }
        /**
         * Generates a JSON representation of the data model.
         *
         * @returns A plain object representation of the
         *   `DomainModel`.
         * @example
         * ```typescript
         * const json = userModel.toJSON();
         * console.log(json);
         * ```
         */
        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;
        }
        /**
         * Returns the parent namespace where this data model
         * exists.
         *
         * @returns The parent `DomainNamespace`.
         * @throws Error When no parent is found.
         * @example
         * ```typescript
         * const parent = userModel.getParentInstance();
         * console.log(parent.key);
         * ```
         */
        getParentInstance() {
            const parentKey = this.root.graph.parent(this.key);
            if (!parentKey) {
                return this.root;
            }
            return this.root.graph.node(parentKey);
        }
        /**
         * Adds a child entity to the current model.
         *
         * @param value The partial entity schema.
         * @returns The created entity instance.
         * @example
         * ```typescript
         * const entity = model.addEntity({
         *   key: 'address',
         *   info: { name: 'Address' },
         * })
         * ```
         */
        addEntity(input) {
            if (input && input.key && this.root.graph.hasNode(input.key)) {
                throw new Error(`Entity with key ${input?.key} already exists`);
            }
            const item = new DomainEntity(this.root, input);
            this.root.graph.setNode(item.key, item);
            this.root.graph.setParent(item.key, this.key);
            this.fields.push({
                key: item.key,
                type: 'entity',
            });
            this.root.notifyChange();
            return item;
        }
        /**
         * Removes an entity from the current model.
         *
         * @param key The key of the model to remove.
         * @throws {RemoveEntityException} If the model does not exist.
         * @example
         * ```typescript
         * model.removeEntity('address')
         * ```
         */
        removeEntity(key) {
            if (!this.root.graph.hasNode(key)) {
                throw new RemoveEntityException(`Trying to remove the ${key} entity, but it doesn't exist`);
            }
            const node = this.root.graph.node(key);
            if (!node || node.kind !== DomainEntityKind) {
                throw new RemoveEntityException(`Trying to remove the ${key} entity, but it is not an entity`);
            }
            if (node.getParentInstance() !== this) {
                throw new RemoveEntityException(`Trying to remove the ${key} entity, but it is not a child of this model`);
            }
            removeGraphNode(this.root.graph, key);
            this.removeField(key);
            this.root.notifyChange();
        }
        /**
         * Attaches an entity to the current model. It changes the parent of the entity
         * to the current model in the graph.
         *
         * The attached entity must be already a child of another model. It detaches the entity
         * from the previous model.
         *
         * @param key The key of the entity to attach.
         * @throws {AttachException} If the entity does not exist.
         * @throws {AttachException} If passed key doesn't belong to an entity.
         * @throws {AttachException} When the entity is already a child of the current model.
         * @throws {AttachException} If the entity is not in the same domain.
         * @example
         * ```typescript
         * model.attachEntity('address')
         * ```
         */
        attachEntity(key) {
            if (!this.root.graph.hasNode(key)) {
                throw new AttachException(`Trying to attach the ${key} entity, but it doesn't exist`);
            }
            const node = this.root.graph.node(key);
            if (!node || node.kind !== DomainEntityKind) {
                throw new AttachException(`Trying to attach the ${key} entity, but it is not a entity`);
            }
            if (node.domain.key !== this.root.key) {
                throw new AttachException(`Trying to attach the ${key} entity, 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`);
            }
            parent.detachEntity(key);
            if (!this.fields.some((item) => item.key === key)) {
                this.fields.push({
                    type: 'entity',
                    key: key,
                });
            }
            this.root.graph.setParent(key, this.key);
            // verify that the entity is moved to the current model
            const graphParent = this.root.graph.parent(key);
            if (graphParent !== this.key) {
                throw new AttachException(`Trying to attach the ${key} entity, but it is not a child of this model`);
            }
            this.root.notifyChange();
            return this;
        }
        /**
         * Detaches an entity from the current model. It changes the parent of the entity
         * to the root graph.
         * Unlike remove, it doesn't remove the entity from the graph nor notifies the change.
         * @param key The key of the entity to detach.
         * @throws {DetachException} If the entity does not exist.
         * @throws {DetachException} If passed key doesn't belong to an entity.
         * @throws {DetachException} When the entity is not a child of the current model.
         */
        detachEntity(key) {
            const index = this.fields.findIndex((item) => item.key === key);
            if (index === -1) {
                throw new DetachException(`Trying to detach the ${key} entity, but it doesn't exist`);
            }
            const node = this.root.graph.node(key);
            if (!node || node.kind !== DomainEntityKind) {
                throw new DetachException(`Trying to detach the ${key} entity, but it is not an entity`);
            }
            if (node.getParentInstance() !== this) {
                throw new DetachException(`Trying to detach the ${key} entity, but it is not a child of this model`);
            }
            this.fields.splice(index, 1);
            this.root.graph.setParent(key);
            return this;
        }
        /**
         * Lists all entities.
         *
         * Note, it accounts for the order of the entities as
         * defined in the `fields` array.
         *
         * @returns A generator that yields each `DomainEntity`.
         * @example
         * ```typescript
         * for (const entity of model.listEntities()) {
         *   console.log(entity.key);
         * }
         * ```
         */
        *listEntities() {
            for (const { key, type } of this.fields) {
                if (type !== 'entity') {
                    continue;
                }
                const node = this.root.graph.node(key);
                if (!node || node.kind !== DomainEntityKind) {
                    continue;
                }
                yield node;
            }
        }
        /**
         * Checks if this model has any entities.
         *
         * @returns True if the model has entities.
         * @example
         * ```typescript
         * if (model.hasEntities()) {
         *   // ...
         * }
         * ```
         */
        hasEntities() {
            return this.fields.some((item) => item.type === 'entity');
        }
        /**
         * Creates breadcrumbs from this data model to the root namespace.
         */
        breadcrumbs() {
            const result = [];
            result.push({
                key: this.key,
                name: this.info.getLabel('Unnamed model'),
                kind: DomainModelKind,
            });
            let parent = this.getParentInstance();
            while (parent && parent !== this.root) {
                result.push({
                    key: parent.key,
                    kind: parent.kind,
                    name: parent.info.getLabel('Unnamed namespace'),
                });
                const typed = parent;
                parent = typed.getParentInstance();
            }
            result.push({
                key: this.root.key,
                name: this.root.info.getLabel('Unnamed data domain'),
                kind: this.root.kind,
            });
            return result.reverse();
        }
        /**
         * Checks if this model has any fields (entities).
         *
         * @returns True if the model has fields.
         * @example
         * ```typescript
         * if (model.hasFields()) {
         *   // ...
         * }
         * ```
         */
        hasFields() {
            return this.fields.length > 0;
        }
        removeField(key) {
            this.fields = this.fields.filter((item) => item.key !== key);
        }
        /**
         * Checks whether the data model is a child of the given namespace.
         * The relationship doesn't have to be direct, as long as the data model is in the hierarchy it will return true.
         *
         * @param key The key of the parent namespace to check.
         * @returns True if this data model 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 { DomainModel };
//# sourceMappingURL=DomainModel.js.map