#
# Tis file is part of TreeTime, a tree editor and data analyser
#
# Copyright (C) GPLv3, 2015, Jacob Kanev
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA

# -*- coding:utf-8 -*-

from .item import *
from textwrap import wrap
import datetime
from math import floor, ceil, inf
import graphviz as gv
# todo: We're only including these for two error messages. Change them into exceptions and remove this dependency. It breaks the design.
from PyQt6 import QtCore, QtWidgets

class Field:
    """
    A set of instructions to view/display the content of data items.
    Fields are part of nodes, and are stored in templates.
    """
    Types = {'string': 'text', 'url': 'text', 'text': 'text', 'sum': 'numerical', 'count': 'numerical',
             'set': 'any', 'sum-time': 'numerical', 'difference': 'numerical', 'difference-time': 'numerical',
             'mean': 'numerical', 'mean-percent': 'numerical',
             'min': 'numerical', 'max': 'numerical', 'min-string': 'text', 'max-string': 'text',
             'product': 'numerical', 'reciprocal': 'numerical', 'ratio': 'numerical', 'ratio-percent': 'numerical',
             'node-name': 'text', 'node-path': 'text'}

    def __init__(self, node=None, ownFields=False, childFields=False, siblingFields=False, parentFields=False,
                 fieldType=None):
        """Initialises the class, links the source node, field type, and sets the evaluation method."""
        
        self.cache = None
        self.ownFields = ownFields or []
        self.siblingFields = siblingFields or []
        self.childFields = childFields or []
        self.parentFields = parentFields or []
        self.sourceNode = node
        self.getValue = None
        self.getString = None
        self.hidden = False
        self.fieldType = fieldType
        if (self.fieldType != ""):
            self.initFieldType()

    def __deepcopy__(self, memo):
        ''' Overload of the deepcopy function, to avoid copying the node recursively.'''

        if memo is None:
            memo = {}
        
        fieldType = self.__class__
        newField = fieldType.__new__(fieldType)
        
        # cache doesn't get copied
        newField.cache = None
        
        # simple fields are copies
        newField.ownFields = copy.deepcopy(self.ownFields)
        newField.siblingFields = copy.deepcopy(self.siblingFields)
        newField.childFields = copy.deepcopy(self.childFields)
        newField.parentFields = copy.deepcopy(self.parentFields)
        newField.fieldType = copy.deepcopy(self.fieldType)
        newField.hidden = copy.deepcopy(self.hidden)

        # the source node is a link
        newField.sourceNode = self.sourceNode
        
        # functions are set
        if (newField.fieldType != ""):
            newField.initFieldType()
        
        memo[id(self)] = newField
        return newField

    @staticmethod
    def getFieldValue(field):
        """
        :param field: the field to get the value from
        :return: Depending on the field type, the current value. Mostly this is just the content of the "content" key,
        but with timers this is different.
        """
        if field["type"] == "timer":
            running_since = field.get("running_since")
            partial = field["content"]
            running_since = running_since and datetime.datetime.strptime(running_since,
                                                                      "%Y-%m-%d %H:%M:%S")
            if running_since:
                elapsed = datetime.datetime.now() - running_since
                return partial \
                       + elapsed.days * 24.0 \
                       + elapsed.seconds / 60.0 / 60.0 \
                       + elapsed.microseconds / 60.0 / 60.0 / 1000000.0
            else:
                return partial
        else:
            return field["content"]

    def getFieldValues(self, sort=False):
        """ Gets all values of all related fields in a list.
        Order is: own fields first, then child fields, then sibling fields, then parent fields.
        Item fields of the same name have precedence over tree fields.
        """

        values = []
        
        # look in own fields
        node = self.sourceNode
        if node.item is not None: # don't try to get values from the root node
            for f in self.ownFields:
                if f in node.fields:
                    values += [node.fields[f].getValue()]
                elif f in node.item.fields:
                    values += [Field.getFieldValue(node.item.fields[f])]
        
        # look in child fields
        node = self.sourceNode
        for f in self.childFields:
            for c in sort and sorted(node.children, key=lambda n: n.name) or node.children:
                if f in c.fields:
                    values += [c.fields[f].getValue()]
                elif c.item and c.item.fields and f in c.item.fields:
                    values += [Field.getFieldValue(c.item.fields[f])]

        node = self.sourceNode.parent
        if node:

            # look in sibling fields
            for f in self.siblingFields:
                for c in sort and sorted(node.children, key=lambda n: n.name) or node.children:
                    if c != self.sourceNode:
                        if f in c.fields:
                            values += [c.fields[f].getValue()]
                        elif f in c.item.fields:
                            values += [Field.getFieldValue(c.item.fields[f])]

            # look in parent fields
            for f in self.parentFields:
                if f in node.fields:
                    values += [node.fields[f].getValue()]
                elif f in node.item.fields:
                    values += [Field.getFieldValue(node.item.fields[f])]
        
        # return
        return values

    def initFieldType(self):
        
        if self.fieldType == "string":
            self.getValue = self.getValueString
            self.getString = self.getStringUnchanged
        elif self.fieldType == "url":
            self.getValue = self.getValueString
            self.getString = self.getStringUnchanged
        elif self.fieldType == "text":
            self.getValue = self.getValueString
            self.getString = self.getStringUnchanged
        elif self.fieldType == "count":
            self.getValue = self.getValueCount
            self.getString = self.getStringRounded
        elif self.fieldType == "set":
            self.getValue = self.getValueSet
            self.getString = self.getStringSet
        elif self.fieldType == "sum":
            self.getValue = self.getValueSum
            self.getString = self.getStringRounded
        elif self.fieldType == "sum-time":
            self.getValue = self.getValueSum
            self.getString = self.getStringTime
        elif self.fieldType == "difference":
            self.getValue = self.getValueDifference
            self.getString = self.getStringRounded
        elif self.fieldType == "difference-time":
            self.getValue = self.getValueDifference
            self.getString = self.getStringTime
        elif self.fieldType == "mean":
            self.getValue = self.getValueMean
            self.getString = self.getStringRounded
        elif self.fieldType == "mean-percent":
            self.getValue = self.getValueMean
            self.getString = self.getStringPercent
        elif self.fieldType == "min":
            self.getValue = self.getValueMin
            self.getString = self.getStringRounded
        elif self.fieldType == "max":
            self.getValue = self.getValueMax
            self.getString = self.getStringRounded
        elif self.fieldType == "min-string":
            self.getValue = self.getValueMinString
            self.getString = self.getStringUnchanged
        elif self.fieldType == "max-string":
            self.getValue = self.getValueMaxString
            self.getString = self.getStringUnchanged
        elif self.fieldType == "product":
            self.getValue = self.getValueProduct
            self.getString = self.getStringRounded
        elif self.fieldType == "reciprocal":
            self.getValue = self.getValueReciprocal
            self.getString = self.getStringRounded
        elif self.fieldType == "ratio":
            self.getValue = self.getValueRatio
            self.getString = self.getStringRounded
        elif self.fieldType == "ratio-percent":
            self.getValue = self.getValueRatio
            self.getString = self.getStringPercent
        elif self.fieldType == "node-name":
            self.getValue = self.getValueNodeName
            self.getString = self.getStringUnchanged
        elif self.fieldType == "node-path":
            self.getValue = self.getValueNodePath
            self.getString = self.getStringUnchanged
        else:
            if self.fieldType:
                print(f'... error: field type {self.fieldType} does not exist.')

    def getStringPercent(self):
        if self.sourceNode and self.getValue:
            v = self.getValue()
            try:
                if v:
                    whitespace = ""
                    if v < 0.1:
                        whitespace = "  "
                    elif v < 1.0:
                        whitespace = " "
                    return whitespace + str(round(100*v)) + " %"
                else:
                    return ""
            except (TypeError, ValueError):
                message = (f'Error in calculating value for field "{self.name}".\n'
                           'Please check your tree definition for type errors.')
                print(message)
                msgBox = QtWidgets.QMessageBox()
                msgBox.setText(message)
                msgBox.setWindowTitle("TreeTime Error")
                msgBox.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok)
                result = msgBox.exec()
                return "undefined"
        else:
            return ""

    def getStringSet(self):
        if self.sourceNode and self.getValue:
            return ', '.join(sorted([str(n) for n in self.getValue()]))
        else:
            return "[undefined]"

    def getStringTime(self):
        if self.sourceNode and self.getValue:
            v = self.getValue()
            try:
                if v:
                    if v < 0:
                        adjust = 0.49999
                    else:
                        adjust = -0.49999
                    hours = round(v + adjust)
                    minutes = round(60.0*(v - hours) + adjust)
                    seconds = round(3600.0*((v - hours) - minutes/60.0) + adjust)
                    sign = v < 0.0 and "- " or "  "
                    return "{}{:02d}:{:02d}:{:02d}".format(sign, abs(hours), abs(minutes), abs(seconds))
                else:
                    return ""
            except (TypeError, ValueError):
                print(f"Calculation error in node {self.sourceNode.name} for field of type {self.fieldType}. "
                      f"Type mismatch, please check field definition.")
                return "undefined"
        else:
            return ""

    def getStringUnchanged(self):
        if self.sourceNode and self.getValue:
            return str(self.getValue())
        else:
            return "[undefined]"

    def getStringRounded(self):
        if self.sourceNode and self.getValue:
            value = self.getValue()
            if value:
                try:
                    return str(round(value, 3))
                except (TypeError, ValueError):
                    print(f"Calculation error in node {self.sourceNode.name} for field of type {self.fieldType}. "
                          f"Type mismatch, please check field definition.")
                    return "undefined"
            else:
                return ""
        else:
            return "[undefined]"

    def getValueNodeName(self):
        if self.cache is not None:
            return self.cache
        else:
            s = ""
            item = self.sourceNode.item
            if item is None:
                return s
            for t in self.parentFields:

                # find tree
                tree = self.sourceNode
                while tree.parent is not None:
                    tree = tree.parent
                if type(tree) == Forest:
                    tree = tree.children[t]

                # find node in tree
                if type(tree) == Tree:
                    path = item.trees[t]
                    node = tree.findNode(path)
                    if node is not None:
                        parent = node.parent
                        if parent is not None:
                            s += parent.name
            return s

    def getValueNodePath(self):
        if self.cache is not None:
            return self.cache
        else:
            s = ""
            item = self.sourceNode.item
            if item is None:
                return s
            for t in self.parentFields:

                # find tree
                tree = self.sourceNode
                while tree.parent is not None:
                    tree = tree.parent
                if type(tree) == Forest:
                    tree = tree.children[t]

                # find node in tree
                if type(tree) == Tree:
                    path = item.trees[t]
                    node = tree.findNode(path)
                    if node and node.parent:
                        parent = node.parent
                        s += parent.name
                        while parent.parent and parent.parent.parent and parent.parent.parent.parent : # don't display forest or tree names
                            parent = parent.parent
                            s = parent.name + " | " + s
            return s

    def getValueString(self):
        values = self.getFieldValues(sort=True)
        s = ''
        for v in values:
            if v:     # either sum up using the value
                s += v
            else:     # or the neutral element for addition ('')
                s += ''
        return s

    def getValueSet(self):
        values = self.getFieldValues(sort=True)
        result_set = set()
        for v in values:
            if isinstance(v, set):
                result_set = result_set | v
            elif v:     # either sum up using the value
                result_set.add(v)
        return result_set

    def getValueCount(self):
        values = self.getFieldValues()
        sum = 0
        for v in values:
            if v:     # ignore missing value
                sum += 1
            else:     # or the neutral element for addition (0)
                sum += 0
        return sum

    def getValueSum(self):
        values = self.getFieldValues()
        sum = 0
        for v in values:
            if v:     # either sum up using the value
                try:
                    sum += v
                except:
                    pass
            else:     # or the neutral element for addition (0)
                sum += 0
        return sum

    def getValueDifference(self):
        """
        Calculates different a - b - c - d of values a,b,c,d
        """
        values = self.getFieldValues()
        difference = 0
        first = True
        for v in values:
            if first:
                if v:
                    try:
                        difference += v     # first element is positive
                    except:
                        pass
                first = False
            else:
                if v:
                    try:
                        difference -= v     # all other elements are negative
                    except:
                        pass
        return difference

    def getValueMin(self):
        values = self.getFieldValues()
        minValue = inf
        for v in values:
            if v < minValue:     # either sum up using the value
                minValue = v
        return minValue

    def getValueMax(self):
        values = self.getFieldValues()
        maxValue = -inf
        for v in values:
            if v > maxValue:     # either sum up using the value
                maxValue = v
        return maxValue

    def getValueMinString(self):
        values = self.getFieldValues()
        minValue = ""
        # find minimum ignoring "" (otherwise empty fields will overrule everything)
        for v in values:
            if v and (not minValue or v < minValue):
                minValue = v
        return minValue

    def getValueMaxString(self):
        values = self.getFieldValues()
        maxValue = ""
        for v in values:
            if v > maxValue:
                maxValue = v
        return maxValue

    def getValueMean(self):
        values = self.getFieldValues()
        sum = 0.0
        n = 0.0
        for v in values:
            if v:     # in case of 'None', use neutal elements of 0 for addition, and 1 for multiplication
                try:
                    n += 1.0
                    sum += v
                except:
                    pass
        if n > 0.0:
            sum = sum/n
        else:
            sum = None
        return sum

    def getValueProduct(self):
        """
        Returns the product a*b*c*d*... of field values a,b,c,d...
        """
        values = self.getFieldValues()
        prod = 1.0
        for v in values:
            try:
                prod *= v
            except:
                pass    # same as: prod *= 1.0
        return prod

    def getValueReciprocal(self):
        """
        Returns the reciprocal 1/(a+b+c+d+...) of field values a,b,c,d...
        """
        values = self.getFieldValues()
        rec = 0.0
        for v in values:
            try:
                rec += v
            except:
                pass    # same as: prod *= 1.0
            try:
                rec = 1.0/rec
            except:
                rec = None
        return rec

    def getValueRatio(self):
        """
        Returns the ratio a/(b+c+d+e+...) of field values a,b,c,d,e,...
        """
        values = self.getFieldValues()
        if len(values) < 2:
            return None
        else:
            denom = values[0]
            sum = 0
            for v in values[1:]:
                if v:
                    try:
                        sum += v
                    except:
                        pass
            if sum != 0:
                return denom/sum
            else:
                return None
    
    def writeToString(self):
        string = "    field-type " + json.dumps(self.fieldType) + "\n"
        string += "        own-fields " + json.dumps(self.ownFields) + "\n"
        string += "        child-fields " + json.dumps(self.childFields) + "\n"
        string += "        sibling-fields " + json.dumps(self.siblingFields) + "\n"
        string += "        parent-fields " + json.dumps(self.parentFields) + "\n"
        if self.hidden:
            string += "        hidden\n"
        return string

    def readFromString(self, string):
        
        s = string.split("\n        field-type ")
        name = json.loads(s[0])
        s = s[1].split("\n        own-fields ")
        try:
            self.fieldType = json.loads(s[0])
        except:
            self.printReadError(name, "field-type", s[0])
        s = s[1].split("\n        child-fields ")
        try:
            self.ownFields = json.loads(s[0])
        except:
            self.printReadError(name, "own-fields", s[0])
        s = s[1].split("\n        sibling-fields ")
        try:
            self.childFields = json.loads(s[0])
        except:
            self.printReadError(name, "child-fields", s[0])
        s = s[1].split("\n        parent-fields ")
        try:
            self.siblingFields = json.loads(s[0])
        except:
            self.printReadError(name, "sibling-fields", s[0])
        s = s[1].split("\n        hidden")
        try:
            self.parentFields = json.loads(s[0])
        except:
            self.printReadError(name, "parent-fields", s[0])
        if len(s) == 2:
            self.hidden = True
        self.initFieldType()
        
        return name

    def printReadError(self, name, parameter, string):
        print('error reading field parameter "' + parameter + '" of field "' + name + '": definition string "' + string + '" cannot be parsed.')


