+    true if note has an attribute with given type and name (including inherited)
+
+
+
+
+
             import server from '../services/server.js';
 import Attribute from './attribute.js';
+import noteAttributeCache from "../services/note_attribute_cache.js";
 
 const LABEL = 'label';
-const LABEL_DEFINITION = 'label-definition';
 const RELATION = 'relation';
-const RELATION_DEFINITION = 'relation-definition';
 
 /**
+ * FIXME: since there's no "full note" anymore we can rename this to Note
+ *
  * This note's representation is used in note tree and is kept in TreeCache.
  */
 class NoteShort {
@@ -70,8 +71,6 @@ class NoteShort {
         this.noteId = row.noteId;
         /** @param {string} */
         this.title = row.title;
-        /** @param {int} */
-        this.contentLength = row.contentLength;
         /** @param {boolean} */
         this.isProtected = !!row.isProtected;
         /** @param {string} one of 'text', 'code', 'file' or 'render' */
@@ -83,6 +82,10 @@ class NoteShort {
     }
 
     addParent(parentNoteId, branchId) {
+        if (parentNoteId === 'none') {
+            return;
+        }
+
         if (!this.parents.includes(parentNoteId)) {
             this.parents.push(parentNoteId);
         }
@@ -97,6 +100,10 @@ class NoteShort {
 
         this.childToBranch[childNoteId] = branchId;
 
+        this.sortChildren();
+    }
+
+    sortChildren() {
         const branchIdPos = {};
 
         for (const branchId of Object.values(this.childToBranch)) {
@@ -184,9 +191,9 @@ class NoteShort {
     getOwnedAttributes(type, name) {
         const attrs = this.attributes
             .map(attributeId => this.treeCache.attributes[attributeId])
-            .filter(attr => !!attr);
+            .filter(Boolean); // filter out nulls;
 
-        return this.__filterAttrs(attrs, type, name)
+        return this.__filterAttrs(attrs, type, name);
     }
 
     /**
@@ -195,48 +202,66 @@ class NoteShort {
      * @returns {Attribute[]} all note's attributes, including inherited ones
      */
     getAttributes(type, name) {
-        const ownedAttributes = this.getOwnedAttributes();
+        return this.__filterAttrs(this.__getCachedAttributes([]), type, name);
+    }
 
-        const attrArrs = [
-            ownedAttributes
-        ];
-
-        for (const templateAttr of ownedAttributes.filter(oa => oa.type === 'relation' && oa.name === 'template')) {
-            const templateNote = this.treeCache.getNoteFromCache(templateAttr.value);
-
-            if (templateNote) {
-                attrArrs.push(templateNote.getAttributes());
-            }
+    __getCachedAttributes(path) {
+        // notes/clones cannot form tree cycles, it is possible to create attribute inheritance cycle via templates
+        // when template instance is a parent of template itself
+        if (path.includes(this.noteId)) {
+            return [];
         }
 
-        if (this.noteId !== 'root') {
-            for (const parentNote of this.getParentNotes()) {
-                // these virtual parent-child relationships are also loaded into frontend tree cache
-                if (parentNote.type !== 'search') {
-                    attrArrs.push(parentNote.getInheritableAttributes());
+        if (!(this.noteId in noteAttributeCache.attributes)) {
+            const newPath = [...path, this.noteId];
+            const attrArrs = [ this.getOwnedAttributes() ];
+
+            if (this.noteId !== 'root') {
+                for (const parentNote of this.getParentNotes()) {
+                    // these virtual parent-child relationships are also loaded into frontend tree cache
+                    if (parentNote.type !== 'search') {
+                        attrArrs.push(parentNote.__getInheritableAttributes(newPath));
+                    }
+                }
+            }
+
+            for (const templateAttr of attrArrs.flat().filter(attr => attr.type === 'relation' && attr.name === 'template')) {
+                const templateNote = this.treeCache.notes[templateAttr.value];
+
+                if (templateNote && templateNote.noteId !== this.noteId) {
+                    attrArrs.push(templateNote.__getCachedAttributes(newPath));
+                }
+            }
+
+            noteAttributeCache.attributes[this.noteId] = [];
+            const addedAttributeIds = new Set();
+
+            for (const attr of attrArrs.flat()) {
+                if (!addedAttributeIds.has(attr.attributeId)) {
+                    addedAttributeIds.add(attr.attributeId);
+
+                    noteAttributeCache.attributes[this.noteId].push(attr);
                 }
             }
         }
 
-        const attributes = attrArrs.flat();
-
-        return this.__filterAttrs(attributes, type, name);
+        return noteAttributeCache.attributes[this.noteId];
     }
 
     __filterAttrs(attributes, type, name) {
-        if (type && name) {
+        if (!type && !name) {
+            return attributes;
+        } else if (type && name) {
             return attributes.filter(attr => attr.type === type && attr.name === name);
         } else if (type) {
             return attributes.filter(attr => attr.type === type);
         } else if (name) {
             return attributes.filter(attr => attr.name === name);
-        } else {
-            return attributes;
         }
     }
 
-    getInheritableAttributes() {
-        const attrs = this.getAttributes();
+    __getInheritableAttributes(path) {
+        const attrs = this.__getCachedAttributes(path);
 
         return attrs.filter(attr => attr.isInheritable);
     }
@@ -257,14 +282,6 @@ class NoteShort {
         return this.getAttributes(LABEL, name);
     }
 
-    /**
-     * @param {string} [name] - label name to filter
-     * @returns {Attribute[]} all note's label definitions, including inherited ones
-     */
-    getLabelDefinitions(name) {
-        return this.getAttributes(LABEL_DEFINITION, name);
-    }
-
     /**
      * @param {string} [name] - relation name to filter
      * @returns {Attribute[]} all note's relations (attributes with type relation), including inherited ones
@@ -281,14 +298,6 @@ class NoteShort {
         return this.getAttributes(RELATION, name);
     }
 
-    /**
-     * @param {string} [name] - relation name to filter
-     * @returns {Attribute[]} all note's relation definitions including inherited ones
-     */
-    getRelationDefinitions(name) {
-        return this.getAttributes(RELATION_DEFINITION, name);
-    }
-
     /**
      * @param {string} type - attribute type (label, relation, etc.)
      * @param {string} name - attribute name
@@ -448,6 +457,35 @@ class NoteShort {
         return targets;
     }
 
+    /**
+     * @returns {NoteShort[]}
+     */
+    getTemplateNotes() {
+        const relations = this.getRelations('template');
+
+        return relations.map(rel => this.treeCache.notes[rel.value]);
+    }
+
+    hasAncestor(ancestorNote) {
+        if (this.noteId === ancestorNote.noteId) {
+            return true;
+        }
+
+        for (const templateNote of this.getTemplateNotes()) {
+            if (templateNote.hasAncestor(ancestorNote)) {
+                return true;
+            }
+        }
+
+        for (const parentNote of this.getParentNotes()) {
+            if (parentNote.hasAncestor(ancestorNote)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     /**
      * Clear note's attributes cache to force fresh reload for next attribute request.
      * Cache is note instance scoped.
@@ -466,6 +504,26 @@ class NoteShort {
             .map(attributeId => this.treeCache.attributes[attributeId]);
     }
 
+    /**
+     * Get relations which target this note
+     *
+     * @returns {NoteShort[]}
+     */
+    async getTargetRelationSourceNotes() {
+        const targetRelations = this.getTargetRelations();
+
+        return await this.treeCache.getNotes(targetRelations.map(tr => tr.noteId));
+    }
+
+    /**
+     * Return note complement which is most importantly note's content
+     *
+     * @return {Promise<NoteComplement>}
+     */
+    async getNoteComplement() {
+        return await this.treeCache.getNoteComplement(this.noteId);
+    }
+
     get toString() {
         return `Note(noteId=${this.noteId}, title=${this.title})`;
     }
@@ -483,7 +541,8 @@ class NoteShort {
     }
 }
 
-export default NoteShort;
+export default NoteShort;
+
         
     
 
@@ -499,7 +558,7 @@ export default NoteShort;