import { __esDecorate, __runInitializers } from "tslib";
import { DomainAssociationKind } from '../models/kinds.js';
import { DomainElement } from './DomainElement.js';
import { nanoid } from '../nanoid.js';
import { Thing } from '../models/Thing.js';
import { observed, retargetChange, toRaw } from '../decorators/observed.js';
import { DomainAttributeAttributes } from './DataFormat.js';
import { ShapeGenerator } from './amf/ShapeGenerator.js';
import { DataSemantics, isAssociationSemantic } from './Semantics.js';
/**
 * Represents an association between entities in a data domain.
 *
 * A `DomainAssociation` defines a relationship between a
 * `DomainEntity` and one or more other entities. It specifies
 * how entities are connected and can represent various types
 * of relationships, such as one-to-one, one-to-many, or
 * many-to-many.
 *
 * **Key Features:**
 *
 * -   **Target Entities:** Specifies the entities that are
 *     the target of the association.
 * -   **Cardinality:** Defines whether the association is
 *     single-valued or multi-valued (e.g., one-to-one vs.
 *     one-to-many).
 * -   **Required Status:** Indicates whether the association
 *     is mandatory or optional.
 * -   **Schema Customization:** Allows defining a custom
 *     schema for the association, overriding the default
 *     schema derived from the target entities.
 * -   **Bindings:** Supports defining how the association is
 *     represented in different formats (e.g., web APIs,
 *     protocol buffers).
 * -   **Foreign Namespaces:** Can target entities in other
 *     namespaces, enabling cross-namespace relationships.
 *
 * **Usage:**
 *
 * The preferred way to create a `DomainAssociation` is
 * through the `DomainEntity.addAssociation()` method.
 *
 * **Example:**
 *
 * ```typescript
 * const dataDomain = new DataDomain();
 * const userModel = dataDomain.addModel({ key: 'userModel' });
 * const userEntity = userModel.addEntity({ key: 'user' });
 * const addressEntity = userModel.addEntity({ key: 'address' });
 * // Add an association from User to Address
 * const addressAssociation = userEntity.addAssociation();
 * addressAssociation.addTarget(addressEntity);
 * ```
 */