class Node:
    """
    A tree structure consisting of nodes that are parents of nodes etc. A node is a view-object to display one item.
    """
    
    def __init__(self, parent, tree, path):
        self.parent = parent
        self.children = []
        self.item = None
        self.name = ""
        self.fields = {}
        self.tree = tree
        self.path = path
        self.viewNode = None
        self.nameChangeCallback = None
        self.fieldChangeCallback = None
        self.deletionCallback = None
        self.moveCallback = None
        self.fieldNameChangeCallback = None
        self.fieldOrderChangeCallback = None

    @staticmethod
    def _wrap_lines(raw_lines, chars=70):
        """
        Helper function to wrap long text
        :param string: Input string, in one single line
        :return: Array with text wrapped at 70 chars, preserving newlines, but trimming space (no double new lines)
        """
        lines = []
        for line in [s.strip() for s in raw_lines.split('\n')]:
            lines += wrap(line, chars) or '\n'
        return lines

    def _evaluateContext(self, fields, context):
        """
        Helper function, calculates fieds, context, and whether we're in the current node, based on the context path.
        :param fields: the initial "fields" variable. If the context is "False", that variable will be
            returned in the "fields" return value unchanged. This is important in non-context display settings.
        :param context: The path of the node that is to be displayed as "node with context".
        :return: fields, children, context_current - three bools indicating whether the calling node should display
            its children (yes, if it is a sibling in the ancestry path, otherwise no), its fields (yes, if it is the
            target node and we have fields enabled in the gui, otherwise no), and whether it is the target
            node (interesting for separate css things)
        """
        children = True
        fields_local = fields
        context_current = False
        if context:
            fields_local = False
            children = False

            # display children if we're a direct ancestor
            if len(self.path) <= len(context) and self.path == context[:len(self.path)]:
                children = True

                # display fields if we're the actual target node
                if len(self.path) == len(context):
                    fields_local = fields
                    context_current = True

        return fields_local, children, context_current

    def map(self, function, parameter, depthFirst):
        ''' Applies the function to each node in the tree. The function must receive one parameter and return one
        parameter. The return value is used as parameter for the next function call. The value Parameter is used in the
        first call.'''
        
        # if depthFirst then recurse first and apply later
        if depthFirst:
            for c in self.children:
                parameter = c.map(function, parameter, depthFirst)
                
            return function(self, parameter)
        else:
            parameter = function(self, parameter)
            for c in self.children:
                parameter = c.map(function, parameter, depthFirst)
                
            return parameter

    def printForest(self, indent=0):
        
        # create leading white space
        s = ""
        for i in range(indent):
            s += "    "
        s += " → " + str(self.tree)
        for p in self.path:
            s += "[" + str(p) + "]"
        s += "    "
        if self.item is not None:
            for p in self.item.trees[self.tree]:
                s += "[" + str(p) + "]"
        s += " " + self.__class__.__name__ + " "
        
        # append item content and print
        s += self.name
        for name,field in self.fields.items():
            if not field.hidden:
                s += " [" + name + "] " + str(field.getString())
        print(s)
        
        # recurse
        for i in range(len(self.children)):
            self.children[i].printForest(indent + 1)

    def to_csv(self, nodeNames=True, fieldNames=True, fieldContent=True , context=False, sep=',', treeColumn=True,
               first=True, depth=-1):
        """
        Create a csv representation of the current branch.
        :fieldOrder: The order of fields in the tree
        :first: If this is the first call (first=True), add a title line with field names,
                on recursive calls (=False) don't
        :depth: A number listing how many levels should be in the export
        :return: CSV. Quotes become double-quotes, fields with commas, line breaks or quotes are quoted,
            the first field is called "Tree" and contains the tree structure by pre-pending parents to each node, the
            second field is called "Name" and contains the node name. All other fields from the tree definition follow.
            Children are sorted alphabetically.

            Tree,Name,"Spent Hours"
            "Projects | Build Robot","Build Robot",10
            "Projects | Build Robot | Start Working",Design,20
        """

        def clean_field(text):
            text.replace('"', '""')
            if '"' in text or '\n' in text or sep in text:
                text = '"' + text + '"'
            return text

        # evaluate context
        fields_local, children, context_current = self._evaluateContext(fieldNames or fieldContent, context)
        csv = ""

        # find root node
        tree = self
        while tree.parent.parent is not None:
            tree = tree.parent

        # add title line
        if first and fieldNames:
            csv = 'Tree'+sep+'Name'
            for f in tree.fieldOrder:
                csv += sep
                if fieldNames:
                    csv += clean_field(f)
            csv += '\n'

        # add node path
        if treeColumn:
            field = ''
            pipe = False
            if len(self.path) > 1:
                for n in range(1, len(self.path)):
                    field += pipe and ' | ' or ''
                    field += tree.findNode(self.path[:n]).name
                    pipe = True
            field += pipe and ' | ' or ''
            field += self.name
            csv += clean_field(field)
            if nodeNames:
                csv += sep

        # add node name
        if nodeNames:
            csv += self.name

        # add fields
        if fields_local and fieldContent:
            for f in tree.fieldOrder:
                csv += sep + clean_field(self.fields[f].getString())
        csv += '\n'

        # add children
        if depth and children:
            for c in sorted(self.children, key=lambda x: x.name):
                csv += c.to_csv(first=False, nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                context=context, sep=sep, treeColumn=treeColumn, depth=depth - 1)

        # return
        return csv

    def to_image(self, nodeNames=True, fieldNames=True, fieldContent=True , context=False, depth=-1, engine='dot',
                 format='png', graph=False, parent_id=False, colour='blue', exclude_root=False, invisible_root=False,
                 current_depth=0):
        """ Creates an image (png/svg) using graphics with different style options
        :param nodeNames: Whether (True/False) to include node names in the output
        :param fieldNames: Whether (True/False) to include field names in the output
        :param fieldContent: Whether (True/False) to include field content in the output
        :param context: For handing information down recursively
        :param engine: The graphviz engine to use
        :param colour: The colour of the first node
        :param exclude_root: Whether to exclude the root node in plotting
        :param first: Whether this is the final call (start/end of recursion)
        :return: A bitmap
        """

        def quote_string(str):
            """
            :param str: The string to quote
            :return: String with characters "><& replaced by their html entities
            """
            return str.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')\
                      .replace('"', '&quot;').replace('?', '&#63;')

        # background colours, applied by depth: blue, red, yellow, green
        colours = {'blue': '#f4f4ff', 'purple': '#fbf4ff', 'red': '#fff4f4', 'orange': '#fffbf2',
                       'yellow': '#fffff2', 'green': '#fafff3', 'turquoise': '#f4ffff'}
        next_colour = {'blue': 'purple', 'purple': 'red', 'red': 'orange', 'orange': 'yellow', 'yellow': 'green',
                           'green': 'turquoise', 'turquoise': 'blue'}

        # evaluate context
        fields_local, children, context_current = self._evaluateContext(fieldNames or fieldContent, context)
        # if top call, create graph object
        if not current_depth:
            graph_attrs = {'twopi': {'overlap': 'scalexy', 'sep': '-0', 'outputorder': 'nodesfirst'},
                           'neato': {'overlap': 'prism', 'sep': '+20', 'maxiter': '500', 'epsilon': '0.00001',
                                     'outputorder': 'nodesfirst'},
                           'sfdp': {'overlap': 'prism', 'sep': '+10', 'outputorder': 'nodesfirst',
                                    'smoothing': 'spring'},
                           'dot': {'overlap': 'prism', 'sep': '+10', 'outputorder': 'nodesfirst'},
                           'circo': {'overlap': 'prism', 'sep': '+10', 'outputorder': 'nodesfirst'}}
            graph = gv.Digraph(graph_attr=graph_attrs[engine])

        # add myself invisibly
        if invisible_root:
            # create id
            node_id = "invis"
            # add invisible parent for layout reasons
            graph.node('invis', '', style='invis', root='true')

        elif exclude_root:
            node_id = False

        # add myself, using the path as ID
        else:

            # create id
            node_id = "{}".format(self.path)

            # create fields string
            if fields_local:
                field_string = ""
                for name, field in self.fields.items():
                    if not field.hidden:
                        content = field.getString().strip()
                        name_string = (fieldNames and f'<FONT FACE="Helvetica" POINT-SIZE="10">{quote_string(name)}</FONT>'
                                       or '')
                        if content:     # wrap field content, larger bits of text start with a newline
                            lines = [quote_string(l) for l in Node._wrap_lines(content, chars=50)]
                            if field.fieldType == "url":
                                link = quote_string(len(content) > 50 and content[:50]+'...' or content)
                                if fieldContent:
                                    content_string = (f'<TD ALIGN="LEFT" VALIGN="TOP" HREF="{quote_string(content)}">'
                                                      f'<FONT FACE="Helvetica" COLOR="#0000a0" POINT-SIZE="10">'
                                                      f'{link}</FONT></TD>')
                                else:
                                    content_string = ''
                                field_string += ('<TR><TD ALIGN="RIGHT" VALIGN="TOP">'
                                                 f'{name_string}</TD>{content_string}</TR>')
                            else:
                                content_string = fieldContent and ('<FONT FACE="Helvetica" POINT-SIZE="10">'
                                                                   + '<BR ALIGN="LEFT"/>'.join(lines)
                                                                   + '</FONT>') \
                                                              or ''
                                field_string += ('<TR><TD ALIGN="RIGHT" VALIGN="TOP">'
                                                 f'{name_string}</TD>'
                                                 f'<TD ALIGN="LEFT" VALIGN="TOP">{content_string}</TD></TR>')
            else:
                field_string = ""

            # node title
            if context:
                if context_current:
                    font_size = 2.5
                else:
                    font_size = 1.0 + 1.0 / (
                                1.0 + max((len(context) - len(self.path)), 0))  # 2 for target, decay to 1.0
            else:
                font_size = 1.0 + 2.0 / (1.0 + current_depth)  # start with 3.0, exponentially decay to 1.0

            # assemble label
            title = '<BR ALIGN="LEFT"/>'.join(Node._wrap_lines(quote_string(self.name), chars=30))
            if nodeNames:
                label = (f'<<TABLE><TR><TD COLSPAN="2">'
                         f'<FONT FACE="Helvetica" POINT-SIZE="{int(font_size*10)}">{title}</FONT>'
                         f'</TD></TR>{field_string}</TABLE>>')
            else:
                if field_string:
                    label = (f'<<TABLE>{field_string}</TABLE>>')
                else:
                    label = ' '

            # are we the root node?
            if not parent_id:
                root_node = 'true'
            else:
                root_node = 'false'

            # add node
            graph.node(node_id, label, shape="rectangle", color=colours[colour], style='filled', root=root_node)

            # add connection to parent
            if parent_id:
                if parent_id == 'invis':
                    graph.edge(parent_id, node_id, style='invis', len='0.2')
                else:
                    graph.edge(parent_id, node_id, color='gray', len='0.2')

        # recurse over children
        if children and depth:
            for c in sorted(self.children, key=lambda c: c.name):
                parent_id = node_id
                c.to_image(nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent, context=context,
                           depth=depth-1, engine=engine, graph=graph, parent_id=parent_id, colour=next_colour[colour],
                           current_depth=current_depth+1)

        # end of recursion, create picture
        image = None

        if not current_depth:
            try:
                # pre-layout to avoid edge
                if engine == 'twopi':
                    image = graph.pipe(format=format, engine=engine)
                elif engine == 'dot':
                    image = graph.unflatten(stagger=5,
                                            chain=3,
                                            fanout=3).pipe(format=format, engine=engine)
                else:    # sfdp/neato
                    try:     # sometimes sfdp dies with a strange error: "SpringSmoother_new: Assertion `nz > 0' failed"
                        image = graph.pipe(format=format, engine=engine)
                    except:
                        image = graph.pipe(format=format, engine='neato')
            except:
                # almost impossible to include graphviz correctly in pyinstaller for linux.
                message = ('This software relies on the GraphViz library for diagram drawing.<br/>'
                           'Please install graphviz on your system. <br/> Instructions are here: '
                           '<a href="https://graphviz.org/download/">graphviz.org/download</a>.')
                msgBox = QtWidgets.QMessageBox()
                msgBox.setText(message)
                msgBox.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextBrowserInteraction)
                msgBox.setTextFormat(QtCore.Qt.TextFormat.RichText)
                msgBox.setWindowTitle("External Library Missing")
                msgBox.setStandardButtons(
                    QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel)
                result = msgBox.exec()


        return image

    def to_txt(self, nodeNames=True, fieldNames=True, fieldContent=True , context=False, lastitem=[], depth=-1):
        """
        Create a text representation of the current branch.
        :param lastitem: A list of booleans showing whether the great-grandparent, grandparent, parent, is the
                         last item of the list of siblings
        :param nodeNames: if true, all node titles are included, if false, only fields exported
        :param fieldNames: if true, all fieldNames are written in front of the field, if false, field content only
        :param fieldContent: if true, field content is shown, if false, no field content is shown
                             (mostly fieldNames will be False too i that case)
        :return: a string like this:

                    █ abc
                      def

                    █ abc
                    │ def
                    │
                    ┝━━█ abc
                    │  │ def
                    │  │
                    │  ┝━━█ abc
                    │  │    def
                    │  │
                    │  ┕━━█ abc
                    │        def
                    │
                    ┕━━█ abc
                       │ def
                       │
                       ┝━━█ abc
                       │    def
                       │
                       ┕━━█ abc
                            def
        """

        text = ""

        # evaluate context
        fields_local, children, context_current = self._evaluateContext(fieldNames or fieldContent, context)

        # create leading tree graphics
        # please pay attention: The spaces in the graphic strings are special unicode figure spaces
        if children and self.children and depth:
            pre_node_prefix = ""
            first_line_prefix = "●  "
            line_prefix = "│  "
        else:
            pre_node_prefix = ""
            first_line_prefix = "●  "
            line_prefix = "   "
        n = 0
        for last in lastitem[::-1]:
            if not last:
                if n:
                    pre_node_prefix = "│  " + pre_node_prefix
                    first_line_prefix = "│  " + first_line_prefix
                    line_prefix = "│  " + line_prefix
                else:
                    pre_node_prefix = "│  " + pre_node_prefix
                    first_line_prefix = "├──" + first_line_prefix
                    line_prefix = "│  " + line_prefix
            else:
                if n:
                    pre_node_prefix = "   " + pre_node_prefix
                    first_line_prefix = "   " + first_line_prefix
                    line_prefix = "   " + line_prefix
                else:
                    pre_node_prefix = "│  " + pre_node_prefix
                    first_line_prefix = "└──" + first_line_prefix
                    line_prefix = "   " + line_prefix
            n += 1

        # append item content and print
        text += pre_node_prefix + "\n"

        # add node name (possibly multi-line)
        first = True
        name_lines = (nodeNames and Node._wrap_lines(self.name)) \
                     or (fieldNames and ['—' * 74]) \
                     or ['    ' + '—'*70]
        for line in name_lines:
            line = line == '\n' and line or line + '\n'
            if first:
                text += first_line_prefix + line
                first = False
            else:
                text += line_prefix + line

        if fields_local:
            first_field = True
            for name, field in self.fields.items():
                if not field.hidden:

                    if first_field:
                        text += line_prefix + "—"*(80 - len(line_prefix)) + "\n"  # empty line after title
                        first_field = False

                    # wrap field content, larger bits of text start with a newline
                    lines = fieldContent and Node._wrap_lines(field.getString()) or []

                    # empty array if only whitespace content
                    if lines == ['\n']:
                        lines = []

                    # start on new line if field content spans multiple lines
                    if fieldNames:
                        if len(lines) > 1:
                            lines = [''] + lines

                    # assemble final text with tree decorations
                    first = True
                    for line in lines:
                        line = line == '\n' and line or line + '\n'
                        if first and fieldNames:
                            text += line_prefix + name + ": " + line
                            first = False
                        else:
                            text += line_prefix + "    " + line

        # recurse
        if children and depth:
            sorted_children = sorted(self.children, key=lambda c: c.name)
        else:
            sorted_children = []
        if len(sorted_children) > 1:
            childprefix = lastitem.copy()
            childprefix.append(False)
            for i in range(len(sorted_children)-1):
                text += sorted_children[i].to_txt(nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                                  context=context, lastitem=childprefix, depth=depth-1)
        if len(sorted_children):
            childprefix = lastitem.copy()
            childprefix.append(True)
            text += sorted_children[-1].to_txt(nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent,
                                               context=context, lastitem=childprefix, depth=depth-1)

        # return
        return text

    def to_md(self, nodeNames=True, fieldNames=True, fieldContent=True , context=False, depth=-1, current_depth=1):
        """
        Create a markdown representation of the current branch.
        """

        text = ""

        # evaluate context
        fields_local, children, context_current = self._evaluateContext(fieldNames or fieldContent, context)

        # add node name
        if nodeNames:
            text += '\n' + (min(current_depth, 6)) * '#' + ' ' + self.name.strip() + '\n\n'

        if fields_local:
            for name, field in self.fields.items():
                if not field.hidden:

                    if field.sourceNode:
                        # wrap field content, larger bits of text start with a newline
                        lines = fieldContent and Node._wrap_lines(field.getString().replace('\n', '\n'), chars=120) or []

                        # empty array if only whitespace content
                        if lines == ['\n']:
                            lines = []

                        # assemble final text only if field is used
                        first = True
                        for line in lines:
                            if first and fieldNames:
                                text += '*  *' + name.strip() + ':* ' + line + '\n'
                                first = False
                            else:
                                text += '    ' + line.replace('#', '\\#') + '\n'

        # recurse
        if children and depth:
            sorted_children = sorted(self.children, key=lambda c: c.name)
        else:
            sorted_children = []

        for child in sorted_children:
            text += child.to_md(nodeNames=nodeNames, fieldNames=fieldNames, fieldContent=fieldContent, context=context,
                                depth=depth-1, current_depth=current_depth+1)

        # return
        return text

    def to_html(self, header=False, footer=False, nodeNames=True, fieldNames=True, fieldContent=True , context=False,
                style='tiles', depth=-1, current_depth=0):

        # evaluate context
        fields_local, children, context_current = self._evaluateContext(fieldNames or fieldContent, context)

        # background colours, applied by depth: blue, red, yellow, green
        backgrounds = ['blue', 'purple', 'red', 'orange', 'yellow', 'green', 'turquoise']

        # page header
        if header and style == 'tiles' and not context:
            html = '<!DOCTYPE html><html lang="en">' \
                   + f'<meta charset="utf-8"><title>{self.name} — TreeTime</title><style>' \
                   'body {font-family: sans-serif; color: black; background-color: white; font-size: 0.9em; width: fit-content; height: fit-content; margin: auto;} '\
                   'em {color: #555;}' \
                   'div.red {background-color: #fff9ff;}' \
                   'div.orange {background-color: #fffdf7;}' \
                   'div.yellow {background-color: #fdfff7;}' \
                   'div.green {background-color: #fbfff0;}' \
                   'div.turquoise {background-color: #f8fcff;}' \
                   'div.blue {background-color: #f9f9ff;}' \
                   'div.purple {background-color: #fbf7ff;}' \
                   'div.node {position: relative; float: left; border: 1px solid; margin: 0.6em; padding: 0.6em; width: min-content; border-radius: 1em; border-color: #CCC;}' \
                   'div.name {padding: 0.2em; margin: 0.2em; position: relative; float: left; width: 100%;} ' \
                   'div.fields {position: relative; float: left; clear: left; width: auto; border-top: 1px solid; border-color: #808080;} ' \
                   'div.children {position: relative; float: left; clear: left; width: max-content;} ' \
                   'div.string {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.min-string {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.max-string {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.set {position: relative; float: left; width: 20em; margin: 0.3em; padding: 0.3em; }' \
                   'div.text {position: relative; float: left; width: 20em; margin: 0.3em; padding: 0.3em; }' \
                   'div.url {position: relative; float: left; width: 20em; margin: 0.3em; padding: 0.3em; }' \
                   'div.count {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.sum {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.sum-time {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.difference {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.difference-time {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.min {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.max {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.mean {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.mean-percent {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.product {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.reciprocal {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.ratio {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.ratio-percent {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.node-name {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.node-path {position: relative; float: left; width: 20em; margin: 0.3em; padding: 0.3em; }' \
                   '</style></head><body>'
        elif header and style == 'tiles' and context:
            html = '<!DOCTYPE html><html lang="en">' \
                   + f'<meta charset="utf-8"><title>{self.name} – TreeTime</title><style>' \
                   'body {font-family: sans-serif; color: black; background-color: white; font-size: 1.2em; width: fit-content; height: fit-content; margin: auto;} ' \
                   'em {color: #555;}' \
                   'div.red {background-color: #fff9ff;}' \
                   'div.orange {background-color: #fffdf7;}' \
                   'div.yellow {background-color: #fdfff7;}' \
                   'div.green {background-color: #fbfff0;}' \
                   'div.turquoise {background-color: #f8fcff;}' \
                   'div.blue {background-color: #f9f9ff;}' \
                   'div.purple {background-color: #fbf7ff;}' \
                   'div.node {position: relative; float: left; border: 1px solid; margin: 0.6em; padding: 0.6em; width: min-content; border-radius: 1em; border-color: #CCC;}' \
                   'div.node {position: relative; float: left; border: 1px solid; margin: 0.6em; padding: 0.6em; width: min-content; border-radius: 1em; border-color: #CCC;}' \
                   'div.name {padding: 0.2em; margin: 0.2em; position: relative; float: left; width: 12em;} ' \
                   'div.name_current {padding: 0.2em; margin: 0.2em; position: relative; float: left; width: 48.5em;} ' \
                   'div.fields {position: relative; float: left; clear: left; width: 74em; border-top: 1px solid; border-color: #808080;} ' \
                   'div.children {position: relative; float: left; clear: left; width: max-content;} ' \
                   'div.string {position: relative; float: left; width: 20em; margin: 0.3em; padding: 0.3em; }' \
                   'div.min-string {position: relative; float: left; width: 20em; margin: 0.3em; padding: 0.3em; }' \
                   'div.max-string {position: relative; float: left; width: 20em; margin: 0.3em; padding: 0.3em; }' \
                   'div.set {position: relative; float: left; width: 40em; margin: 0.3em; padding: 0.3em; }' \
                   'div.text {position: relative; float: left; width: 40em; margin: 0.3em; padding: 0.3em; }' \
                   'div.url {position: relative; float: left; width: 20em; margin: 0.3em; padding: 0.3em; }' \
                   'div.count {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.sum {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.sum-time {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.difference {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.difference-time {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.min {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.max {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.mean {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.mean-percent {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.product {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.reciprocal {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.ratio {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.ratio-percent {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.node-name {position: relative; float: left; width: 20em; margin: 0.3em; padding: 0.3em; }' \
                   'div.node-path {position: relative; float: left; width: 40em; margin: 0.3em; padding: 0.3em; }' \
                   '</style></head><body>'
        elif header and style == 'list':
            html = '<!DOCTYPE html><html lang="en">' \
                   + f'<meta charset="utf-8"><title>{self.name} — TreeTime</title><style>' \
                   'body {font-family: sans-serif; color: black; background-color: white; font-size: 0.9em;} ' \
                   'em {color: #555;}' \
                   'div.red {background-color: #fff9ff;}' \
                   'div.orange {background-color: #fffdf7;}' \
                   'div.yellow {background-color: #fdfff7;}' \
                   'div.green {background-color: #fbfff0;}' \
                   'div.turquoise {background-color: #f8fcff;}' \
                   'div.blue {background-color: #f9f9ff;}' \
                   'div.purple {background-color: #fbf7ff;}' \
                   'div.node {position: relative; float: left; clear: left; border-left: 2px solid; margin: 0.6em; padding: 0.6em; width: max-content; border-color: #808080;}' \
                   'div.name {padding: 0.2em; margin: 0.2em; position: relative; float: left;} ' \
                   'div.fields {position: relative; float: left; width: max-content; border-color: #808080;}' \
                   'div.children {position: relative; float: left; clear: left; width: max-content;} ' \
                   'div.string {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.min-string {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.max-string {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.set {position: relative; float: left; width: 30em; margin: 0.3em; padding: 0.3em; }' \
                   'div.text {position: relative; float: left; width: 30em; margin: 0.3em; padding: 0.3em; }' \
                   'div.url {position: relative; float: left; width: 20em; margin: 0.3em; padding: 0.3em; }' \
                   'div.count {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.sum {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.sum-time {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.difference {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.difference-time {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.min {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.max {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.mean {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.mean-percent {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.product {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.reciprocal {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.ratio {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.ratio-percent {position: relative; float: left; width: 5em; margin: 0.3em; padding: 0.3em; }' \
                   'div.node-name {position: relative; float: left; width: 10em; margin: 0.3em; padding: 0.3em; }' \
                   'div.node-path {position: relative; float: left; width: 20em; margin: 0.3em; padding: 0.3em; }' \
                   '</style></head><body>'
        elif header and style == 'document':
            html = '<!DOCTYPE html><html lang="en">' \
                   + f'<meta charset="utf-8"><title>{self.name} — TreeTime</title><style>' \
                   'body {font-family: sans-serif; color: black; background-color: white;} '\
                   'em {color: #555;}' \
                   'div.red {background-color: #fff;}' \
                   'div.orange {background-color: #fff;}' \
                   'div.yellow {background-color: #fff;}' \
                   'div.green {background-color: #fff;}' \
                   'div.turquoise {background-color: #fff;}' \
                   'div.blue {background-color: #fff;}' \
                   'div.purple {background-color: #fff;}' \
                   'div.node {position: relative; float: left; border: none; padding: 1%; width: 97%;} ' \
                   'div.name {padding: 1%; position: relative; float: left; width: 99%; color: #008} ' \
                   'div.name_current {padding: 1%; position: relative; float: left; width: 99%; color: #008} ' \
                   'div.fields {position: relative; float: left; clear: left; width: 99%; border-top: 1px solid; border-color: #00a;} ' \
                   'div.children {position: relative; float: left; clear: left; width: 100%;} ' \
                   'div.string {position: relative; float: left; width: 25%; padding: 1%; }' \
                   'div.min-string {position: relative; float: left; width: 25%; padding: 1%; }' \
                   'div.max-string {position: relative; float: left; width: 25%; padding: 1%; }' \
                   'div.set {position: relative; float: left; width: 100%; padding: 1%; }' \
                   'div.text {position: relative; float: left; width: 100%; padding: 1%; }' \
                   'div.url {position: relative; float: left; width: 45%; padding: 1%; }' \
                   'div.count {position: relative; float: left; width: 10%; padding: 1%; }' \
                   'div.sum {position: relative; float: left; width: 10%; padding: 1%; }' \
                   'div.sum-time {position: relative; float: left; width: 25%; padding: 1%; }' \
                   'div.difference {position: relative; float: left; width: 10%; padding: 1%; }' \
                   'div.difference-time {position: relative; float: left; width: 25%; padding: 1%; }' \
                   'div.min {position: relative; float: left; width: 10%; padding: 1%; }' \
                   'div.max {position: relative; float: left; width: 10%; padding: 1%; }' \
                   'div.mean {position: relative; float: left; width: 10%; padding: 1%; }' \
                   'div.mean-percent {position: relative; float: left; width: 10%; padding: 1%; }' \
                   'div.product {position: relative; float: left; width: 10%; padding: 1%; }' \
                   'div.reciprocal {position: relative; float: left; width: 10%; padding: 1%; }' \
                   'div.ratio {position: relative; float: left; width: 10%; padding: 1%; }' \
                   'div.ratio-percent {position: relative; float: left; width: 10%; padding: 1%; }' \
                   'div.node-name {position: relative; float: left; width: 25%; padding: 1%; }' \
                   'div.node-path {position: relative; float: left; width: 45%; padding: 1%; }' \
                   '</style></head><body>'
        else:
            html = ''

        # node header
        html += '<div class="node {}">'.format(backgrounds[(depth+1) % len(backgrounds)])
        needed_columns = context_current and 5 or 1    # the amount of columns needed by this child

        # font size for title
        if context:
            if context_current:
                font_size = 1.5
            else:
                font_size = 0.5 + 1.0 / (2.0 + max((len(context) - len(self.path)), 0))  # 1 em for target, decay to 0.5
        else:
            font_size = 1.0 + 1.0 / (1.0 + current_depth)  # start with 2 em, exponentially decay to 0.5 em

        # write title
        if nodeNames:
            if style == 'tiles' or style == 'document':
                if context_current:
                    html += f'<div class="name_current" style="font-size: {font_size:0.2f}em">{self.name}</div>'
                else:
                    html += f'<div class="name" style="font-size: {font_size:0.2f}em">{self.name}</div>'
            if style == 'list':
                html += f'<div class="name" style="font-size: {font_size:0.2f}em; ' \
                        f'width: {(20.0 - (1.2*current_depth))/font_size:0.2f}em;">{self.name}</div>'
        elif style == 'list':
            html += f'<div class="name" style="width: {(10.0 - (1.2 * current_depth)):0.2f}em;"></div>'

        # node fields
        if fields_local:
            html += '<div class="fields">'
            for name, field in self.fields.items():
                if not field.hidden:
                    content = field.getString().strip()
                    if content or (style == 'list'):   # in tile mode empty fields are hidden, in list mode always displayed
                        content = fieldContent and content.replace('\n', '<br/>') or ''
                        name_string = fieldNames and f'<em>{name}</em><br/>' or ''
                        if field.fieldType == "url":
                            html += (f'<div class="{field.fieldType}">{name_string}<a href="{content}">'
                                     f'{len(content) > 40 and content[:37]+"..." or content}</a></div>')
                        else:
                            html += f'<div class="{field.fieldType}">{name_string}{content}</div>'
            html += '</div>'

        child_html = ""

        # children
        if children and depth:

            child_count = 0
            sorted_children = sorted(self.children, key=lambda c: c.name)
            group_open = False
            for i in range(len(sorted_children)):

                # write next child
                child_columns = 1     # number of columns needed by child sub-branch
                child_columns, child_html = sorted_children[i].to_html(depth=depth-1,
                                                                       nodeNames=nodeNames, fieldNames=fieldNames,
                                                                       fieldContent=fieldContent, context=context,
                                                                       current_depth=current_depth+1, style=style)

                # start new child group on first child in group of 5, or if child needs more columns than available
                if child_columns > 5-child_count:
                    child_count = 0
                if child_count == 0:
                    if group_open:
                        html += '</div>'
                    html += '<div class="children">'
                    group_open = True

                # add html to main html
                html += child_html
                child_count += child_columns
                needed_columns = max(needed_columns, min(child_count, 5))

                # close child group after fourth child of after node with children
                if child_count >= 5:
                    child_count = 0
                if child_count == 0:
                    html += '</div>'
                    group_open = False

            # node footer
            if group_open:
                html += '</div>'
        html += "</div>"

        # page footer
        if footer:
            html += '</body></html>'

        # finished.
        return needed_columns, html

    def createPathTo(self, item, treeindex, nodeindex, viewtemplate):
        """
        Links an item into a tree, using the given path. Empty nodes get created on the way.
        """
        
        # either recurse deeper
        if nodeindex < len(item.trees[treeindex]):
            n = item.trees[treeindex][nodeindex]
            while n >= len(self.children):
                self.addChild()
            self.children[n].createPathTo(item,treeindex,nodeindex+1,viewtemplate)
        
        # or, if in final node, link item
        else:
            self.item = item
            self.name = item.name
            self.initFields(viewtemplate)
            self.registerCallbacks()

    def registerCallbacks(self):
        """
        Register callbacks from the parent (QNode), so changes from the tree layer
        can be shown in the QT GUI layer
        """
        if self.item is not None:
            self.item.registerNameChangeCallback(self.tree, self.notifyNameChange)
            self.item.registerFieldChangeCallback(self.tree, lambda x: self.notifyFieldChange(x, True))
            self.item.registerDeletionCallback(self.tree, self.notifyDeletion)
            self.item.registerMoveCallback(self.tree, self.notifyMove)
            self.item.registerSelectionCallback(self.tree, lambda x: self.notifySelection(x))
            self.item.registerViewNode(self.tree, self)

    def registerViewNode(self, viewNode):
        """
        Register the respective QT GUI view node with the node
        """
        self.viewNode = viewNode

    def findNode(self, path, currentIndex=None):
        """
        Recursive function to walk down a path in a tree and return the node at the last step
        """

        # if currentIndex==False this function was called with the intention of calling Tree.findNode().
        # return None in this case.
        if currentIndex is None:
            return None

        # either recurse deeper
        if currentIndex < len(path):
            childNumber = path[currentIndex]
            if childNumber >= len(self.children):
                return None
            return self.children[childNumber].findNode(path, currentIndex+1)
        
        # or, return myself
        else:
            return self

    def addChild(self):
        """
        Add a child to a node. The child is a new copy of the default node.
        """
        node = Node(self, self.tree, self.path + [len(self.children)])
        self.children += [node]
        return node

    def addNodeAsChild(self, node):
        """
        Add an existing node to a new parent.
        """
        self.children += [node]
        node.parent = self
        self.renumberChildren()
        node.item.notifyFieldChange("")
        self.notifyNameChange(self.name)

    def removeChild(self, child):
        if child in self.children:
            self.children.remove(child)
            self.renumberChildren()
            for f in self.fields:
                self.notifyFieldChange(f, True)
            child.parent = None

    def renumberChildren(self):
        """
        Correct the paths after children have been removed or added.
        """
        for i, c in enumerate(self.children):
            c.path = self.path + [i]
            if c.item:
                # print("{} {}".format(c.path, c.item.name))
                c.item.trees[self.tree] = self.path + [i]
                c.renumberChildren()

    def addItemAsChild(self, item):
        """
        Creates a node and links it to the item. Updates the item's forest indexes.
        """
        node = self.addChild()
        node.item = item
        node.name = item.name
        node.registerCallbacks()
        item.trees[node.tree] = node.path
        node.initFields(self.fields)
        return node

    def addField(self, name, field):
        """ Adds a new field recursively in the entire tree. Silently, no messages sent. After adding, the visibility
        still needs to be adapted, and then update messages sent through the tree. . The first call does not create deep
        copies, only subsequent recursive calls do. This is to not touch existing references when the top-level fields
        are changed (meta nodes keep pointers to the parameter lists).
        :param name: The field name
        :param field: The field itself
        :return: void
        """
        self.fields[name] = field
        self.fields[name].sourceNode = self
        for c in self.children:
            c.addField(name, copy.deepcopy(field))

    def findNodeByName(self, name):
        """
        Finds the first node of a given name in the tree.
        """
        if self.name == name:
            return self
        else:
            for c in self.children:
                found = c.findNodeByName(name)
                if found is not None:
                    return found
        return None

    def initFields(self, fields):
        self.fields = copy.deepcopy(fields)
        
        # first set all fields to myself
        for name, field in self.fields.items():
            field.sourceNode = self
        
        # and only then send notification
        for name, field in self.fields.items():
            self.notifyFieldChange(name, True)

    def findNodeByName(self, name):
        """
        Finds the first node of a given name in the tree.
        """
        if self.name == name:
            return self
        else:
            for c in self.children:
                found = c.findNodeByName(name)
                if found is not None:
                    return found
        return None

    def registerNameChangeCallback(self, callback):
        self.nameChangeCallback = callback

    def registerFieldChangeCallback(self, callback):
        self.fieldChangeCallback = callback

    def registerDeletionCallback(self, callback):
        self.deletionCallback = callback

    def registerMoveCallback(self, callback):
        self.moveCallback = callback

    def registerSelectionCallback(self, callback):
        self.selectionCallback = callback

    def registerFieldNameChangeCallback(self, callback):
        self.fieldNameChangeCallback = callback

    def registerFieldOrderChangeCallback(self, callback):
        self.fieldOrderChangeCallback = callback

    def notifyNameChange(self, newName):
        """
        Callback used to notify a node of a name change. Changes the name, then
        recreates strings for the fiels 'node-name' and 'node-path'.
        Recurses down the tree to update the node-path strings of all children by calling the function
        notifyParentNameChange.
        """

        # if name is not false, set new name and notify GUI
        if newName:
            self.name = newName
            if self.nameChangeCallback is not None:
                self.nameChangeCallback(newName)

        # always adapt all my fields in all trees (better: call the callback from the item without recursion)
        if self.item:
            for v in self.item.viewNodes:
                v and v.notifyFieldChange(False, False)

        # notify all children, recursive, but only update their fields
        for c in self.children:
            c.notifyNameChange(False)

    def notifyFieldChange(self, fieldName, recursion):
        """
        Callback, called whenever a field in a related item has changed. Updates all fields of all children (but
        stops at first layer), and all siblings.
        :fieldName: A dummy, but needed in order to have the correct number of parameters when used in callbacks
        """

        if self.fieldChangeCallback is not None:
            for f in self.fields:
                self.fieldChangeCallback(f, self.fields[f].getString())
        
        if recursion:
            if self.parent is not None:
                # notify all siblings, non-recursive
                for c in self.parent.children:
                    if c is not self:
                        c.notifyFieldChange(fieldName, False)
                # notify all parents, recursive
                self.parent.notifyFieldChange(fieldName, True)

    def notifyDeletion(self):
        """
        Callback, called when the underlying item was deleted or removed from this tree.
        Recursion removes the complete child branch in this tree.
        """

        # unlink item
        self.item = None
        
        # tell the GUI layer to remove my QNode from its parent
        if self.deletionCallback is not None:
            self.deletionCallback()
            
        # make parent renumber children
        self.parent.removeChild(self)

    def notifyMove(self):
        """
        Callback, called when the underlying item moved to a different parent within the tree.
        Recursion renumbers all children in the new and in the old parent.
        """

        # find new parent and remember old parent
        oldParent = self.parent
        tree = self
        while tree.parent.parent is not None:
            tree = tree.parent
        path = self.item.trees[self.tree]
        newParent = tree.findNode(path[0:-1])

        # move node
        oldParent.removeChild(self)
        newParent.addNodeAsChild(self)

        # tell the GUI layer to move my QNode to its new parent
        if self.moveCallback is not None:
            self.moveCallback()

    def notifySelection(self, select):
        """
        Callback, called whenever the underlying item was selected.
        """

        # tell the GUI layer to select my QNode
        if self.selectionCallback is not None:
            self.selectionCallback(select)

    def removeEmptyNodes(self):
        """
        Recurse to find nodes that have no items. If found, those nodes are removed and their siblings renumbered.
        Called after initial loading of the file.
        """
        for c in self.children:
            c.removeEmptyNodes()
        if not self.item:
            self.notifyDeletion()

    def updateFieldOrderEntry(self, n, newName):
        """ Notifies the QNode of a change in the tree's fieldOrderer in this node (if it is a tree), and in the node's view node, via the
        callback
        :param n: Index of node
        :param newName: New name of node
        :return: void
        """
        if self.fieldNameChangeCallback:
            self.fieldNameChangeCallback(n, newName)
        for c in self.children:
            c.updateFieldOrderEntry(n, newName)

    def updateFieldOrder(self, fieldOrder):
        """ Updates the field order in this node (if it is a tree), and in the node's view node, via the callback
        :param fieldOrder: New fieldOrder dict
        :return: void
        """
        if self.fieldOrderChangeCallback:
            self.fieldOrderChangeCallback(fieldOrder)
        for c in self.children:
            c.updateFieldOrder(fieldOrder)

    def updateFieldVisibility(self, field, hidden):
        """ Propagates a new state of the visibility flag of a field down. Nothing else, just the flag.
        The flag is later used in exports.
        """
        self.fields[field].hidden = hidden
        for c in self.children:
            c.updateFieldVisibility(field, hidden)

    def changeFieldName(self, oldName, newName):
        """
        :param oldName: current field name
        :param newName: new field name, or False (if field is removed)
        :return: True
        """
        if oldName in self.fields.keys():
            print(f'changing "{oldName}" to "{newName}" in node {self.name}')
            if newName:
                self.fields[newName] = self.fields.pop(oldName)
            else:
                self.item.notifyFieldNameChange(oldName, newName)
                self.fields.pop(oldName)
        for c in self.children:
            c.changeFieldName(oldName, newName)
        return True

    def changeFieldType(self, field, newType):
        """
        Changes the type of a field to the new type, sets the appropriate callbacks, and recurses down all children.
        Does not update anything (not possible during changing), this has to be done separately by calling the
        notifyDefinitionChange function.
        :param field: The field name to update
        :param newType: The new type of the field
        :return: void
        """
        self.fields[field].fieldType = newType
        self.fields[field].initFieldType()
        for c in self.children:
            c.changeFieldType(field, newType)

    def changeFieldDefinition(self, fieldName, field):
        """
        Changing the field definition during meta structure editing. Will apply a deep copy of the field to itself,
        and recursively, all children.
        """
        self.fields[fieldName] = copy.deepcopy(field)
        self.fields[fieldName].sourceNode = self
        for c in self.children:
            c.changeFieldDefinition(fieldName, field)

    def changeFieldOwnParameters(self, name, params):
        """
        Recursively changes the own-fields parameter list in all fields. The first call does not create deep copies,
        only subsequent recursive calls do. This is to not touch existing references when the top-level fields are
        changed (meta nodes keep pointers to the parameter lists).
        :param name: Name of the field
        :param params: List of parameters
        :return:
        """
        self.fields[name].ownFields = params
        for c in self.children:
            c.changeFieldOwnParameters(name, copy.deepcopy(params))

    def changeFieldChildParameters(self, name, params):
        """
        Recursively changes the own-fields parameter list in all fields. The first call does not create deep copies,
        only subsequent recursive calls do. This is to not touch existing references when the top-level fields are
        changed (meta nodes keep pointers to the parameter lists).
        :param name: Name of the field
        :param params: List of parameters
        :return:
        """
        self.fields[name].childFields = params
        for c in self.children:
            c.changeFieldChildParameters(name, copy.deepcopy(params))

    def changeFieldSiblingParameters(self, name, params):
        """
        Recursively changes the sibling-fields parameter list in all fields. The first call does not create deep copies,
        only subsequent recursive calls do. This is to not touch existing references when the top-level fields are
        changed (meta nodes keep pointers to the parameter lists).
        :param name: Name of the field
        :param params: List of parameters
        :return:
        """
        self.fields[name].siblingFields = params
        for c in self.children:
            c.changeFieldSiblingParameters(name, copy.deepcopy(params))

    def changeFieldParentParameters(self, name, params):
        """
        Recursively changes the own-fields parameter list in all fields. The first call does not create deep copies,
        only subsequent recursive calls do. This is to not touch existing references when the top-level fields are
        changed (meta nodes keep pointers to the parameter lists).
        :param name: Name of the field
        :param params: List of parameters
        :return:
        """
        self.fields[name].parentFields = params
        for c in self.children:
            c.changeFieldParentParameters(name, copy.deepcopy(params))

    def updateFieldContent(self, fieldName):
        """
        Notifying a change of the field definition during meta structure editing. Will call the notify functions on
        all children, recursively.
        :fieldName: name of field, if False updates all fields
        """
        for c in self.children:
            c.updateFieldContent(fieldName)
        if self.fieldChangeCallback:
            try:
                if fieldName:
                    self.fieldChangeCallback(fieldName, self.fields[fieldName].getString())
                else:
                    for f in self.fields:
                        self.fieldChangeCallback(f, self.fields[f].getString())

            except BaseException as e:
                message = (f'Error when applying new definition for field "{fieldName}:".\n'
                           f'{e}\n'
                           'Please check for circular dependencies and type errors.')
                print(message)
                msgBox = QtWidgets.QMessageBox()
                msgBox.setText(message)
                msgBox.setWindowTitle("TreeTime Error")
                msgBox.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok)
                result = msgBox.exec()


