/* eslint-disable @typescript-eslint/no-this-alias */
import { Thing } from '../models/Thing.js';
import { DataAssociation } from './DataAssociation.js';
import { DataEntity } from './DataEntity.js';
import { DataModel } from './DataModel.js';
import { DataProperty } from './DataProperty.js';
import { DataNamespaceKind, DataModelKind } from '../models/kinds.js';
import v4 from '../lib/uuid.js';
class DataNamespaceParent {
    kind = DataNamespaceKind;
    key = '';
    /**
     * The ordered list of items in this namespace.
     */
    items = [];
    /**
     * The description of the data namespace.
     */
    info = Thing.fromName('');
    /**
     * When a namespace is a sub-namespace this is the reference to the
     * root namespace with all definitions.
     */
    root;
    constructor(root) {
        this.root = root;
    }
    /**
     * @returns The parent namespace of this namespace. It returns `undefined` when this is the root namespace.
     */
    getParent() {
        const { root, key } = this;
        if (root) {
            const result = root.findParent(key);
            if (result === this.root) {
                return undefined;
            }
            return result;
        }
        // we are the root namespace.
        return undefined;
    }
    /**
     * Adds a data namespace to the structure.
     * @param init The name of the namespace to add, namespace's schema, or instance.
     */
    addNamespace(init) {
        return this.root.addNamespace(init, this.key);
    }
    /**
     * Lists namespaces that are in this namespace items.
     */
    listNamespaces() {
        const result = [];
        const { items } = this;
        const root = this.getRoot();
        const { namespaces } = root.definitions;
        items.forEach((i) => {
            if (i.kind !== DataNamespaceKind) {
                return;
            }
            const def = namespaces.find((j) => j.key === i.key);
            if (def) {
                result.push(def);
            }
        });
        return result;
    }
    /**
     * Lists namespaces that are in this namespace items.
     */
    listDataModels() {
        const result = [];
        const { items } = this;
        const root = this.getRoot();
        const { models } = root.definitions;
        items.forEach((i) => {
            if (i.kind !== DataModelKind) {
                return;
            }
            const def = models.find((j) => j.key === i.key);
            if (def) {
                result.push(def);
            }
        });
        return result;
    }
    /**
     * @returns The root of the namespaces tree. It might be the same object.
     */
    getRoot() {
        if (this.root) {
            return this.root;
        }
        return this;
    }
    /**
     * Removes self from the parent namespace with all data models.
     * This does noting for the root namespace.
     */
    remove() {
        const { root } = this;
        if (!root) {
            throw new Error(`Unable to remove the root namespace this way.`);
        }
        const models = this.listDataModels();
        const children = this.listNamespaces();
        models.forEach((m) => m.remove());
        children.forEach((c) => c.remove());
        const index = root.definitions.namespaces.findIndex((i) => i.key === this.key);
        if (index >= 0) {
            root.definitions.namespaces.splice(index, 1);
        }
    }
}
export class DataItem {
    kind = DataModelKind;
    key = '';
    /**
     * A reference to the top level namespace.
     */
    root;
    static isDataItem(input) {
        const typed = input;
        if (!input || ![DataModelKind, DataNamespaceKind].includes(typed.kind)) {
            return false;
        }
        return true;
    }
    static dataNamespace(root, key) {
        const item = new DataItem(root, {
            kind: DataNamespaceKind,
            key,
        });
        return item;
    }
    static dataModel(root, key) {
        const item = new DataItem(root, {
            kind: DataModelKind,
            key,
        });
        return item;
    }
    /**
     * @param root The top-most data namespace.
     * @param input The project item definition used to restore the state.
     */
    constructor(root, input) {
        this.root = root;
        let init;
        if (typeof input === 'string') {
            init = JSON.parse(input);
        }
        else if (typeof input === 'object') {
            init = input;
        }
        else {
            throw new Error('Specify the type of the item.');
        }
        this.new(init);
    }
    new(init) {
        if (!DataItem.isDataItem(init)) {
            throw new Error(`Not a data item.`);
        }
        const { kind, key } = init;
        this.kind = kind;
        this.key = key;
    }
    toJSON() {
        const result = {
            kind: this.kind,
            key: this.key,
        };
        return result;
    }
    getItem() {
        const { root, key, kind } = this;
        const { definitions } = root;
        if (kind === DataModelKind) {
            return definitions.models.find((i) => i.key === key);
        }
        if (kind === kind) {
            return definitions.namespaces.find((i) => i.key === key);
        }
    }
}
/**
 * Data namespace is a logical description of the hierarchy in the data.
 */