let DomainAssociation = (() => {
    let _classSuper = DomainElement;
    let _info_decorators;
    let _info_initializers = [];
    let _info_extraInitializers = [];
    let _required_decorators;
    let _required_initializers = [];
    let _required_extraInitializers = [];
    let _multiple_decorators;
    let _multiple_initializers = [];
    let _multiple_extraInitializers = [];
    let _schema_decorators;
    let _schema_initializers = [];
    let _schema_extraInitializers = [];
    let _bindings_decorators;
    let _bindings_initializers = [];
    let _bindings_extraInitializers = [];
    let _targets_decorators;
    let _targets_initializers = [];
    let _targets_extraInitializers = [];
    let _semantics_decorators;
    let _semantics_initializers = [];
    let _semantics_extraInitializers = [];
    let _onDelete_decorators;
    let _onDelete_initializers = [];
    let _onDelete_extraInitializers = [];
    let _readOnly_decorators;
    let _readOnly_initializers = [];
    let _readOnly_extraInitializers = [];
    return class DomainAssociation extends _classSuper {
        static {
            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
            _info_decorators = [retargetChange()];
            _required_decorators = [observed()];
            _multiple_decorators = [observed()];
            _schema_decorators = [observed({ deep: true })];
            _bindings_decorators = [observed({ deep: true })];
            _targets_decorators = [observed({ deep: true })];
            _semantics_decorators = [observed({ deep: true })];
            _onDelete_decorators = [observed()];
            _readOnly_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, _required_decorators, { kind: "accessor", name: "required", static: false, private: false, access: { has: obj => "required" in obj, get: obj => obj.required, set: (obj, value) => { obj.required = value; } }, metadata: _metadata }, _required_initializers, _required_extraInitializers);
            __esDecorate(this, null, _multiple_decorators, { kind: "accessor", name: "multiple", static: false, private: false, access: { has: obj => "multiple" in obj, get: obj => obj.multiple, set: (obj, value) => { obj.multiple = value; } }, metadata: _metadata }, _multiple_initializers, _multiple_extraInitializers);
            __esDecorate(this, null, _schema_decorators, { kind: "accessor", name: "schema", static: false, private: false, access: { has: obj => "schema" in obj, get: obj => obj.schema, set: (obj, value) => { obj.schema = value; } }, metadata: _metadata }, _schema_initializers, _schema_extraInitializers);
            __esDecorate(this, null, _bindings_decorators, { kind: "accessor", name: "bindings", static: false, private: false, access: { has: obj => "bindings" in obj, get: obj => obj.bindings, set: (obj, value) => { obj.bindings = value; } }, metadata: _metadata }, _bindings_initializers, _bindings_extraInitializers);
            __esDecorate(this, null, _targets_decorators, { kind: "accessor", name: "targets", static: false, private: false, access: { has: obj => "targets" in obj, get: obj => obj.targets, set: (obj, value) => { obj.targets = value; } }, metadata: _metadata }, _targets_initializers, _targets_extraInitializers);
            __esDecorate(this, null, _semantics_decorators, { kind: "accessor", name: "semantics", static: false, private: false, access: { has: obj => "semantics" in obj, get: obj => obj.semantics, set: (obj, value) => { obj.semantics = value; } }, metadata: _metadata }, _semantics_initializers, _semantics_extraInitializers);
            __esDecorate(this, null, _onDelete_decorators, { kind: "accessor", name: "onDelete", static: false, private: false, access: { has: obj => "onDelete" in obj, get: obj => obj.onDelete, set: (obj, value) => { obj.onDelete = value; } }, metadata: _metadata }, _onDelete_initializers, _onDelete_extraInitializers);
            __esDecorate(this, null, _readOnly_decorators, { kind: "accessor", name: "readOnly", static: false, private: false, access: { has: obj => "readOnly" in obj, get: obj => obj.readOnly, set: (obj, value) => { obj.readOnly = value; } }, metadata: _metadata }, _readOnly_initializers, _readOnly_extraInitializers);
            if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
        }
        /**
         * The kind of the domain element.
         */
        kind;
        #info_accessor_storage = __runInitializers(this, _info_initializers, void 0);
        /**
         * The description of the association namespace.
         */
        get info() { return this.#info_accessor_storage; }
        set info(value) { this.#info_accessor_storage = value; }
        #required_accessor_storage = (__runInitializers(this, _info_extraInitializers), __runInitializers(this, _required_initializers, void 0));
        /**
         * Wether the data association is required.
         */
        get required() { return this.#required_accessor_storage; }
        set required(value) { this.#required_accessor_storage = value; }
        #multiple_accessor_storage = (__runInitializers(this, _required_extraInitializers), __runInitializers(this, _multiple_initializers, void 0));
        /**
         * Whether the data association allows multiple items.
         */
        get multiple() { return this.#multiple_accessor_storage; }
        set multiple(value) { this.#multiple_accessor_storage = value; }
        #schema_accessor_storage = (__runInitializers(this, _multiple_extraInitializers), __runInitializers(this, _schema_initializers, void 0));
        /**
         * The schema allowing to translate the model into a
         * specific format (like JSON, RAML, XML, etc.)
         */
        get schema() { return this.#schema_accessor_storage; }
        set schema(value) { this.#schema_accessor_storage = value; }
        #bindings_accessor_storage = (__runInitializers(this, _schema_extraInitializers), __runInitializers(this, _bindings_initializers, void 0));
        /**
         * The list of bindings for this property.
         *
         * A binding defines a translation from a data model to a specific format.
         * For example allows to define properties required to generate AMF shape and therefore RAML/OAS shapes for web APIs
         * or a protocol buffer schema.
         */
        get bindings() { return this.#bindings_accessor_storage; }
        set bindings(value) { this.#bindings_accessor_storage = value; }
        /**
         * The key of the parent entity.
         */
        parent = __runInitializers(this, _bindings_extraInitializers);
        #targets_accessor_storage = __runInitializers(this, _targets_initializers, void 0);
        /**
         * The list of keys associated with the entity through this association.
         * An association without a target is considered invalid and discarded when processing the values.
         * When multiple associations are set then we are dealing with an union.
         */
        get targets() { return this.#targets_accessor_storage; }
        set targets(value) { this.#targets_accessor_storage = value; }
        #semantics_accessor_storage = (__runInitializers(this, _targets_extraInitializers), __runInitializers(this, _semantics_initializers, []
        /**
         * Defines the behavior when a parent entity in an association is deleted.
         */
        ));
        /**
         * Semantics applied to this association.
         */
        get semantics() { return this.#semantics_accessor_storage; }
        set semantics(value) { this.#semantics_accessor_storage = value; }
        #onDelete_accessor_storage = (__runInitializers(this, _semantics_extraInitializers), __runInitializers(this, _onDelete_initializers, void 0));
        /**
         * Defines the behavior when a parent entity in an association is deleted.
         */
        get onDelete() { return this.#onDelete_accessor_storage; }
        set onDelete(value) { this.#onDelete_accessor_storage = value; }
        #readOnly_accessor_storage = (__runInitializers(this, _onDelete_extraInitializers), __runInitializers(this, _readOnly_initializers, void 0));
        /**
         * Whether the association is read-only.
         */
        get readOnly() { return this.#readOnly_accessor_storage; }
        set readOnly(value) { this.#readOnly_accessor_storage = value; }
        /**
         * Creates a full data association schema with defaults.
         *
         * @param input The partial data association schema.
         * @returns The data association schema.
         */
        static createSchema(input = {}) {
            const { key = nanoid(), semantics = [] } = input;
            const info = Thing.fromJSON(input.info, { name: 'new_association' }).toJSON();
            const result = {
                kind: DomainAssociationKind,
                key,
                info,
            };
            if (Array.isArray(semantics)) {
                result.semantics = [...semantics];
            }
            if (input.onDelete) {
                result.onDelete = input.onDelete;
            }
            if (typeof input.readOnly === 'boolean') {
                result.readOnly = input.readOnly;
            }
            if (input.schema) {
                result.schema = structuredClone(input.schema);
            }
            if (typeof input.multiple === 'boolean') {
                result.multiple = input.multiple;
            }
            if (typeof input.required === 'boolean') {
                result.required = input.required;
            }
            if (Array.isArray(input.bindings)) {
                result.bindings = input.bindings.map((i) => structuredClone(i));
            }
            if (Array.isArray(input.targets)) {
                result.targets = input.targets.map((i) => ({ ...i }));
            }
            return result;
        }
        /**
         * Creates a new data association instance.
         *
         * You probably want to use the `DomainEntity.addAssociation()` method instead.
         *
         * @param root A reference to the root DataDomain instance.
         * @param parent The key of the parent entity.
         * @param input The partial data association schema.
         */
        constructor(root, parent, input) {
            const init = DomainAssociation.createSchema(input);
            super(root, init.key);
            __runInitializers(this, _readOnly_extraInitializers);
            this.parent = parent;
            this.kind = DomainAssociationKind;
            this.info = new Thing(init.info);
            if (init.schema) {
                this.schema = structuredClone(init.schema);
            }
            if (typeof init.multiple === 'boolean') {
                this.multiple = init.multiple;
            }
            if (typeof init.required === 'boolean') {
                this.required = init.required;
            }
            if (Array.isArray(init.bindings)) {
                this.bindings = init.bindings.map((i) => structuredClone(i));
            }
            else {
                this.bindings = [];
            }
            if (Array.isArray(init.targets)) {
                this.targets = init.targets.map((i) => ({ ...i }));
            }
            else {
                this.targets = [];
            }
            if (Array.isArray(init.semantics)) {
                this.semantics = init.semantics.map((item) => structuredClone(item));
            }
            else {
                this.semantics = [];
            }
            if (init.onDelete) {
                this.onDelete = init.onDelete;
            }
            if (typeof init.readOnly === 'boolean') {
                this.readOnly = init.readOnly;
            }
        }
        /**
         * Generates a JSON representation of the association.
         *
         * @returns A plain object representation of the `DomainAssociation`.
         */
        toJSON() {
            const result = {
                kind: this.kind,
                key: this.key,
                info: this.info.toJSON(),
            };
            if (this.schema) {
                result.schema = structuredClone(toRaw(this, this.schema));
            }
            if (typeof this.multiple === 'boolean') {
                result.multiple = this.multiple;
            }
            if (typeof this.required === 'boolean') {
                result.required = this.required;
            }
            if (Array.isArray(this.bindings) && this.bindings.length) {
                result.bindings = toRaw(this, this.bindings)?.map((i) => structuredClone(i));
            }
            if (Array.isArray(this.targets) && this.targets.length) {
                result.targets = toRaw(this, this.targets)?.map((i) => ({ ...i }));
            }
            if (Array.isArray(this.semantics) && this.semantics.length) {
                result.semantics = toRaw(this, this.semantics)?.map((i) => structuredClone(i));
            }
            if (this.onDelete) {
                result.onDelete = this.onDelete;
            }
            if (typeof this.readOnly === 'boolean' && this.readOnly) {
                result.readOnly = this.readOnly;
            }
            return result;
        }
        /**
         * Returns a parent data entity where this data property exist.
         *
         * @returns The parent `DomainEntity` or undefined if not found.
         */
        getParentInstance() {
            return this.root.graph.node(this.parent);
        }
        /**
         * Removes this property from the parent entity.
         *
         * This method removes the association from the parent
         * entity's list of associations.
         *
         * @example
         * ```typescript
         * association.remove();
         * ```
         */
        remove() {
            const parent = this.getParentInstance();
            parent?.removeAssociation(this.key);
        }
        /**
         * Creates the `schema` object if missing and returns it.
         *
         * @returns The schema object.
         * @example
         * ```typescript
         * const schema = association.ensureSchema();
         * ```
         */
        ensureSchema() {
            if (!this.schema) {
                this.schema = {};
            }
            return this.schema;
        }
        /**
         * Checks whether the passed value is one of the supported
         * data property attributes.
         *
         * @param value The value to test
         * @returns True when the passed value is one of the
         *   supported data property attributes.
         */
        static isValidAttribute(value) {
            if (typeof value !== 'string') {
                return false;
            }
            return DomainAttributeAttributes.includes(value);
        }
        /**
         * Creates if not existing and returns web bindings
         * definition.
         *
         * @returns The web binding definition
         * @example
         * ```typescript
         * const webBindings = association.getWebBinding();
         * webBindings.hidden = true;
         * ```
         */
        getWebBinding() {
            let object = this.bindings.find((i) => i.type === 'web');
            if (!object) {
                object = { type: 'web', schema: {} };
                this.bindings.push(object);
                this.domain.notifyChange();
            }
            return object.schema;
        }
        /**
         * Returns the schema value of the binding, if any was
         * created.
         *
         * @param type The type of the binding to read.
         * @returns The binding schema, if any
         * @example
         * ```typescript
         * const webBindings = association.readBinding('web');
         * if (webBindings) {
         *   // ...
         * }
         * ```
         */
        readBinding(type) {
            const item = this.bindings.find((i) => i.type === type);
            return item?.schema || undefined;
        }
        /**
         * Adds a target entity to the association.
         *
         * @param init The key of an entity, its instance, or schema.
         * @param namespace - The key of the foreign namespace this
         *   entity belongs to. Do not set this value for local entities.
         */
        addTarget(init, namespace) {
            let key;
            if (typeof init === 'string') {
                key = init;
            }
            else {
                key = init.key;
                if (init.domain.key !== this.root.key) {
                    namespace = init.domain.key;
                }
            }
            if (this.targets.some((i) => i.key === key && i.domain === namespace)) {
                const message = `Target ${key} already exists.`;
                throw new Error(message);
            }
            if (namespace && !this.root.dependencies.has(namespace)) {
                throw new Error(`Foreign namespace "${namespace}" does not exist.`);
            }
            const info = {
                key,
            };
            if (namespace) {
                info.domain = namespace;
            }
            this.targets.push(info);
            const targetKey = namespace ? `${namespace}:${key}` : key;
            const label = {
                type: 'association',
            };
            if (namespace) {
                label.domain = namespace;
                label.foreign = true;
            }
            // We need to associate the target with the association
            // in the graph. This way, when looking for associations
            // on a target entities we can easily locate the
            // association.
            this.root.graph.setEdge(this.key, targetKey, label);
            // this.root.notifyChange()
        }
        /**
         * Removes a target entity from the targets list.
         *
         * @param init The key of an entity, its instance, or
         *   schema.
         * @example
         * ```typescript
         * association.removeTarget(addressEntity);
         * ```
         */
        removeTarget(init) {
            let key;
            if (typeof init === 'string') {
                key = init;
            }
            else {
                key = init.key;
            }
            const index = this.targets.findIndex((i) => i.key === key);
            if (index >= 0) {
                this.targets.splice(index, 1);
                // this.root.notifyChange()
            }
            this.root.graph.removeEdge(this.key, key);
        }
        /**
         * Lists all target entities of this association.
         *
         * @returns A generator that yields each target
         *   `DomainEntity`.
         * @example
         * ```typescript
         * for (const target of association.listTargets()) {
         *   console.log(target.key);
         * }
         * ```
         */
        *listTargets() {
            for (const target of this.targets) {
                const { key, domain } = target;
                const targetKey = domain ? `${domain}:${key}` : key;
                const entity = this.root.graph.node(targetKey);
                if (entity) {
                    yield entity;
                }
            }
        }
        /**
         * Creates a Property Shape of AMF.
         * The property itself is auto-generated. If the `schema` is defined then it is used
         * as the `range` of the property. Otherwise basic shape is generated for the range.
         *
         * This is a preferred way of reading the AMF shape as this synchronizes changed
         * data properties with the shape definition.
         *
         * @returns AMF property shape definition.
         */
        toApiShape() {
            const serializer = new ShapeGenerator();
            return serializer.associationProperty(this);
        }
        /**
         * Adds or updates a semantic to the association.
         * @param semantic The semantic to add to the association.
         * @throws Error if the semantic is not an association semantic.
         */
        addSemantic(semantic) {
            const sem = DataSemantics[semantic.id];
            if (!isAssociationSemantic(sem)) {
                throw new Error(`Invalid semantic type: ${semantic.id}. Expected an association semantic.`);
            }
            const index = this.semantics.findIndex((s) => s.id === semantic.id);
            if (index >= 0) {
                this.semantics[index] = semantic;
            }
            else {
                this.semantics.push(semantic);
            }
            this.domain.notifyChange();
        }
        /**
         * Removes a semantic from the association.
         * @param semanticId The ID of the semantic to remove.
         */
        removeSemantic(semanticId) {
            const index = this.semantics.findIndex((s) => s.id === semanticId);
            if (index >= 0) {
                this.semantics.splice(index, 1);
                this.domain.notifyChange();
            }
        }
        /**
         * Checks if the association has a specific semantic.
         * @param semanticId The ID of the semantic to check.
         * @returns True if the semantic is present, false otherwise.
         */
        hasSemantic(semanticId) {
            return this.semantics.some((s) => s.id === semanticId);
        }
        /**
         * Creates a duplicate of this association.
         * It places the duplicate on the parent entity right after the original association.
         *
         * @returns A new `DomainAssociation` instance that is a duplicate of this one.
         */
        duplicate() {
            const parent = this.getParentInstance();
            if (!parent) {
                throw new Error(`Cannot duplicate association ${this.key} as it has no parent entity.`);
            }
            const originalIndex = parent.fields.findIndex((f) => f.key === this.key);
            if (originalIndex === -1) {
                throw new Error(`Cannot duplicate association ${this.key} as it does not exist on the parent entity fields list.`);
            }
            const baseName = this.info.name || 'field';
            const newName = parent.generateUniqueName(baseName);
            // Making a copy and restoring it through the `addAssociation()` method
            // scales better than copying it manually.
            const copy = this.toJSON();
            // Delete properties that should not be copied
            delete copy.key;
            delete copy.targets;
            // Set the new name for the duplicated association
            if (copy.info) {
                copy.info.name = newName;
            }
            else {
                copy.info = { name: newName };
            }
            const result = parent.addAssociation({}, copy);
            // Copy the target entities
            for (const target of this.targets) {
                result.addTarget(target.key, target.domain);
            }
            // Move the duplicate to be right after the original
            const fromIndex = parent.fields.length - 1;
            const duplicateField = parent.fields[fromIndex];
            parent.fields.splice(fromIndex, 1);
            parent.fields.splice(originalIndex + 1, 0, duplicateField);
            this.domain.notifyChange();
            return result;
        }
    };
})();
export { DomainAssociation };
//# sourceMappingURL=DomainAssociation.js.map