class Tree(Node):
    """
    A tree inside a forest. One item can appear several times in the forest, but only once in each tree.
    """

    def __init__(self, parent, index):
        """Initialise"""
        
        super().__init__(parent, index, [])
        self.fieldOrder = []
        self.fields = {}
        self.name = ""

    def createPathTo(self, item, treeindex):
        """
        Sort the item into the forest, creating existing nodes on the fly if missing.
        """

        # per tree: loop over all nodes, creating if necessary
        # per node: create final node and link it to item
        if item.trees[treeindex]:
            n = item.trees[treeindex][0]
            while n >= len(self.children):
                self.addChild()
            self.children[n].createPathTo(item, treeindex, 1, self.fields)

    def findNode(self, path):
        """
        Find the node at a given path. Calls the respective function of the Node class.
        """

        # per tree: loop over all nodes, creating if necessary
        return super().findNode(path, 0)

    def writeToString(self):
        string = "tree " + json.dumps(self.name) + "\n"

        # first write the visible fields in order
        for name in self.fieldOrder:
            string += "    field " + json.dumps(name) + "\n"
            string += "    " + self.fields[name].writeToString()

        # then the hidden fields
        for n,f in self.fields.items():
            if n not in self.fieldOrder:
                string += "    field " + json.dumps(n) + "\n"
                string += "    " + f.writeToString()
        return string

    def readFromString(self, string):
        fieldStrings = string.split("\n    field ")
        for n,fs in enumerate(fieldStrings):
            if n==0:
                self.name = json.loads(fs)
            else:
                f = Field()
                name = f.readFromString(fs)
                self.fields[name] = f
                if not f.hidden:
                    self.fieldOrder += [name]

    def fieldIndexFromName(self, name):
        """
        :param name: The name of the tree
        :return: The tree index of the tree with the given name
        """
        [index] = [n for n, field in enumerate(self.fieldOrder) if field==name] or [-1]
        return index

    def removeEmptyNodes(self):
        """
        Recurse to find nodes that have no items. If found, those nodes are removed and their siblings renumbered.
        Overrides same function in node, does not remove self (trees have no item anyway).
        """
        for c in self.children:
            c.removeEmptyNodes()

    def changeName(self, newName):
        """
        Called by a MetaNode. Changes the tree's name and calls all callbacks to propagate that change through the
        system, including (finally) the MetaNode.
        :param newName: The new name
        :return: void
        """
        self.name = newName

    def changeFieldName(self, oldName, newName):
        """
        Called by a MetaNode. Changes a field's name and calls all callbacks to propagate that change through the
        system, including (finally) the MetaNode. Calls the changeField() and the notifyFieldsChanges() from the
        nodes class.
        :param newName: The new name. If =None, the field is removed from the parameter list.
        :return: void
        """

        def renameSingle(paramList):
            changed = False
            for n, x in enumerate(paramList):
                if x == oldName:
                    if newName:
                        paramList[n] = newName
                    else:
                        paramList.pop(n)
                    changed = True
            return changed

        # update field order list, if field in list (for hidden fields and data item fields, do nothing)
        [index] = [n for n, x in enumerate(self.fieldOrder) if x == oldName] or [-1]
        if index+1:
            if newName:
                self.updateFieldOrderEntry(index, newName)
                self.fieldOrder[index] = newName
            else:
                self.updateFieldOrderEntry(index, newName)
                self.fieldOrder.pop(index)

        # Change name in field itself, if this is a tree field (for data item fields, do nothing)
        if oldName in self.fields:
            field = self.fields.pop(oldName)
            if newName:
                self.fields[newName] = field

        # Build new fields if they use the old name in definition
        changes = []    # list of fields that were changed
        for fname, field in self.fields.items():
            changed = False
            changed |= renameSingle(field.ownFields)
            changed |= renameSingle(field.childFields)
            changed |= renameSingle(field.siblingFields)
            changed |= renameSingle(field.parentFields)
            if changed:
                changes += [fname]

        # Replace all fields in tree and update their value
        for c in self.children:
            c.changeFieldName(oldName, newName)
            for fname in changes:
                c.changeFieldDefinition(fname, self.fields[fname])    # this is the renamed field, replace old name

        # then (when all fields are replaced) send the definition updates
        for fname in changes:
            self.updateFieldContent(fname)

    def updateFieldType(self, fieldName):
        """
        Sets all fields in the tree to the type in the top-level field
        :param fieldName:
        :return:
        """

        # Change type of all fields, using a loop to leave the metanode-node-array links on the top level unchanged
        newType = self.fields[fieldName].fieldType
        for c in self.children:
            c.changeFieldType(fieldName, newType)

        # Notify definition change
        self.updateFieldContent(fieldName)

    def updateFieldVisibility(self, name):
        """
        Called by a MetaNode. A top-level field visibility has changed, update the fieldOrder list, then update all
        nodes and redisplay.
        :param name: The field name
        :param hidden: Whether the field is now hidden or not
        :return: void
        """

        field = self.fields[name]
        inOrder = name in self.fieldOrder

        # Return if nothing to do
        if (field.hidden and not inOrder) or (not field.hidden and inOrder):
            return

        # Adapt fieldOrder entry
        index = -1
        if inOrder:
            [index] = [n for n, x in enumerate(self.fieldOrder) if x == name]
            self.fieldOrder.pop(index)
        else:
            self.fieldOrder += [name]

        # change visibilty in field, for export function (everything else is done by fieldOrder)
        for c in self.children:
            c.updateFieldVisibility(name, field.hidden)

        # Propagate new field order to all QNodes
        self.updateFieldOrder(self.fieldOrder)

        # Update all field values
        self.updateFieldContent(None)

    def updateFieldParameters(self, fieldName, listName):
        """
        Called by a MetaNode. A top-level field parameter list has changed, update the fieldOrder list, then update all
        nodes and redisplay.
        :param name: The field name
        :param hidden: Whether the field is now hidden or not
        :return: True if success, False if error
        """
        result = True
        if listName == 'own-fields':
            self.changeFieldOwnParameters(fieldName, self.fields[fieldName].ownFields)
        elif listName == 'child-fields':
            self.changeFieldChildParameters(fieldName, self.fields[fieldName].childFields)
        elif listName == 'sibling-fields':
            self.changeFieldSiblingParameters(fieldName, self.fields[fieldName].siblingFields)
        elif listName == 'parent-fields':
            self.changeFieldParentParameters(fieldName, self.fields[fieldName].parentFields)
        else:
            print(f"Error when replacing parameters for field {fieldName:} list type {listName} does not exist")
            result = False

        # Update all field values
        self.updateFieldContent(fieldName)
        return result