export class DataNamespace extends DataNamespaceParent {
    definitions;
    /**
     * The list of foreign namespaces.
     * This is used to find entities from foreign namespaces.
     * Values here are treated as read only (objects can be frozen).
     *
     * This should be set by the application and the namespace will do
     * nothing to populate this value.
     */
    foreign = [];
    /**
     * Creates a new data namespace from a name.
     * @param name The name to set.
     */
    static fromName(name, root) {
        const ns = new DataNamespace(undefined, root);
        const info = Thing.fromName(name);
        ns.info = info;
        return ns;
    }
    static definitions() {
        return {
            models: [],
            associations: [],
            entities: [],
            properties: [],
            namespaces: [],
            tags: [],
        };
    }
    constructor(input, root) {
        super(root);
        this.definitions = DataNamespace.definitions();
        let init;
        if (typeof input === 'string') {
            init = JSON.parse(input);
        }
        else if (typeof input === 'object') {
            init = input;
            if (!init.kind) {
                init.kind = DataNamespaceKind;
            }
        }
        else {
            init = {
                kind: DataNamespaceKind,
                key: v4(),
                definitions: {},
                items: [],
                info: Thing.fromName('').toJSON(),
            };
        }
        this.new(init);
    }
    new(init) {
        if (!init || !init.definitions || !init.items) {
            throw new Error(`Not a data namespace.`);
        }
        const { key = v4(), definitions = {}, items, info } = init;
        this.key = key;
        if (info) {
            this.info = new Thing(info);
        }
        else {
            this.info = Thing.fromName('');
        }
        if (Array.isArray(items)) {
            this.items = items.map((i) => new DataItem(this, i));
        }
        else {
            this.items = [];
        }
        if (Array.isArray(definitions.associations)) {
            this.definitions.associations = definitions.associations.map((i) => new DataAssociation(this, i));
        }
        else {
            this.definitions.associations = [];
        }
        if (Array.isArray(definitions.properties)) {
            this.definitions.properties = definitions.properties.map((i) => new DataProperty(this, i));
        }
        else {
            this.definitions.properties = [];
        }
        // note, entities must be restored after properties / associations
        if (Array.isArray(definitions.entities)) {
            this.definitions.entities = definitions.entities.map((i) => new DataEntity(this, i));
        }
        else {
            this.definitions.entities = [];
        }
        // must be set after entities.
        if (Array.isArray(definitions.models)) {
            this.definitions.models = definitions.models.map((i) => new DataModel(this, i));
        }
        else {
            this.definitions.models = [];
        }
        if (Array.isArray(definitions.namespaces)) {
            this.definitions.namespaces = definitions.namespaces.map((i) => new DataNamespace(i, this));
        }
        else {
            this.definitions.namespaces = [];
        }
        if (Array.isArray(definitions.tags)) {
            this.definitions.tags = [...definitions.tags];
        }
        else {
            this.definitions.tags = [];
        }
    }
    toJSON() {
        const result = {
            key: this.key,
            kind: DataNamespaceKind,
            info: this.info.toJSON(),
            items: this.items.map((i) => i.toJSON()),
            definitions: {},
        };
        const { associations, entities, models, namespaces, properties, tags } = this.definitions;
        if (Array.isArray(associations) && associations.length) {
            result.definitions.associations = associations.map((i) => i.toJSON());
        }
        if (Array.isArray(entities) && entities.length) {
            result.definitions.entities = entities.map((i) => i.toJSON());
        }
        if (Array.isArray(models) && models.length) {
            result.definitions.models = models.map((i) => i.toJSON());
        }
        if (Array.isArray(namespaces) && namespaces.length) {
            result.definitions.namespaces = namespaces.map((i) => i.toJSON());
        }
        if (Array.isArray(properties) && properties.length) {
            result.definitions.properties = properties.map((i) => i.toJSON());
        }
        if (Array.isArray(tags) && tags.length) {
            result.definitions.tags = [...tags];
        }
        return result;
    }
    /**
     * Checks if this is the root namespace.
     * @returns True if this is the root namespace.
     */
    isRoot() {
        return this.root === undefined;
    }
    /**
     * Finds a parent namespace for the given namespace.
     * @param key The namespace key to find the parent for.
     * @returns The parent namespace or undefined when the namespace does not exist. It may return the root namespace.
     */
    findParent(key) {
        const { definitions, items = [] } = this;
        const rootIndex = items.findIndex((i) => i.key === key);
        if (rootIndex >= 0) {
            return this;
        }
        const definition = definitions.namespaces.find((i) => i.items.some((item) => item.key === key));
        if (definition) {
            return definition;
        }
        return undefined;
    }
    /**
     * Adds a data namespace to the structure.
     * @param init The name of the namespace to add, namespace's schema, or instance.
     * @param parent The optional key of the parent namespace to add the new namespace to.
     */
    addNamespace(init, parent) {
        let root;
        if (parent) {
            const rootCandidate = this.findParent(parent);
            if (!rootCandidate) {
                throw new Error(`Unable to find the parent namespace ${parent}`);
            }
            root = rootCandidate;
        }
        else {
            root = this;
        }
        let definition;
        if (typeof init === 'string') {
            definition = DataNamespace.fromName(init, this.root || this);
        }
        else if (init instanceof DataNamespace) {
            definition = init;
        }
        else {
            definition = new DataNamespace(init, this.root || this);
        }
        ;
        (this.root || this).definitions.namespaces.push(definition);
        const item = DataItem.dataNamespace(this.root || this, definition.key);
        if (!Array.isArray(root.items)) {
            root.items = [];
        }
        root.items.push(item);
        return definition;
    }
    /**
     * Finds a namespace in the definitions.
     * @param key The key of the namespace to find.
     * @returns The namespace definition or undefined when not found.
     */
    findNamespace(key) {
        const { definitions } = this.root || this;
        return definitions.namespaces.find((i) => i.key === key);
    }
    /**
     * Finds a namespace and calls the `remove()` on it.
     * @param key The key of the namespace to find.
     */
    removeNamespace(key) {
        const root = this.root || this;
        if (root.key === key) {
            throw new Error(`Unable to remove the root namespace this way.`);
        }
        const { definitions } = root;
        const ns = definitions.namespaces.find((i) => i.key === key);
        if (ns) {
            ns.remove();
        }
    }
    /**
     * Adds a data model to a namespace.
     * @param init The name of the data model to add, data model's schema, or its instance.
     * @param parent The optional key of the parent namespace to add the new data model to.
     */
    addDataModel(init, parent) {
        let root;
        if (parent) {
            const rootCandidate = this.findNamespace(parent);
            if (!rootCandidate) {
                throw new Error(`Unable to find the parent namespace ${parent}`);
            }
            root = rootCandidate;
        }
        else {
            root = this;
        }
        let definition;
        if (typeof init === 'string') {
            definition = DataModel.fromName(this.root || this, init);
        }
        else if (init instanceof DataModel) {
            definition = init;
        }
        else {
            definition = new DataModel(this.root || this, init);
        }
        ;
        (this.root || this).definitions.models.push(definition);
        const item = DataItem.dataModel(this.root || this, definition.key);
        if (!Array.isArray(root.items)) {
            root.items = [];
        }
        root.items.push(item);
        return definition;
    }
    /**
     * Finds a data model in the definitions.
     * @param key The key of the data model to find.
     * @returns The data model definition or undefined when not found.
     */
    findDataModel(key) {
        const { definitions } = this.root || this;
        return definitions.models.find((i) => i.key === key);
    }
    /**
     * Finds a data model and calls the `remove()` on it.
     * @param key The key of the data model to find.
     */
    removeDataModel(key) {
        const { definitions } = this.root || this;
        const model = definitions.models.find((i) => i.key === key);
        if (model) {
            model.remove();
        }
    }
    /**
     * @returns The graph of associations where keys are the source
     * entities and the value is the list of all target entities.
     */
    associationGraph() {
        const graph = {};
        const { definitions } = this.root || this;
        const { associations, entities } = definitions;
        for (const assoc of associations) {
            if (!assoc.targets.length) {
                continue;
            }
            const srcEntity = entities.find((i) => i.associations.some((a) => a === assoc));
            if (!srcEntity) {
                continue;
            }
            if (!graph[srcEntity.key]) {
                graph[srcEntity.key] = [];
            }
            const targetIds = assoc.targets.map((i) => i.key);
            graph[srcEntity.key].splice(0, 0, ...targetIds);
        }
        return graph;
    }
    /**
     * Prints out all associations from one entity to another through all entities that may be in between.
     *
     * @param from The key of the from entity
     * @param to The key of the target entity
     * @param g The graph generated with `associationGraph()`
     * @param path The current list of entity ids. Do not set this, it is for the recursive processing of the graph.
     * @param visited The list of visited paths to avoid cycles.
     *  Do not set this, it is for the recursive processing of the graph.
     */
    *associationPath(from, to, g, path = [], visited = new Set()) {
        if (from === to) {
            yield path.concat(to);
            return;
        }
        if (visited.has(from)) {
            // it's a cycle
            return;
        }
        if (g[from]) {
            visited.add(from);
            path.push(from);
            for (const neighbor of g[from]) {
                yield* this.associationPath(neighbor, to, g, path, visited);
            }
            visited.delete(from);
            path.pop();
        }
    }
    /**
     * Scans all associations for foreign namespaces and returns
     * the list of all namespaces used in the association graph.
     * This will never add this namespace's key.
     *
     * This function should be used to read keys for all related
     * namespaces through associations.
     *
     * @returns All keys of foreign namespaces.
     */
    computeForeignNamespaceKeys() {
        const result = [];
        const { associations = [] } = this.definitions;
        associations.forEach((association) => {
            const { targets = [] } = association;
            targets.forEach((target) => {
                const { namespace } = target;
                if (!namespace || namespace === this.key) {
                    return;
                }
                result.push(namespace);
            });
        });
        return result;
    }
    /**
     * Finds an entity in this namespace.
     * @param key The key of the entity to find.
     */
    findEntity(key) {
        const { definitions } = this.root || this;
        return definitions.entities.find((i) => i.key === key);
    }
    /**
     * Finds a property by its key.
     * @param key The key of the property to find
     * @returns The property or undefined if not found.
     */
    findProperty(key) {
        const { definitions } = this.root || this;
        return definitions.properties.find((i) => i.key === key);
    }
    /**
     * Finds an association by its key.
     * @param key The key of the property to find
     * @returns The property or undefined if not found.
     */
    findAssociation(key) {
        const { definitions } = this.root || this;
        return definitions.associations.find((i) => i.key === key);
    }
    /**
     * Searches for entities for association targets.
     * This is a helper function to discover entities in the current and foreign namespaces.
     *
     * @param targets The list of targets
     * @returns A list of entities. An `undefined` is put at the index where an entity cannot be found
     */
    findAssociatedEntities(targets) {
        const result = [];
        targets.forEach((target) => {
            let ns;
            if (target.namespace) {
                ns = this.foreign.find((i) => i.key === target.namespace);
            }
            else {
                ns = this;
            }
            if (!ns) {
                result.push(undefined);
                return;
            }
            const entity = ns.findEntity(target.key);
            if (entity) {
                result.push(entity);
            }
            else {
                result.push(undefined);
            }
        });
        return result;
    }
    /**
     * Finds an associated entity in the current or foreign namespace.
     * This is a helper function to discover entities in the current and foreign namespaces.
     *
     * @param key The key of the entity to find.
     * @param namespace The optional namespace to search in.
     * If not set, the current namespace is used.
     * This is used to find entities in foreign namespaces.
     * @returns The entity or undefined if not found.
     */
    findAssociatedEntity(key, namespace) {
        let ns;
        if (namespace) {
            ns = this.foreign.find((i) => i.key === namespace);
        }
        else {
            ns = this;
        }
        if (!ns) {
            return undefined;
        }
        return ns.findEntity(key);
    }
    addForeign(ns) {
        const exists = this.foreign.some((i) => i.key === ns.key);
        if (exists) {
            return;
        }
        this.foreign.push(ns);
    }
    removeForeign(ns) {
        const index = this.foreign.findIndex((i) => i === ns);
        if (index >= 0) {
            this.foreign.splice(index, 1);
        }
    }
    hasForeignNamespace(key) {
        return this.foreign.some((i) => i.key === key);
    }
}
//# sourceMappingURL=DataNamespace.js.map