class Forest(Node):
    """
    The trunk node containing trees, that contain the nodes. Also manages the node templates.
    """

    def __init__(self, filename):
        """Initialise"""
        
        super().__init__(None, None, [])
        self.itemPool = None
        self.itemTypes = None
        self.readFromFile(filename)

    def createPaths(self):
        """
        Sort all items from the itempool into the forest,
        creating the forest structure as defined by the items' node indexes
        """
        for item in self.itemPool.items:
            self.createPathTo(item)

    def createPathTo(self, item):
        
        # start: loop over all trees, creating if necessary
        for b in range(len(item.trees)):
            if b >= len(self.children):
                self.addTree()
            if item.trees[b]:
                self.children[b].createPathTo(item,b)

    def addTree(self):
        self.children += [Tree(self, len(self.children))]

    def renumberChildren(self):
        """
        Correct the paths after children have been removed or added.
        """
        for i, c in enumerate(self.children):
            c.renumberChildren()

    def removeEmptyNodes(self):
        """
        Recurse to find nodes that have no items. If found, those nodes are removed and their siblings renumbered.
        Overrides same function in node, does not remove self (forests have no item anyway).
        """
        for c in self.children:
            c.removeEmptyNodes()

    def writeToString(self):
        s = ""
        for b in self.children:
            s += b.writeToString() + "\n"
        return s

    def readFromString(self, string):
        string = string.split("\n\ntree ")
        n = 0
        for s in string:
            if s != "":
                self.children[n].readFromString(s)
                n += 1

    def writeToFile(self, filename):
        treeString = self.writeToString()
        typeString = self.itemTypes.writeToString()
        itemString = self.itemPool.writeToString()
        with open(filename, "w") as f:
            f.write("--trees--\n\n")
            f.write(treeString)
            f.write("--item-types--\n\n")
            f.write(typeString)
            f.write("--item-pool--\n\n")
            f.write(itemString)

    def readFromFile(self, filename):
        print(f"Reading file {filename}...")
        with open(filename, "r") as f:

            # chop up string into trees, types, and items part
            s = f.read()
            s = s.split("--trees--")[1]
            s = s.split("--item-types--")
            treeString = s[0]
            s = s[1].split("--item-pool--")
            typeString = s[0]
            itemString = s[1]

        # create item pool
        print(f"... reading data items ...")
        self.itemTypes = ItemPool()
        self.itemTypes.readFromString(typeString)

        # create type pool
        print(f"... reading data type ...")
        self.itemPool = ItemPool()
        self.itemPool.readFromString(itemString)
        self.createPaths()

        # create trees
        print(f"... reading tree definitions ...")
        self.readFromString(treeString)
        self.createPaths()     # atm we still need this twice. Fix it.

        # link name change function of default item to all trees
        for t,tree in enumerate(self.children):
            self.itemTypes.items[0].registerFieldNameChangeCallback(t, lambda old, new, tree=tree: tree.changeFieldName(old, new))

        # remove empty nodes
        print(f"... removing empty nodes ...")
        self.removeEmptyNodes()
        print(f"... done.")

    def treeIndexFromName(self, name):
        """
        :param name: The name of the tree
        :return: The tree index of the tree with the given name
        """
        [index] = [n for n, tree in enumerate(self.children) if tree.name == name] or [-1]
        return index

    def listFieldNames(self, scope=None):
        """
        :param scope: The name of a tree or None. If not None, only tree fields within that tree, and data field
                      names, are listed. Otherwise, all tree field names and data field names are listed.
        :return: a list of existing tree field and data item field names
        """
        # find name for new field
        existingNames = [f for f in self.itemTypes.items[0].fields]
        for tree in self.children:
            if not scope or tree.name == scope:
                existingNames += [f for f in tree.fields]
        return existingNames

    def changeTreeFieldName(self, treeName, fieldName, newName):
        for c in self.children:
            if c.name == treeName:
                c.changeFieldName(fieldName, newName)

    def changeDataFieldName(self, fieldName, newName):
        self.itemTypes.changeFieldName(fieldName, newName)
        self.itemPool.changeFieldName(fieldName, newName)

    def updateDataFieldType(self, fieldName):
        """ Propagets the change of data field type in the governing data item (default type) through the pool
        :param fieldName: name of field
        :return:
        """
        newType = self.itemTypes.items[0].fields[fieldName]['type']
        self.itemPool.changeFieldType(fieldName, newType)

    def updateTreeFieldType(self, treeName, fieldName):
        """
        Propagates the type that was set in a top-level field through the tree and updates the display
        :param treeName: p
        :param fieldName:
        :return:
        """
        for c in self.children:
            if c.name == treeName:
                c.updateFieldType(fieldName)

    def updateTreeFieldVisibility(self, treeName, fieldName):
        for tree in self.children:
            if tree.name == treeName:
                tree.updateFieldVisibility(fieldName)

    def newDataField(self):
        """
        :return: A pointer to the newly created data field in the default data item
        """

        # find new name
        name = 'new field'
        n = 2
        while name in self.listFieldNames():
            name = f'new field {n}'
            n += 1

        # create default field (string)
        field = {'type': 'string', 'content': ''}
        self.itemTypes.addField(name, field)
        self.itemPool.addField(name, field)

        # return field
        return name, self.itemTypes.items[0].fields[name]

    def newTreeField(self, treeName):
        """
        Creates a new tree field of default type (string)
        :param tree: the name of the tree to add the field to
        :param index: the index of the new field in the visible
        :return: a pointer to the newly created field
        """

        # find new name
        name = 'New Field'
        n = 2
        while name in self.listFieldNames(treeName):
            name = f'New Field {n}'
            n += 1

        # create default field (string)
        field = Field(fieldType='string')

        # register with trees
        for tree in self.children:
            if tree.name == treeName:
                tree.addField(name, field)
                tree.updateFieldVisibility(name)

        # return field
        return name, field

    def newTree(self):
        """
        Creates a new tree and append it.
        :param tree: the name of the tree to add the field to
        :param index: the index of the new field in the visible
        :return: a pointer to the newly created field
        """

        # find new name
        name = 'New Tree'
        n = 2
        while name in (c.name for c in self.children):
            name = f'New Tree {n}'
            n += 1

        # add new tree to forest
        self.addTree()
        self.itemTypes.addTree()
        self.itemPool.addTree()
        tree = self.children[-1]
        tree.name = name

        # return tree
        return tree

    def deleteDataField(self, name):
        self.itemTypes.deleteField(name)
        self.itemPool.deleteField(name)

    def deleteTreeField(self, treeName, fieldName):
        """
        Deletes a tree field
        :param treeName: the name of the tree to remove the field from
        :param fieldName: the name of the field in the tree
        :return: a pointer to the newly created field
        """

        # register with trees
        for tree in self.children:
            if tree.name == treeName:
                tree.changeFieldName(fieldName, None)    # notifiy the tree

    def deleteTree(self, treeName):
        """
        Deletes a tree by removing it from the parent and removing all tree paths from all items.
        Python takes care of deleting the actual objects. The update of the GUI is handled by the caller.
        :param treeName: The name of the tree to be deleted
        :return: The index of the deleted tree
        """
        n = self.treeIndexFromName(treeName)
        self.itemTypes.deleteTree(n)
        self.itemPool.deleteTree(n)
        self.children.pop(n)
        return n

    def updateTreeFieldParameters(self, treeName, fieldName, listName):
        """
        Propagates the changed parameter list that was set in a top-level field through the tree and updates the display
        :param treeName: The name of the tree
        :param fieldName: The name of the tree field
        :param listName: The name of the parameter list, one of 'own-fields', 'child-fields', 'sibling-fields',
        'parent-fields'.
        :return:
        """
        for c in self.children:
            if c.name == treeName:
                c.updateFieldParameters(fieldName, listName)

