class MH(object):
    """
    Define constants and generator templates for data
    model description.
    """

    #################
    ### Node Type ###
    #################

    NonTerminal = 1
    Generator = 2
    Leaf = 3
    Regex = 5

    RawNode = 4  # if a Node() is provided

    ##################################
    ### Non-Terminal Node Specific ###
    ##################################

    # shape_type & section_type attribute
    Ordered = '>'
    Random = '=..'
    FullyRandom = '=.'
    Pick = '=+'

    # duplicate_mode attribute
    Copy = 'u'
    ZeroCopy = 's'


    ##############################
    ### Regex Parser Specific ####
    ##############################

    class Charset:
        ASCII = 1
        ASCII_EXT = 2
        UNICODE = 3

    ##########################
    ### Node Customization ###
    ##########################

    class Custo:
        # NonTerminal node custo
        class NTerm:
            MutableClone = NonTermCusto.MutableClone
            FrozenCopy = NonTermCusto.FrozenCopy
            CollapsePadding = NonTermCusto.CollapsePadding

        # Generator node (leaf) custo
        class Gen:
            ForwardConfChange = GenFuncCusto.ForwardConfChange
            CloneExtNodeArgs = GenFuncCusto.CloneExtNodeArgs
            ResetOnUnfreeze = GenFuncCusto.ResetOnUnfreeze
            TriggerLast = GenFuncCusto.TriggerLast

        # Function node (leaf) custo
        class Func:
            FrozenArgs = FuncCusto.FrozenArgs
            CloneExtNodeArgs = FuncCusto.CloneExtNodeArgs


    #######################
    ### Node Attributes ###
    #######################

    class Attr:
        Freezable = NodeInternals.Freezable
        Mutable = NodeInternals.Mutable
        Determinist = NodeInternals.Determinist
        Finite = NodeInternals.Finite
        Abs_Postpone = NodeInternals.Abs_Postpone

        Separator = NodeInternals.Separator

        LOCKED = NodeInternals.LOCKED
        DEBUG = NodeInternals.DEBUG


    ############################################
    ### Helpers for Generator Node Templates ###
    ############################################

    @staticmethod
    def _validate_int_vt(vt):
        if not issubclass(vt, fvt.INT):
            raise DataModelDefinitionError("The value type requested is not supported! (expect a subclass of INT)")
        return vt

    @staticmethod
    def _validate_vt(vt):
        if not issubclass(vt, fvt.INT) and not issubclass(vt, fvt.String):
            raise DataModelDefinitionError("The value type requested is not supported!")
        return vt

    @staticmethod
    def _handle_attrs(n, set_attrs, clear_attrs):
        if set_attrs is not None:
            for sa in set_attrs:
                n.set_attr(sa)
        if clear_attrs is not None:
            for ca in clear_attrs:
                n.clear_attr(ca) class DataModelDefinitionError(Exception): pass class CharsetError(RegexParserError):

    def __init__(self):
        RegexParserError.__init__(self, "Some character(s) into the regex are incoherent with the provided charset.") class InitialStateNotFoundError(RegexParserError):

    def __init__(self):
        RegexParserError.__init__(self, "No state was declared as initial.") class QuantificationError(RegexParserError):

    def __init__(self, message=None):

        if message is None:
            message = u"Quantifier must be specified as followed: {X[,Y]} with X \u2264 Y."

        RegexParserError.__init__(self, message) class StructureError(RegexParserError):

    def __init__(self, char):
        message = ""
        if char == '}':
            message = "Unopened bracket, nothing to close."
        elif char == ')':
            message = "Unopened parenthesis, nothing to close."
        elif char == "]":
            message = "Unopened squared bracket, nothing to close."
        else:
            message = "Unclosed element."
        RegexParserError.__init__(self, message) class InconvertibilityError(RegexParserError):

    def __init__(self):
        RegexParserError.__init__(self, "Described regular expression is to complex, it can't be " +
                                        "translated into a non-terminal only composed of terminal ones.") class EscapeError(RegexParserError):

    def __init__(self, char=None):
        if char is None:
            message = "Nothing to escape."
        elif char in ('\\','(',')','[',']','{','}','+','?','*','|','-'):
            message = char + " is a special character: it needs to be escaped in order to be used in this context."
        else:
            message = char + " is not a special character: it is useless to escape it."
        RegexParserError.__init__(self, message) class InvalidRangeError(RegexParserError): pass class EmptyAlphabetError(RegexParserError): pass class Node(object):
    '''A Node is the basic building-block used within a graph-based data model.

    Attributes:
      internals (:obj:`dict` of :obj:`str` --> :class:`NodeInternals`): Contains all the configuration of a
        node. A configuration is associated to the internals/contents
        of a node, which can live independently of the other
        configuration.
      current_conf (str): Identifier to a configuration. Every usable node use at least one main
        configuration, namely ``'MAIN'``.
      name (str): Identifier of a node. Defined at instantiation.
        Shall be unique from its parent perspective.
      env (Env): One environment object is added to all the nodes of a node
        graph when the latter is registered within a data model
        (cf. :func:`DataModel.register()`). It is used for sharing
        global resources between nodes.
      entangled_nodes (set(:class:`Node`)): Collection of all the nodes entangled with this one. All
        the entangled nodes will react the same way as one of their
        peers (within some extent) if this peer is subjected to a
        stimuli. The node's properties related to entanglement are
        only the ones that directly define a node. For instance,
        changing a node's NodeInternals will propagate to its
        entangled peers but changing the state of a node's
        NodeInternals won't propagate. It is used for dealing with
        multiple instance of a same node (within the scope of a
        NonTerm
        node---cf. :func:`NodeInternals_NonTerm.get_subnodes_with_csts()`).
        But this mechanism can also be used for your own specific purpose. 
      semantics (:class:`NodeSemantics`): (optional) Used to associate a semantics to a
        node. Can be used during graph traversal in order to perform
        actions related to semantics.
      fuzz_weight (int): The fuzz weight is an optional attribute of Node() which
        express Data Model designer's hints for prioritizing the nodes
        to fuzz. If set, this attribute is used by some generic
        *disruptors* (the ones that rely on a ModelWalker object---refer to
        fuzzing_primitives.py)
      depth (int): Depth of the node within the graph from a specific given
        root. Will be computed lazily (only when requested).
      tmp_ref_count (int): (internal use) Temporarily used during the creation of multiple
        instance of a same node, especially in order to generate unique names.
      _post_freeze_handler (function): Is executed just after a node is frozen (which
        is the result of requesting its value when it is not
        freezed---e.g., at its creation).
    '''
   
    DJOBS_PRIO_nterm_existence = 100
    DJOBS_PRIO_dynhelpers = 200
    DJOBS_PRIO_genfunc = 300

    DEFAULT_DISABLED_VALUE = b'' #b'<EMPTY::' + uuid.uuid4().bytes + b'::>'
    DEFAULT_DISABLED_NODEINT = NodeInternals_Empty()

    CORRUPT_EXIST_COND = 5
    CORRUPT_QTY_SYNC = 6
    CORRUPT_NODE_QTY = 7
    CORRUPT_SIZE_SYNC = 8

    def __init__(self, name, base_node=None, copy_dico=None, ignore_frozen_state=False,
                 accept_external_entanglement=False, acceptance_set=None,
                 subnodes=None, values=None, value_type=None, vt=None, new_env=False):
        '''
        Args:
          name (str): Name of the node. Every children node of a node shall have a unique name.
            Useful to look for specific nodes within a graph.
          subnodes (list): (Optional) List of subnodes.
            If provided the Node will be created as a non-terminal node.
          values (list): (Optional) List of strings.
            If provided the instantiated node will be a  String-typed leaf node (taking its possible
            values from the parameter).
          value_type (VT): (Optional) The value type that characterize the node. Defined within
            `value_types.py` and inherits from either `VT` or `VT_Alt`. If provided the instantiated
            node will be a value_type-typed leaf node.
          vt (VT): alias to `value_type`.
          base_node (Node): (Optional) If provided, it will be used as a template to create the new node.
          ignore_frozen_state (bool): [If `base_node` provided] If True, the clone process of
            base_node will ignore its current state.
          accept_external_entanglement (bool): [If `base_node` provided] If True, during the cloning
            process of base_node, every entangled nodes outside the current graph will be referenced
            within the new node without being copied. Otherwise, a *Warning* message will be raised.
          acceptance_set (set): [If `base_node` provided] If provided, will be used as a set of
            entangled nodes that could be referenced within the new node during the cloning process.
          copy_dico (dict): [If `base_node` provided] It is used internally during the cloning process,
           and should not be used for any functional purpose.
          new_env (bool): [If `base_node` provided] If True, the `base_node` attached :class:`Env()`
           will be copied. Otherwise, the same will be used. If `ignore_frozen_state` is True, a
           new :class:`Env()` will be used.
        '''

        assert '/' not in name  # '/' is a reserved character

        self.internals = {}
        self.name = name
        self.env = None

        self.entangled_nodes = None

        self.semantics = None
        self.fuzz_weight = None

        self._post_freeze_handler = None 

        self.depth = 0
        self.tmp_ref_count = 1

        self.abs_postpone_sent_back = None  # used for absorption to transfer a resolved postpone
                                            # node back to where it was defined

        if base_node is not None and subnodes is None and values is None and value_type is None:

            self._delayed_jobs_called = base_node._delayed_jobs_called

            if base_node.env is None:
                self.env = None
            else:
                if new_env:
                    self.env = Env() if ignore_frozen_state else copy.copy(base_node.env)
                    # we always keep a reference to objects coming from other part of the framework,
                    # namely: knowledge_source
                    # self.env.knowledge_source = base_node.env.knowledge_source
                else:
                    self.env = base_node.env

            node_dico = self.set_contents(base_node,
                                          copy_dico=copy_dico, ignore_frozen_state=ignore_frozen_state,
                                          accept_external_entanglement=accept_external_entanglement,
                                          acceptance_set=acceptance_set, preserve_node=False)

            if new_env and self.env is not None:
                self.env.update_node_refs(node_dico, ignore_frozen_state=ignore_frozen_state)
            elif DEBUG:
                print("\n*** WARNING: the copied node '%s' don't have an Env() " \
                      "associated with it!\n" % base_node.name)

            if self.env is not None and self.env.djobs_exists(Node.DJOBS_PRIO_dynhelpers):
                self.env.execute_basic_djobs(Node.DJOBS_PRIO_dynhelpers)

        else:
            self._delayed_jobs_called = False

            self.add_conf('MAIN')
            self.set_current_conf('MAIN')

            self.reset_fuzz_weight()

            if subnodes is not None:
                self.set_subnodes_basic(subnodes)

            elif values is not None:
                self.set_values(values=values)

            elif value_type is not None:
                self.set_values(value_type=value_type)
            elif vt is not None:
                self.set_values(value_type=vt)

            else:
                self.make_empty()

    def get_clone(self, name=None, ignore_frozen_state=False, new_env=True):
        '''Create a new node. To be used wihtin a graph-based data model.
        
        Args:
          name (str): name of the new Node instance. If ``None`` the current name will be used.
          ignore_frozen_state (bool): if set to False, the clone function will produce
            a Node with the same state as the duplicated Node. Otherwise, the only the state won't be kept.
          new_env (bool): If True, the current :class:`Env()` will be copied.
            Otherwise, the same will be used.

        Returns:
          Node: duplicated Node object
        '''

        if name is None:
            name = self.name

        return Node(name, base_node=self, ignore_frozen_state=ignore_frozen_state, new_env=new_env)


    def __copy__(self):
        # This copy is only used internally by NodeInternals_NonTerm.get_subnodes_with_csts()
        # It does not handle self.internals nor self.entangled_nodes which are copied
        # in a different way.

        new_node = type(self)(self.name)
        new_node.__dict__.update(self.__dict__)
        if self.semantics is not None:
            new_node.semantics = copy.copy(self.semantics)
            new_node.semantics.make_private()

        return new_node

    def set_contents(self, base_node,
                     copy_dico=None, ignore_frozen_state=False,
                     accept_external_entanglement=False, acceptance_set=None,
                     preserve_node=True):
        '''Set the contents of the node based on the one provided within
        `base_node`. This method performs a deep copy of `base_node`,
        but some parameters can change the behavior of the copy.

        .. note:: python deepcopy() is not used for perfomance reason
          (10 to 20 times slower).

        Args:
          base_node (Node): (Optional) Used as a template to create the new node.
          ignore_frozen_state (bool): If True, the clone process of
            base_node will ignore its current state.
          preserve_node (bool): preserve the :class:`NodeInternals` attributes (making sense to preserve)
            of the possible overwritten NodeInternals.
          accept_external_entanglement (bool): If True, during the cloning
            process of base_node, every entangled nodes outside the current graph will be referenced
            within the new node without being copied. Otherwise, a *Warning* message will be raised.
          acceptance_set (set): If provided, will be used as a set of
            entangled nodes that could be referenced within the new node during the cloning process.
          copy_dico (dict): It is used internally during the cloning process,
            and should not be used for any functional purpose.

        Returns:
          dict: For each subnodes of `base_node` (keys), reference the corresponding subnodes within the new node.
        '''

        self._post_freeze_handler = base_node._post_freeze_handler
        
        if self.internals:
            self.internals = {}
        if self.entangled_nodes:
            self.entangled_nodes = None

        if copy_dico is not None:
            node_dico = copy_dico
        else:
            node_dico = {}

        func_nodes = set()
        entangled_set = set()
        delayed_node_internals = set()

        self.fuzz_weight = base_node.fuzz_weight

        if base_node.semantics is not None:
            self.semantics = copy.copy(base_node.semantics)
            self.semantics.make_private()

        for conf in base_node.internals:
            self.add_conf(conf)

            new_internals = copy.copy(base_node.internals[conf])
            if preserve_node:
                new_internals.make_private(ignore_frozen_state=ignore_frozen_state,
                                           accept_external_entanglement=accept_external_entanglement,
                                           delayed_node_internals=delayed_node_internals,
                                           forget_original_sync_objs=True)
                new_internals.set_contents_from(self.internals[conf])
            else:
                new_internals.make_private(ignore_frozen_state=ignore_frozen_state,
                                           accept_external_entanglement=accept_external_entanglement,
                                           delayed_node_internals=delayed_node_internals,
                                           forget_original_sync_objs=False)

            self.internals[conf] = new_internals
            self.internals[conf].env = self.env

            if base_node.is_nonterm(conf):
                self.internals[conf].import_subnodes_full_format(internals=base_node.internals[conf])
                self.internals[conf].make_private_subnodes(node_dico, func_nodes, self.env,
                                                           ignore_frozen_state=ignore_frozen_state,
                                                           accept_external_entanglement=accept_external_entanglement,
                                                           entangled_set=entangled_set,
                                                           delayed_node_internals=delayed_node_internals)
                self.internals[conf].make_private(ignore_frozen_state=ignore_frozen_state,
                                                  accept_external_entanglement=accept_external_entanglement,
                                                  delayed_node_internals=delayed_node_internals)
                self._finalize_nonterm_node(conf)

        # Once node_dico has been populated from the node tree,
        # we deal with 'nodes' argument of Func and GenFunc that does not belong to this
        # tree. And we complete the node_dico.
        for conf in base_node.internals:
            if base_node.is_func(conf) or base_node.is_genfunc(conf):
                self.internals[conf].make_args_private(node_dico, entangled_set, ignore_frozen_state=ignore_frozen_state,
                                                       accept_external_entanglement=accept_external_entanglement)

        # Now we deal with the 'nodes' argument of the Func and
        # GenFunc Nodes within the copied tree, that has been let
        # aside
        for e in func_nodes:
            for conf in e.confs:
                if e.is_func(conf) or e.is_genfunc(conf):
                    e.internals[conf].make_args_private(node_dico, entangled_set, ignore_frozen_state=ignore_frozen_state,
                                                        accept_external_entanglement=accept_external_entanglement)

        # We deal with node refs within NodeInternals, once the node_dico is complete
        for n in delayed_node_internals:
            n._update_node_refs(node_dico, debug=n)

        if base_node.entangled_nodes is not None and ((not ignore_frozen_state) or accept_external_entanglement):
            entangled_set.add(base_node)

        # all nodes in entangled_set are already private nodes
        for node in entangled_set:
            intrics = set()
            for e in node.entangled_nodes:
                if e in node_dico:
                    intrics.add(node_dico[e])
                elif e is base_node:
                    intrics.add(self)
                elif e is node:
                    intrics.add(e)
                elif accept_external_entanglement:
                    intrics.add(e)
                elif acceptance_set is not None and e in acceptance_set:
                    intrics.add(e)
                else:
                    DEBUG = True
                    # TOFIX: the dynamically created subnodes by a
                    # Non terminal node, may have in their
                    # entangled list some mirror subnodes from
                    # another Non terminal node containing copies
                    # of these subnodes, or maybe subnodes (not
                    # removed) of the node in previous frozen
                    # state
                    if DEBUG:
                        print("\n*** WARNING: detection of entangled node outside the current graph, " \
                                  "whereas 'accept_external_entanglement' parameter is set to False!")
                        print("[ accept_external_entanglement = %r, ignore_frozen_state = %r, current copied node: %s ]" \
                                  % (accept_external_entanglement, ignore_frozen_state, self.name))
                        print(' --> Node: ', node.name, repr(node))
                        print(' --> Entangled with external node: ', e.name, repr(e))
                        print(" --> Entangled nodes of node '%s':" % node.name)
                        for e in node.entangled_nodes:
                            print('  -', e.name, repr(e),
                                  " [in node_dico.keys(): %r / .values(): %r]" % (e in node_dico, e in node_dico.values()))
                        # raise ValueError

            if node is base_node:
                self.entangled_nodes = intrics
            else:
                node.entangled_nodes = intrics

        self.current_conf = copy.copy(base_node.current_conf)

        self._reset_depth(parent_depth=self.depth-1)

        return node_dico

    def set_fuzz_weight(self, w):
        '''Set the fuzzing weight of the node to `w`.

        The fuzz weight is an optional attribute of Node() which
        express Data Model designer's hints for prioritizing the nodes
        to fuzz. If set, this attribute is used by some generic
        *disruptors* (the ones that rely on a ModelWalker object---refer to
        fuzzing_primitives.py)

        Args:
          w (int): Value of the weight (by default every nodes has a weight of 1)

        Returns:
          None
        '''
        self.fuzz_weight = int(w)

    def get_fuzz_weight(self):
        '''Return the fuzzing weight of the node.

        Returns:
          int: the fuzzing weight
        '''
        return self.fuzz_weight

    def reset_fuzz_weight(self, recursive=False):
        '''Reset to standard (1) the fuzzing weight that is associated to this
        node, and all its subnodes if `recursive` parameter is set to `True`.

        Args:
          recursive (bool): if set to `True`, reset also every subnodes (all reachable nodes from this one).

        Returns:
          None
        '''
        self.fuzz_weight = 1
        if recursive:
            for conf in self.internals:
                self.internals[conf].reset_fuzz_weight(recursive=recursive)

    def add_conf(self, conf):
        # @conf could not be None or the empty string
        if conf and conf not in self.internals:
            self.internals[conf] = None
            return True
        else:
            return False

    def remove_conf(self, conf):
        if conf != 'MAIN':
            del self.internals[conf]

    def is_conf_existing(self, conf):
        return conf in self.internals

    def __get_confs(self):
        return self.internals.keys()

    confs = property(fget=__get_confs)
    '''Property giving all node's configurations (read only)'''

    def _set_subtrees_current_conf(self, node, conf, reverse, ignore_entanglement=False):
        conf2 = conf if node.is_conf_existing(conf) else node.current_conf

        if not reverse:
            node.current_conf = conf2

        if node.internals[node.current_conf]: # When an Node is created empty, there is None internals
            node.internals[node.current_conf].set_child_current_conf(node, conf, reverse,
                                                                     ignore_entanglement=ignore_entanglement)

        if not ignore_entanglement and node.entangled_nodes is not None:
            for e in node.entangled_nodes:
                e.internals[e.current_conf].set_child_current_conf(e, conf, reverse,
                                                                   ignore_entanglement=True)

        if reverse:
            node.current_conf = conf2


    def set_current_conf(self, conf, recursive=True, reverse=False, root_regexp=None, ignore_entanglement=False):

        if root_regexp is not None:
            node_list = self.get_reachable_nodes(path_regexp=root_regexp)
        else:
            node_list = [self]

        for e in node_list:
            if recursive:
                self._set_subtrees_current_conf(e, conf, reverse, ignore_entanglement=ignore_entanglement)
            else:
                if e.is_conf_existing(conf):
                    e.current_conf = conf

        if not ignore_entanglement and self.entangled_nodes is not None:
            for e in self.entangled_nodes:
                e.set_current_conf(conf, recursive=recursive, reverse=reverse, root_regexp=root_regexp,
                                   ignore_entanglement=True)


    def get_current_conf(self):
        return self.current_conf

    def gather_alt_confs(self):
        cfs = set()

        for c in self.confs:
            if c != 'MAIN':
                cfs.add(c)
        for c in self.confs:
            if self.is_nonterm(c):
                for e in self.c[c].subnodes_set:
                    cfs = cfs.union(e.gather_alt_confs())

        return cfs

    def entangle_with(self, node):
        assert(node is not self)

        if self.entangled_nodes is None:
            self.entangled_nodes = {self}

        if node.entangled_nodes is None:
            node.entangled_nodes = {node}

        self.entangled_nodes = self.entangled_nodes.union(node.entangled_nodes)
        node.entangled_nodes = self.entangled_nodes

        for e in self.entangled_nodes:
            if e is not self and e is not node:
                e.entangled_nodes = self.entangled_nodes


    def __get_current_internals(self):
        return self.internals[self.current_conf]

    def __set_current_internals(self, internal):
        self.internals[self.current_conf] = internal

    def __get_internals(self):
        return self.internals

    cc = property(fget=__get_current_internals, fset=__set_current_internals)
    '''Property linked to the current node's `internals` (read / write)'''

    c = property(fget=__get_internals)
    '''Property linked to `self.internals` (read only)'''

    def conf(self, conf=None):
        conf = self.__check_conf(conf)
        return self.internals[conf]
    
    def get_internals_backup(self):
        return Node(self.name, base_node=self, ignore_frozen_state=False,
                    accept_external_entanglement=True, new_env=False)

    def set_internals(self, backup):
        self.name = backup.name
        self.env = backup.env
        self.semantics = backup.semantics
        self.fuzz_weight = backup.fuzz_weight
        self.depth = backup.depth
        self.tmp_ref_count = backup.tmp_ref_count
        self.internals = backup.internals
        self.current_conf = backup.current_conf
        self.entangled_nodes = backup.entangled_nodes
        self._delayed_jobs_called = backup._delayed_jobs_called

    def __check_conf(self, conf):
        if conf is None:
            conf = self.current_conf
        elif not self.is_conf_existing(conf):
            raise ValueError
        return conf

    def is_genfunc(self, conf=None):
        conf = self.__check_conf(conf)
        return isinstance(self.internals[conf], NodeInternals_GenFunc)

    def is_func(self, conf=None):
        conf = self.__check_conf(conf)
        return isinstance(self.internals[conf], NodeInternals_Func)

    def is_typed_value(self, conf=None, subkind=None):
        conf = self.__check_conf(conf)
        resp = isinstance(self.internals[conf], NodeInternals_TypedValue)
        if resp and subkind is not None:
            resp = (self.internals[conf].get_current_subkind() == subkind) or \
                   issubclass(self.internals[conf].get_current_subkind(), subkind)
        return resp

    def is_nonterm(self, conf=None):
        conf = self.__check_conf(conf)
        return isinstance(self.internals[conf], NodeInternals_NonTerm)

    def is_term(self, conf=None):
        conf = self.__check_conf(conf)
        return issubclass(self.internals[conf].__class__, NodeInternals_Term)


    def compliant_with(self, internals_criteria=None, semantics_criteria=None, conf=None):
        conf = self.__check_conf(conf)

        if internals_criteria:
            cond1 = self.internals[conf].match(internals_criteria)
        else:
            cond1 = True

        if semantics_criteria:
            if self.semantics is None:
                cond2 = False
            else:
                cond2 = self.semantics.match(semantics_criteria)
        else:
            cond2 = True

        return cond1 and cond2


    def _reset_depth(self, parent_depth):
        self.depth = parent_depth + 1

        for c in self.internals:
            self.internals[c].reset_depth_specific(self.depth)

    def _finalize_nonterm_node(self, conf, depth=None):
        if not depth:
            depth = self.depth

        check_err = set()
        for e in self.internals[conf].subnodes_set:
            check_err.add(e.name)
            e._reset_depth(depth)

        if len(check_err) != len(self.internals[conf].subnodes_set):
            print('\n*** /!\\ ERROR /!\\\n')
            l = []
            guilty = []
            for e in self.internals[conf].subnodes_set:
                print("*** |- node:  ", repr(e))
                print("*** |- name: ", e.name)
                if e.name in l:
                    guilty.append(e.name)
                else:
                    l.append(e.name)

            if guilty:
                print("\n*** You shall not use the same name for Nodes " + \
                          "that share a common parent (%s in conf : '%s')." % (self.name, conf))
                print("*** Duplicated Node name: '%s'" % guilty)
                print('\n*** /!\\ END /!\\\n')
            else:
                print('*** Bug in Node implementation...')

            raise ValueError

        if self.internals[conf].separator is not None:
            sep_name = self.internals[conf].separator.node.name
            if sep_name in check_err:
                print("\n*** The separator node name shall not be used by a subnode " + \
                      "of this non-terminal node: %s in conf : '%s'." % (self.name, conf))
                raise ValueError

    def set_subnodes_basic(self, node_list, conf=None, ignore_entanglement=False, separator=None,
                           preserve_node=True):
        conf = self.__check_conf(conf)

        new_internals = NodeInternals_NonTerm()
        if preserve_node:
            new_internals.set_contents_from(self.internals[conf])
        self.internals[conf] = new_internals
        self.internals[conf].import_subnodes_basic(node_list, separator=separator, preserve_node=preserve_node)
        self._finalize_nonterm_node(conf)
   
        if not ignore_entanglement and self.entangled_nodes is not None:
            for e in self.entangled_nodes:
                e.set_subnodes_basic(node_list=node_list, conf=conf, ignore_entanglement=True, separator=separator)



    def set_subnodes_with_csts(self, wlnode_list, conf=None, ignore_entanglement=False, separator=None,
                               preserve_node=True):
        conf = self.__check_conf(conf)

        new_internals = NodeInternals_NonTerm()
        if preserve_node:
            new_internals.set_contents_from(self.internals[conf])
        self.internals[conf] = new_internals
        self.internals[conf].import_subnodes_with_csts(wlnode_list, separator=separator, preserve_node=preserve_node)
        self._finalize_nonterm_node(conf)

        if not ignore_entanglement and self.entangled_nodes is not None:
            for e in self.entangled_nodes:
                e.set_subnodes_basic(wlnode_list=wlnode_list, conf=conf, ignore_entanglement=True, separator=separator)


    def set_subnodes_full_format(self, subnodes_order, subnodes_attrs, conf=None, separator=None, preserve_node=True):
        conf = self.__check_conf(conf)

        new_internals = NodeInternals_NonTerm()
        if preserve_node:
            new_internals.set_contents_from(self.internals[conf])
        self.internals[conf] = new_internals
        self.internals[conf].import_subnodes_full_format(subnodes_order=subnodes_order,
                                                         subnodes_attrs=subnodes_attrs,
                                                         separator=separator)
        self._finalize_nonterm_node(conf)


    def set_values(self, values=None, value_type=None, conf=None, ignore_entanglement=False,
                   preserve_node=True):
        conf = self.__check_conf(conf)

        new_internals = NodeInternals_TypedValue()
        if preserve_node:
            new_internals.set_contents_from(self.internals[conf])
        self.internals[conf] = new_internals

        if values is not None:
            self.internals[conf].import_value_type(value_type=fvt.String(values=values))

        elif value_type is not None:
            self.internals[conf].import_value_type(value_type)

        else:
            raise ValueError

        if not ignore_entanglement and self.entangled_nodes is not None:
            for e in self.entangled_nodes:
                if value_type is not None:
                    value_type = copy.copy(value_type)
                    value_type.make_private(forget_current_state=True)
                e.set_values(values=copy.copy(values), value_type=value_type, conf=conf, ignore_entanglement=True)


    def set_func(self, func, func_node_arg=None, func_arg=None,
                 conf=None, ignore_entanglement=False, provide_helpers=False,
                 preserve_node=True):
        conf = self.__check_conf(conf)

        new_internals = NodeInternals_Func()
        if preserve_node:
            new_internals.set_contents_from(self.internals[conf])
        self.internals[conf] = new_internals
        self.internals[conf].import_func(func,
                                         fct_node_arg=func_node_arg, fct_arg=func_arg,
                                         provide_helpers=provide_helpers)

        if not ignore_entanglement and self.entangled_nodes is not None:
            for e in self.entangled_nodes:
                e.set_func(func, func_node_arg=func_node_arg,
                           func_arg=func_arg, conf=conf,
                           ignore_entanglement=True)


    def set_generator_func(self, gen_func, func_node_arg=None,
                           func_arg=None, conf=None, ignore_entanglement=False,
                           provide_helpers=False, preserve_node=True):
        conf = self.__check_conf(conf)

        new_internals = NodeInternals_GenFunc()
        if preserve_node:
            new_internals.set_contents_from(self.internals[conf])
        self.internals[conf] = new_internals
        self.internals[conf].import_generator_func(gen_func,
                                                   generator_node_arg=func_node_arg, generator_arg=func_arg,
                                                   provide_helpers=provide_helpers)

        if not ignore_entanglement and self.entangled_nodes is not None:
            for e in self.entangled_nodes:
                e.set_func(gen_func, func_node_arg=func_node_arg, func_arg=func_arg, conf=conf, ignore_entanglement=True)


    def make_empty(self, conf=None):
        conf = self.__check_conf(conf)
        self.internals[conf] = NodeInternals_Empty()
        
    def is_empty(self, conf=None):
        conf = self.__check_conf(conf)
        return isinstance(self.internals[conf], NodeInternals_Empty)

    def absorb(self, blob, constraints=AbsCsts(), conf=None, pending_postpone_desc=None):
        conf, next_conf = self._compute_confs(conf=conf, recursive=True)
        blob = convert_to_internal_repr(blob)
        status, off, sz, postpone_sent_back = self.internals[conf].absorb(blob, constraints=constraints, conf=next_conf,
                                                                          pending_postpone_desc=pending_postpone_desc)
        if postpone_sent_back is not None:
            self.abs_postpone_sent_back = postpone_sent_back

        if len(blob) == sz and status == AbsorbStatus.Absorbed:
            status = AbsorbStatus.FullyAbsorbed
            self.internals[conf].confirm_absorb()
        return status, off, sz, self.name

    def set_absorb_helper(self, helper, conf=None):
        conf = self.__check_conf(conf)
        self.internals[conf].set_absorb_helper(helper)

    def enforce_absorb_constraints(self, csts, conf=None):
        conf = self.__check_conf(conf)
        self.internals[conf].enforce_absorb_constraints(csts)

    def set_size_from_constraints(self, size=None, encoded_size=None, conf=None):
        conf = self.__check_conf(conf)
        self.internals[conf].set_size_from_constraints(size=size, encoded_size=encoded_size)

    # Does not affect function/generator Nodes
    def make_determinist(self, conf=None, all_conf=False, recursive=False):
        self.set_attr(NodeInternals.Determinist, conf, all_conf=all_conf, recursive=recursive)

    # Does not affect function/generator Nodes
    def make_random(self, conf=None, all_conf=False, recursive=False):
        self.clear_attr(NodeInternals.Determinist, conf, all_conf=all_conf, recursive=recursive)

    # Does not affect function/generator & nonterm Nodes
    def make_finite(self, conf=None, all_conf=False, recursive=False):
        self.set_attr(NodeInternals.Finite, conf, all_conf=all_conf, recursive=recursive)

    # Does not affect function/generator & nonterm Nodes
    def make_infinite(self, conf=None, all_conf=False, recursive=False):
        self.clear_attr(NodeInternals.Finite, conf, all_conf=all_conf, recursive=recursive)

    def _compute_confs(self, conf, recursive):
        next_conf = conf if recursive else None
        current_conf = conf if self.is_conf_existing(conf) else self.current_conf

        if self.is_genfunc(current_conf):
            next_conf = conf

        return current_conf, next_conf

    def _set_clone_info(self, info, node):
        '''Used to propagate random draw results when a NonTerm node is frozen
        to the dynamic nodes of its attached subgraphs, namely
        GenFunc/Func nodes which are the only ones which can act
        dynamically.
        '''
        for c in self.internals:
            self.internals[c].set_clone_info(info, node)

    def make_synchronized_with(self, scope, node=None, param=None, sync_obj=None, conf=None):
        conf = self.__check_conf(conf)
        self.internals[conf].set_node_sync(scope=scope, node=node, param=param, sync_obj=sync_obj)

    def synchronized_with(self, scope, conf=None):
        conf = self.__check_conf(conf)
        val = self.internals[conf].get_node_sync(scope)
        return val

    def set_attr(self, name, conf=None, all_conf=False, recursive=False):
        if all_conf:
            for c in self.internals:
                self.internals[c].set_attr(name)
                self.internals[c].set_child_attr(name, all_conf=True, recursive=recursive)

        else:
            conf, next_conf = self._compute_confs(conf, recursive)
            self.internals[conf].set_attr(name)
            self.internals[conf].set_child_attr(name, conf=next_conf, recursive=recursive)


    def clear_attr(self, name, conf=None, all_conf=False, recursive=False):
        if all_conf:
            for c in self.internals:
                self.internals[c].clear_attr(name)
                self.internals[c].clear_child_attr(name, all_conf=True, recursive=recursive)
        else:
            conf, next_conf = self._compute_confs(conf, recursive)
            self.internals[conf].clear_attr(name)
            self.internals[conf].clear_child_attr(name, conf=next_conf, recursive=recursive)

    def is_attr_set(self, name, conf=None):
        conf = self.__check_conf(conf)
        return self.internals[conf].is_attr_set(name)

    def set_private(self, val, conf=None):
        conf = self.__check_conf(conf)
        self.internals[conf].set_private(val)

    def get_private(self, conf=None):
        conf = self.__check_conf(conf)
        return self.internals[conf].get_private()

    def set_semantics(self, sem):
        if isinstance(sem, NodeSemantics):
            self.semantics = sem
        else:
            assert(isinstance(sem, list))
            self.semantics = NodeSemantics(sem)

    def get_semantics(self):
        return self.semantics


    def get_reachable_nodes(self, internals_criteria=None, semantics_criteria=None,
                            owned_conf=None, conf=None, path_regexp=None, exclude_self=False,
                            respect_order=False, relative_depth=-1, top_node=None, ignore_fstate=False):
        
        def __compliant(node, config, top_node):
            if node is top_node and exclude_self:
                return False

            if internals_criteria:
                cond1 = node.internals[config].match(internals_criteria)
            else:
                cond1 = True

            if semantics_criteria:
                if node.semantics is None:
                    cond2 = False
                else:
                    cond2 = node.semantics.match(semantics_criteria)
            else:
                cond2 = True

            if path_regexp is not None:
                paths = node.get_all_paths_from(top_node)
                for p in paths:
                    if re.search(path_regexp, p):
                        cond3 = True
                        break
                else:
                    cond3 = False
            else:
                cond3 = True

            return cond1 and cond2 and cond3


        def get_reachable_nodes_rec(node, config, rdepth, top_node):
            s = []
            # if respect_order:
            #     s = []
            # else:
            #     s = set()

            if config == None:
                config = self.current_conf
                next_conf = None
            else:
                next_conf = config

            if not node.is_conf_existing(config):
                config = node.current_conf

            internal = node.internals[config]

            if node.is_conf_existing(owned_conf) or (owned_conf == None):
                if __compliant(node, config, top_node):
                    s.append(node)
                    # if respect_order:
                    #     s.append(node)
                    # else:
                    #     s.add(node)

            if rdepth <= -1 or rdepth > 0:
                s2 = internal.get_child_nodes_by_attr(internals_criteria=internals_criteria,
                                                      semantics_criteria=semantics_criteria,
                                                      owned_conf=owned_conf, conf=next_conf,
                                                      path_regexp=path_regexp,
                                                      exclude_self=False,
                                                      respect_order=respect_order,
                                                      relative_depth = rdepth - 1,
                                                      top_node=top_node, ignore_fstate=ignore_fstate)
                if s2:
                    for e in s2:
                        if e not in s:
                            s.append(e)
                    # if respect_order:
                    #     for e in s2:
                    #         if e not in s:
                    #             s.append(e)
                    # else:
                    #     s = s.union(s2)

            return s

        if top_node is None:
            nodes = get_reachable_nodes_rec(node=self, config=conf, rdepth=relative_depth,
                                            top_node=self)
        else:
            nodes = get_reachable_nodes_rec(node=self, config=conf, rdepth=relative_depth,
                                            top_node=top_node)

        if respect_order:
            return nodes
        else:
            l1 = []
            l2 = []
            for e in nodes:
                if e.get_fuzz_weight() > 1:
                    l1.append(e)
                else:
                    l2.append(e)
            l1 = sorted(l1, key=lambda x: -x.get_fuzz_weight())

            return l1 + sorted(l2, key=lambda x: x.name)


    @staticmethod
    def filter_out_entangled_nodes(node_list):
        ret = []
        while True:
            if node_list:
                n = node_list.pop()
                if n.entangled_nodes:
                    for en in n.entangled_nodes:
                        if en in node_list:
                            node_list.remove(en)
                ret.append(n)
            else:
                break
        # print('\n*** FILTERED nodes')
        # for n in ret:
        #     print(' |_ ' + n.name)
        return ret


    def get_node_by_path(self, path_regexp=None, path=None, conf=None):
        '''
        The set of nodes that is used to perform the search include
        the node itself and all the subnodes behind it.
        '''
        if path is None:
            assert(path_regexp is not None)
            # Find *one* Node whose path match the regexp
            for n, e in self.iter_paths(conf=conf):
                if re.search(path_regexp, n):
                    ret = e
                    break
            else:
                ret = None
        else:
            htable = self.get_all_paths(conf=conf)
            # Find the Node through exact path
            try:
                ret = htable[path]
            except KeyError:
                ret = None

        return ret


    def _get_all_paths_rec(self, pname, htable, conf, recursive, first=True, clone_idx=0):

        next_conf = conf if recursive else None

        if not self.is_conf_existing(conf):
            conf = self.current_conf
        internal = self.internals[conf]

        name = self.name if first else pname + '/' + self.name

        if name in htable:
            htable[(name, clone_idx)] = self
        else:
            htable[name] = self

        internal.get_child_all_path(name, htable, conf=next_conf, recursive=recursive)


    def get_all_paths(self, conf=None, recursive=True, depth_min=None, depth_max=None):
        """
        Returns:
            dict: the keys are either a 'path' or a tuple ('path', int) when the path already
              exists (case of the same node used more than once within the same non-terminal)
        """
        htable = collections.OrderedDict()
        self._get_all_paths_rec('', htable, conf, recursive=recursive)

        if depth_min is not None or depth_max is not None:
            depth_min = int(depth_min) if depth_min is not None else 0
            depth_max = int(depth_max) if depth_max is not None else -1
            paths = copy.copy(htable)
            for k in paths:
                depth = len(k.split('/'))
                if depth < depth_min:
                    del htable[k]
                elif depth_max != -1 and depth > depth_max:
                    del htable[k]
                
        return htable

    def iter_paths(self, conf=None, recursive=True, depth_min=None, depth_max=None, only_paths=False):
        htable = self.get_all_paths(conf=conf, recursive=recursive, depth_min=depth_min,
                                    depth_max=depth_max)
        for path, node in htable.items():
            if isinstance(path, tuple):
                yield path[0] if only_paths else (path[0], node)
            else:
                yield path if only_paths else (path, node)

    def get_path_from(self, node, conf=None):
        for n, e in node.iter_paths(conf=conf):
            if e == self:
                return n
        else:
            return None


    def get_all_paths_from(self, node, conf=None):
        l = []
        for n, e in node.iter_paths(conf=conf):
            if e == self:
                l.append(n)
        return l

    def set_env(self, env):
        self.env = env
        for c in self.internals:
            self.internals[c].set_child_env(env)

    def get_env(self):
        return self.env

    def freeze(self, conf=None, recursive=True, return_node_internals=False):
        
        ret = self._get_value(conf=conf, recursive=recursive,
                              return_node_internals=return_node_internals)

        if self.env is None:
            print('Warning: freeze() is called on a node that does not have an Env()\n'
                  '  --> node name: {!s}'.format(self.name))

        if self.env is not None and self.env.delayed_jobs_enabled and \
                (not self._delayed_jobs_called or self.env.delayed_jobs_pending):
            self._delayed_jobs_called = True

            if self.env.djobs_exists(Node.DJOBS_PRIO_nterm_existence):
                self.env.cleanup_remaining_djobs(Node.DJOBS_PRIO_nterm_existence)

            if self.env.djobs_exists(Node.DJOBS_PRIO_genfunc):
                self.env.execute_basic_djobs(Node.DJOBS_PRIO_genfunc)

            ret = self._get_value(conf=conf, recursive=recursive,
                                  return_node_internals=return_node_internals)

        return ret

    get_value = freeze

    def _get_value(self, conf=None, recursive=True, return_node_internals=False):

        next_conf = conf if recursive else None
        conf2 = conf if self.is_conf_existing(conf) else self.current_conf

        if self.is_genfunc(conf2):
            next_conf = conf

        internal = self.internals[conf2]
        if internal is None:
            print("\n*** The Node named '{:s}' is used while it has not " \
                      "been completely specified!\n (no NodeInternals has " \
                      "been associted to the Node.)".format(self.name))
            raise ValueError

        ret, was_not_frozen = internal._get_value(conf=next_conf, recursive=recursive,
                                                  return_node_internals=return_node_internals)

        if was_not_frozen:
            self._post_freeze(internal, self)
            # We need to test self.env because an Node can be freezed
            # before being registered in the data model. It triggers
            # for instance when a generator Node is freezed
            # (_get_value() is called on it) during data model
            # construction.
            if internal.is_exhausted() and self.env is not None:
                self.env.notify_exhausted_node(self)

        return ret


    def _post_freeze(self, node_internals, wrapping_node):
        if self._post_freeze_handler is not None:
            self._post_freeze_handler(node_internals, wrapping_node)
        
    def register_post_freeze_handler(self, func):
        self._post_freeze_handler = func

    def is_exhausted(self, conf=None):
        conf = self.__check_conf(conf)
        return self.internals[conf].is_exhausted()

    def is_frozen(self, conf=None):
        conf = self.__check_conf(conf)
        return self.internals[conf].is_frozen()

    def reset_state(self, recursive=False, exclude_self=False, conf=None, ignore_entanglement=False):
        self._delayed_jobs_called = False
        current_conf, next_conf = self._compute_confs(conf=conf, recursive=recursive)
        self.internals[current_conf].reset_state(recursive=recursive, exclude_self=exclude_self, conf=next_conf,
                                                 ignore_entanglement=ignore_entanglement)

        if not ignore_entanglement and self.entangled_nodes is not None:
            for e in self.entangled_nodes:
                e.reset_state(recursive=recursive, exclude_self=exclude_self, conf=next_conf,
                              ignore_entanglement=True)


    def to_bytes(self, conf=None, recursive=True):

        def tobytes_helper(node_internals):
            if isinstance(node_internals, bytes):
                return node_internals
            else:
                return node_internals._get_value(conf=conf, recursive=recursive,
                                                 return_node_internals=False)[0]

        node_internals_list = self.freeze(conf=conf, recursive=recursive)
        if isinstance(node_internals_list, list):
            node_internals_list = list(flatten(node_internals_list))
            if node_internals_list:
                if issubclass(node_internals_list[0].__class__, NodeInternals):
                    node_internals_list = list(map(tobytes_helper, node_internals_list))
                val = b''.join(node_internals_list)
            else:
                val = b''
        else:
            val = node_internals_list

        return val

    def to_str(self, conf=None, recursive=True):
        val = self.to_bytes(conf=conf, recursive=recursive)
        return unconvert_from_internal_repr(val)

    def to_ascii(self, conf=None, recursive=True):
        val = self.to_str(conf=conf, recursive=recursive)
        try:
            if sys.version_info[0] > 2:
                val = eval('{!a}'.format(val))
            else:
                val = str(val)
        except:
            val = repr(val)
        finally:
            return val

    def _tobytes(self, conf=None, recursive=True):

        def tobytes_helper(node_internals):
            if isinstance(node_internals, bytes):
                return node_internals
            else:
                return node_internals._get_value(conf=conf, recursive=recursive,
                                                 return_node_internals=False)[0]

        node_internals_list = self._get_value(conf=conf, recursive=recursive)
        if isinstance(node_internals_list, list):
            node_internals_list = list(flatten(node_internals_list))
            if node_internals_list:
                if issubclass(node_internals_list[0].__class__, NodeInternals):
                    node_internals_list = list(map(tobytes_helper, node_internals_list))
                val = b''.join(node_internals_list)
            else:
                val = b''
        else:
            val = node_internals_list

        return val

    def set_frozen_value(self, value, conf=None):
        conf = self.__check_conf(conf)

        if self.is_term(conf):
            value = convert_to_internal_repr(value)
            self.internals[conf]._set_frozen_value(value)
        else:
            raise ValueError

    def fix_synchronized_nodes(self, conf=None):
        conf = self.__check_conf(conf)
        self.internals[conf].synchronize_nodes(self)

    def unfreeze(self, conf=None, recursive=True, dont_change_state=False,
                 ignore_entanglement=False, only_generators=False,
                 reevaluate_constraints=False):
        self._delayed_jobs_called = False

        next_conf = conf

        if not self.is_conf_existing(conf):
            conf = self.current_conf

        # if self.is_frozen(conf):
        self.internals[conf].unfreeze(next_conf, recursive=recursive, dont_change_state=dont_change_state,
                                      ignore_entanglement=ignore_entanglement, only_generators=only_generators,
                                      reevaluate_constraints=reevaluate_constraints)

        if not ignore_entanglement and self.entangled_nodes is not None:
            for e in self.entangled_nodes:
                e.unfreeze(conf=next_conf, recursive=recursive, dont_change_state=dont_change_state,
                           ignore_entanglement=True, only_generators=only_generators,
                           reevaluate_constraints=reevaluate_constraints)


    def unfreeze_all(self, recursive=True, ignore_entanglement=False):
        self._delayed_jobs_called = False

        for conf in self.internals:
            if self.is_frozen(conf):
                self.internals[conf].unfreeze_all(recursive=recursive)

        if not ignore_entanglement and self.entangled_nodes is not None:
            for e in self.entangled_nodes:
                e.unfreeze_all(recursive=recursive, ignore_entanglement=True)



    def pretty_print(self, max_size=None, conf=None):
        conf = self.__check_conf(conf)
        return self.internals[conf].pretty_print(max_size=max_size)

    def get_nodes_names(self, conf=None, verbose=False, terminal_only=False):
        l = []
        for n, e in self.iter_paths(conf=conf):
            if terminal_only:
                conf = e.__check_conf(conf)
                if not e.is_term(conf):
                    continue

            if verbose:
                l.append((n, e.depth, e._tobytes()))
            else:
                l.append((n, e.depth))

            if e.env is None:
                print(n + ' (' + str(e.depth) + ')' + ' ' + str(e.env))
                print('Node value: ', e._tobytes())
                print("The 'env' attr of this Node is NONE")
                raise ValueError

        return l


    @staticmethod
    def _print(msg, rgb, style='', nl=True, log_func=sys.stdout.write, pretty_print=True):
        end = '\n' if nl else ''
        if pretty_print:
            log_func(style)
            log_func(colorize(msg, rgb=rgb))
            if style:
                log_func(FontStyle.END + end)
            else:
                log_func(end)
        else:
            log_func(msg+end)

    @staticmethod
    def _print_name(msg, style='', nl=True, log_func=sys.stdout.write, pretty_print=True):
        Node._print(msg, rgb=Color.ND_NAME, style=style, nl=nl, log_func=log_func, pretty_print=pretty_print)

    @staticmethod
    def _print_type(msg, style=FontStyle.BOLD, nl=True, log_func=sys.stdout.write, pretty_print=True):
        Node._print(msg, rgb=Color.ND_TYPE, style=style, nl=nl, log_func=log_func, pretty_print=pretty_print)

    @staticmethod
    def _print_contents(msg, style='', nl=True, log_func=sys.stdout.write, pretty_print=True):
        Node._print(msg, rgb=Color.ND_CONTENTS, style=style, nl=nl, log_func=log_func, pretty_print=pretty_print)

    @staticmethod
    def _print_nonterm(msg, style=FontStyle.BOLD, nl=True, log_func=sys.stdout.write, pretty_print=True):
        Node._print(msg, rgb=Color.ND_NONTERM, style=style, nl=nl, log_func=log_func, pretty_print=pretty_print)

    @staticmethod
    def _print_raw(msg, style='', nl=True, hlight=False, log_func=sys.stdout.write, pretty_print=True):
        if hlight:
            st = FontStyle.BOLD if style == '' else style
            Node._print(msg, rgb=Color.ND_RAW_HLIGHT, style=st, nl=nl, log_func=log_func, pretty_print=pretty_print)
        else:
            Node._print(msg, rgb=Color.ND_RAW, style=style, nl=nl, log_func=log_func, pretty_print=pretty_print)

    def show(self, conf=None, verbose=True, print_name_func=None, print_contents_func=None,
             print_raw_func=None, print_nonterm_func=None, print_type_func=None, alpha_order=False,
             raw_limit=None, log_func=sys.stdout.write, pretty_print=True, display_title=True,
             display_gen_node=True):

        if print_name_func is None:
            print_name_func = self._print_name
        if print_nonterm_func is None:
            print_nonterm_func = self._print_nonterm
        if print_contents_func is None:
            print_contents_func = self._print_contents
        if print_raw_func is None:
            print_raw_func = self._print_raw
        if print_type_func is None:
            print_type_func = self._print_type

        sep_deco = ' [SEP]'

        def get_args(node, conf):
            args = ''
            first = True
            for n in node.c[conf].get_node_args():
                if first:
                    first = False
                    args += str(n.get_path_from(self, conf=conf))
                else:
                    args += ', ' + str(n.get_path_from(self, conf=conf))
            if args is '':
                args = 'None'
            return args

        def get_all_smaller_depth(nodes_nb, i, depth, conf):
            smaller_depth = []
            prev_depth = l[i][0].count('/')

            for j in range(i, nodes_nb):
                current = l[j][1]
                sep_nb = l[j][0].count('/')
                if current.depth != sep_nb:
                    # case when the same node is used at different depth
                    if '_seen' not in current.__dict__:
                        current._seen = True
                        current.depth = sep_nb

                if current.depth != prev_depth:
                    break

                prev_depth = current.depth


            for j in range(i, nodes_nb):
                current = l[j][1]
                if '_seen' in current.__dict__:
                    del current._seen

            for j in range(i+1, nodes_nb):
                delta = depth - l[j][1].depth
                if delta > 0:
                    d = l[j][1].depth
                    if d not in smaller_depth:
                        if not smaller_depth or (smaller_depth and d < smaller_depth[-1]):
                            smaller_depth.append(d)

            return smaller_depth

        # in case the node is not frozen
        self.freeze()

        l = []
        for n, e in self.iter_paths(conf=conf):
            l.append((n, e))

        if alpha_order:
            l = sorted(l, key=lambda x: x[0])

        name = '[' + self.name + ']'
        if display_title:
            print_name_func(name, log_func=log_func, pretty_print=pretty_print)
            print_name_func('-' * len(name), log_func=log_func, pretty_print=pretty_print)

        nodes_nb = len(l)

        unindent_generated_node = False

        if verbose:
            prev_depth = 0
            for n, i in zip(l, range(nodes_nb)):
                name, node = n

                conf_tmp = node.__check_conf(conf)
                if isinstance(node.c[conf_tmp], NodeInternals_TypedValue):
                    node_type = node.c[conf_tmp].get_value_type().__class__.__name__
                else:
                    node_type = node.c[conf_tmp].__class__.__name__[len('NodeInternals_'):]

                depth = node.depth
                sep_nb = name.count('/')
                if depth != sep_nb:
                    # detection that the same node is used multiple
                    # times at different level. Thus it is a graph
                    depth = sep_nb
                    node.depth = sep_nb

                def is_node_used_more_than_once(name):
                    node_list = []
                    for item in l:
                        if re.search(name+'$', item[0]):
                            node_list.append(item[1])
                    return len(node_list) != len(set(node_list))

                if is_node_used_more_than_once(node.name):
                    graph_deco = ' --> M'
                else:
                    graph_deco = ''

                if unindent_generated_node:
                    # depth always >=1
                    depth -= 1

                if depth == 0:
                    indent_nonterm = ''
                    indent_spc = ''
                    indent_term = ''
                else:
                    all_smaller_depth = get_all_smaller_depth(nodes_nb, i, depth, conf_tmp)
                    # if i != nodes_nb-1:
                    #     print('DBG1: ', l[i+1][1].depth, l[i+1][0], repr(l[i+1][1]))

                    prefix = ''
                    sz = len(all_smaller_depth)
                    idx = 0
                    for bar in range(depth-1, 0, -1):
                        if idx < sz and bar == all_smaller_depth[idx]:
                            idx += 1
                            prefix = ' |  ' + prefix
                        else:
                            prefix = '    ' + prefix
                    indent_nonterm = prefix + ' \__'
                    indent_term = prefix + ' \__'

                    # l[i+1][1].depth is not reliable in case the node is used at different level
                    if i == nodes_nb-1 or depth != l[i+1][0].count('/'):
                        # if i != nodes_nb-1:
                        #     print('DBG2: ', l[i+1][1].depth, l[i+1][0], repr(l[i+1][1]), i+1)
                        indent_spc = prefix + '    ' + '    '
                    else:
                        indent_spc = prefix + ' |  ' + '    '

                if unindent_generated_node:
                    unindent_generated_node = False
                    depth += 1

                if node.is_term(conf_tmp):
                    raw = node._tobytes()
                    raw_len = len(raw)
                    val = node.pretty_print(max_size=raw_limit)

                    prefix = "{:s}".format(indent_term)
                    name = "{:s} ".format(name)
                    if isinstance(node.c[conf_tmp], NodeInternals_Func):
                        args = get_args(node, conf_tmp)
                        type_and_args = '[{:s} | node_args: {:s}] size={:d}B' \
                            .format(node_type, args, raw_len)
                    else:
                        type_and_args = '[{:s}] size={:d}B'.format(node_type, raw_len)
                    print_nonterm_func(prefix, nl=False, log_func=log_func, pretty_print=pretty_print)
                    print_name_func('({:d}) {:s}'.format(depth, name), nl=False, log_func=log_func, pretty_print=pretty_print)
                    print_type_func(type_and_args, nl=False, log_func=log_func, pretty_print=pretty_print)
                    if node.is_attr_set(NodeInternals.Separator):
                        self._print(sep_deco, rgb=Color.ND_SEPARATOR, style=FontStyle.BOLD, nl=False,
                                    log_func=log_func, pretty_print=pretty_print)
                    self._print(graph_deco, rgb=Color.ND_DUPLICATED, style=FontStyle.BOLD,
                                log_func=log_func, pretty_print=pretty_print)
                    if val is not None:
                        print_nonterm_func("{:s}  ".format(indent_spc), nl=False, log_func=log_func, pretty_print=pretty_print)
                        print_contents_func("\_ {:s}".format(val), log_func=log_func, pretty_print=pretty_print)
                    print_nonterm_func("{:s}  ".format(indent_spc), nl=False, log_func=log_func, pretty_print=pretty_print)
                    if raw_limit is not None and raw_len > raw_limit:
                        print_raw_func("\_raw: {:s}".format(repr(raw[:raw_limit])), nl=False,
                                       log_func=log_func, pretty_print=pretty_print)
                        print_raw_func(" ...", hlight=True, log_func=log_func, pretty_print=pretty_print)
                    else:
                        print_raw_func("\_raw: {:s}".format(repr(raw)), log_func=log_func, pretty_print=pretty_print)
                else:
                    is_gen_node = isinstance(node.c[conf_tmp], NodeInternals_GenFunc)
                    if (is_gen_node and display_gen_node) or not is_gen_node:
                        print_nonterm_func("{:s}[{:d}] {:s}".format(indent_nonterm, depth, name), nl=False,
                                           log_func=log_func, pretty_print=pretty_print)
                    if is_gen_node:
                        if display_gen_node:
                            args = get_args(node, conf_tmp)
                            print_nonterm_func(' [{:s} | node_args: {:s}]'.format(node_type, args),
                                               nl=False, log_func=log_func, pretty_print=pretty_print)
                            self._print(graph_deco, rgb=Color.ND_DUPLICATED, style=FontStyle.BOLD,
                                        log_func=log_func, pretty_print=pretty_print)
                        else:
                            unindent_generated_node = True
                    else:
                        print_nonterm_func(' [{:s}]'.format(node_type), nl=False, log_func=log_func, pretty_print=pretty_print)
                        if node.is_nonterm(conf_tmp) and node.encoder is not None:
                            self._print(' [Encoded by {:s}]'.format(node.encoder.__class__.__name__),
                                        rgb=Color.ND_ENCODED, style=FontStyle.BOLD,
                                        nl=False, log_func=log_func, pretty_print=pretty_print)
                        if node.is_nonterm(conf_tmp) and node.custo.collapse_padding_mode:
                            self._print(' >Collapse Bitfields<',
                                        rgb=Color.ND_CUSTO, style=FontStyle.BOLD,
                                        nl=False, log_func=log_func, pretty_print=pretty_print)
                        self._print(graph_deco, rgb=Color.ND_DUPLICATED, style=FontStyle.BOLD,
                                    log_func=log_func, pretty_print=pretty_print)

        else:
            for name, node in l:
                print_name_func("{:s} [{:d}]".format(name, node.depth), log_func=log_func, pretty_print=pretty_print)


    def __lt__(self, other):
        return self.depth < other.depth


    def __hash__(self):
        return id(self)

    def __str__(self):
        # NEVER return something with self._tobytes() as side
        # effects are not welcomed
        return repr(self)


    def __getitem__(self, key):
        # self._get_value()
        if isinstance(key, str):
            return self.get_node_by_path(key)
        elif isinstance(key, NodeInternalsCriteria):
            return self.get_reachable_nodes(internals_criteria=key)
        elif isinstance(key, NodeSemanticsCriteria):
            return self.get_reachable_nodes(semantics_criteria=key)
        else:
            raise ValueError

    def __setitem__(self, key, val):
        nodes = self[key]
        if not nodes:
            raise ValueError

        if isinstance(val, Node):
            if isinstance(nodes, Node):
                nodes.set_contents(val)
            else:
                for n in nodes:
                    n.set_contents(val)
        elif isinstance(val, NodeSemantics):
            if isinstance(nodes, Node):
                nodes.set_semantics(val)
            else:
                for n in nodes:
                    n.set_semantics(val)
        elif isinstance(val, int):
            if isinstance(nodes, Node):
                # Method defined by INT object (within TypedValue nodes)
                nodes.update_raw_value(val)
            else:
                for n in nodes:
                    n.update_raw_value(val)
        else:
            if isinstance(nodes, Node):
                status, off, size, name = nodes.absorb(convert_to_internal_repr(val),
                                                       constraints=AbsNoCsts())
                if status != AbsorbStatus.FullyAbsorbed:
                    raise ValueError
            else:
                for n in nodes:
                    status, off, size, name = n.absorb(convert_to_internal_repr(val),
                                                           constraints=AbsNoCsts())
                    if status != AbsorbStatus.FullyAbsorbed:
                        raise ValueError

    def __getattr__(self, name):
        internals = self.__getattribute__('internals')[self.current_conf]
        if hasattr(internals, name):
            return getattr(internals, name)
        else:
            return object.__getattribute__(self, name) class NodeInternals_Empty(NodeInternals):
    def _get_value(self, conf=None, recursive=True, return_node_internals=False):
        if return_node_internals:
            return (Node.DEFAULT_DISABLED_NODEINT, True)
        else:
            return (Node.DEFAULT_DISABLED_VALUE, True)

    def get_raw_value(self, **kwargs):
        return Node.DEFAULT_DISABLED_VALUE

    def set_child_env(self, env):
        self.env = env
        print('\n*** Empty Node: {!s}'.format(hex(id(self))))
        raise AttributeError class GenFuncCusto(NodeCustomization):
    """
    Generator node behavior-customization
    To be provided to :meth:`NodeInternals.customize`
    """
    ForwardConfChange = 1
    CloneExtNodeArgs = 2
    ResetOnUnfreeze = 3
    TriggerLast = 4

    _custo_items = {
        ForwardConfChange: True,
        CloneExtNodeArgs: False,
        ResetOnUnfreeze: True,
        TriggerLast: False
    }

    @property
    def forward_conf_change_mode(self):
        return self._custo_items[self.ForwardConfChange]

    @property
    def clone_ext_node_args_mode(self):
        return self._custo_items[self.CloneExtNodeArgs]

    @property
    def reset_on_unfreeze_mode(self):
        return self._custo_items[self.ResetOnUnfreeze]

    @property
    def trigger_last_mode(self):
        return self._custo_items[self.TriggerLast] class NonTermCusto(NodeCustomization):
    """
    Non-terminal node behavior-customization
    To be provided to :meth:`NodeInternals.customize`
    """
    MutableClone = 1
    FrozenCopy = 2
    CollapsePadding = 3

    _custo_items = {
        MutableClone: True,
        FrozenCopy: True,
        CollapsePadding: False
    }

    @property
    def mutable_clone_mode(self):
        return self._custo_items[self.MutableClone]

    @property
    def frozen_copy_mode(self):
        return self._custo_items[self.FrozenCopy]

    @property
    def collapse_padding_mode(self):
        return self._custo_items[self.CollapsePadding] class FuncCusto(NodeCustomization):
    """
    Function node behavior-customization
    To be provided to :meth:`NodeInternals.customize`
    """
    FrozenArgs = 1
    CloneExtNodeArgs = 2

    _custo_items = {
        FrozenArgs: True,
        CloneExtNodeArgs: False,
    }

    @property
    def frozen_args_mode(self):
        return self._custo_items[self.FrozenArgs]

    @property
    def clone_ext_node_args_mode(self):
        return self._custo_items[self.CloneExtNodeArgs] class NodeSemantics(object):
    '''
    To be used while defining a data model as a means to associate
    semantics to an Node.
    '''
    def __init__(self, attrs=[]):
        self.__attrs = attrs

    def add_attributes(self, attrs):
        self.__attrs += attrs

    def _match_optionalbut1_criteria(self, criteria):
        if criteria is None:
            return True

        for c in criteria:
            if c in self.__attrs:
                return True

        return False

    def _match_mandatory_criteria(self, criteria):
        if criteria is None:
            return True

        for c in criteria:
            if c not in self.__attrs:
                return False

        return True

    def _match_exclusive_criteria(self, criteria):
        if criteria is None:
            return True

        match_nb = 0
        for c in criteria:
            if c in self.__attrs:
                match_nb += 1

        if match_nb == 0 or match_nb > 1:
            return False
        else:
            return True

    def _match_negative_criteria(self, criteria):
        if criteria is None:
            return True

        for c in criteria:
            if c in self.__attrs:
                return False

        return True


    def match(self, semantics_criteria):
        '''
        This method is called within get_reachable_nodes() (when the
        'semantics' parameter is provided) to select Node that match
        the given semantics.
        '''
        c1 = self._match_optionalbut1_criteria(semantics_criteria.get_optionalbut1_criteria())
        if not c1:
            return False

        c2 = self._match_mandatory_criteria(semantics_criteria.get_mandatory_criteria())
        if not c2:
            return False

        c3 = self._match_exclusive_criteria(semantics_criteria.get_exclusive_criteria())
        if not c3:
            return False

        c4 = self._match_negative_criteria(semantics_criteria.get_negative_criteria())
        if not c4:
            return False

        return True


    def what_match_from(self, raw_criteria_list):
        matching = []
        for c in raw_criteria_list:
            if c in self.__attrs:
                matching.append(c)

        return matching


    def make_private(self):
        '''
        This method is called during Node copy process. It aims to make
        all your metadata private (if needed).
        '''
        self.__attrs = copy.copy(self.__attrs) class SyncScope(Enum):
    Qty = 1
    QtyFrom = 2
    Existence = 10
    Inexistence = 11
    Size = 20 class SyncQtyFromObj(SyncObj):

    def __init__(self, node, base_qty=0):
        assert node.is_typed_value()
        self._node = node
        self._base_qty = base_qty

    def get_node_containers(self):
        return self._node

    def put_node_containers(self, new_containers):
        self._node = new_containers

    @property
    def qty(self):
        return max(0, self._node.get_raw_value() + self._base_qty) class SyncSizeObj(SyncObj):

    def __init__(self, node, base_size=0, apply_to_enc_size=False):
        assert node.is_typed_value()
        self._node = node
        self.base_size = base_size
        self.apply_to_enc_size = apply_to_enc_size

    def get_node_containers(self):
        return self._node

    def put_node_containers(self, new_containers):
        self._node = new_containers

    @property
    def size_for_absorption(self):
        return max(0, self._node.get_raw_value() - self.base_size)

    def set_size_on_source_node(self, size):
        ok = self._node.update_raw_value(size)
        if not ok:
            print("\n*** WARNING: The node '{:s}' is not compatible with the integer"
                  " '{:d}'".format(self._node.name, size))
        self._node.set_frozen_value(self._node.get_current_value())

    def _sync_nodes_specific(self, src_node):
        if self.apply_to_enc_size:
            sz = len(src_node.to_bytes())
        else:
            if src_node.is_typed_value(subkind=fvt.String):
                # We need to get the str form to be agnostic to any low-level encoding
                # that may change the size ('utf8', ...).
                decoded_val = src_node.get_raw_value(str_form=True)
            else:
                decoded_val = src_node.get_raw_value()
                if not isinstance(decoded_val, bytes):
                    # In this case, this is a BitField or an INT-based object, which are
                    # fixed size object
                    raise DataModelDefinitionError('size sync should not be used for fixed sized object!')
            sz = len(decoded_val)
        sz += self.base_size
        self.set_size_on_source_node(NodeInternals_NonTerm.sizesync_corrupt_hook(src_node, sz)) class NodeCondition(object):
    """
    Base class for every node-related conditions. (Note that NodeCondition
    may be copied many times. If some attributes need to be fully copied,
    handle this through __copy__() overriding).
    """

    def _check_int(self, val, gt_val=None, lt_val=None):
        result = True
        if gt_val is not None:
            result = val >= gt_val
        if lt_val is not None:
            result = val <= lt_val
        return result

    def _check_inclusion(self, curr_val, val=None, neg_val=None):
        result = True
        if val is not None:
            if isinstance(val, (tuple, list)):
                result = curr_val in val
            else:
                result = curr_val == val
        if neg_val is not None:
            if isinstance(neg_val, (tuple, list)):
                result = result and curr_val not in neg_val
            else:
                result = result and curr_val != neg_val
        return result

    def check(self, node):
        raise NotImplementedError class SyncExistenceObj(SyncObj):

    def __init__(self, sync_list, and_junction=True):
        self.sync_list = sync_list
        self.and_clause = and_junction

    def get_node_containers(self):
        return self.sync_list

    def put_node_containers(self, new_containers):
        self.sync_list = new_containers

    def check(self):
        if self.and_clause:
            for node, cond in self.sync_list:
                if not self._condition_satisfied(node, cond):
                    return False
            else:
                return True
        else:
            for node, cond in self.sync_list:
                if self._condition_satisfied(node, cond):
                    return True
            else:
                return False

    def _condition_satisfied(self, node, condition):
        exist = node.env.node_exists(id(node))
        crit_1 = True if exist else False
        crit_2 = True
        if exist and condition is not None:
            try:
                crit_2 = condition.check(node)
            except Exception as e:
                print("\n*** ERROR: existence condition is not verifiable " \
                      "for node '{:s}' (id: {:d})!\n" \
                      "*** The condition checker raise an exception!".format(node.name, id(node)))
                raise
        return crit_1 and crit_2 class Env(object):

    knowledge_source = None

    def __init__(self):
        self.exhausted_nodes = []
        self.nodes_to_corrupt = {}
        self.env4NT = Env4NT()
        self.delayed_jobs_enabled = True
        self._sorted_jobs = None
        self._djob_keys = None
        self._djob_groups = None
        self._dm = None
        self.id_list = None
        self._reentrancy_cpt = 0
        # self._knowledge_source = None

    @property
    def delayed_jobs_pending(self):
        return bool(self._sorted_jobs)

    def __getattr__(self, name):
        if hasattr(self.env4NT, name):
            return self.env4NT.__getattribute__(name)
        else:
            raise AttributeError

    def is_empty(self):
        return not self.exhausted_nodes and not self.nodes_to_corrupt and self.env4NT.is_empty()

    def set_data_model(self, dm):
        self._dm = dm

    def get_data_model(self):
        return self._dm

    # @property
    # def knowledge_source(self):
    #     return self._knowledge_source
    #
    # @knowledge_source.setter
    # def knowledge_source(self, src):
    #     self._knowledge_source = src

    def add_node_to_corrupt(self, node, corrupt_type=None, corrupt_op=lambda x: x):
        if node.entangled_nodes:
            for n in node.entangled_nodes:
                self.nodes_to_corrupt[n] = (corrupt_type, corrupt_op)
        else:
            self.nodes_to_corrupt[node] = (corrupt_type, corrupt_op)

    def remove_node_to_corrupt(self, node):
        if node in self.nodes_to_corrupt:
            if node.entangled_nodes:
                for n in node.entangled_nodes:
                    del self.nodes_to_corrupt[n]
            else:
                del self.nodes_to_corrupt[node]

    def exhausted_node_exists(self):
        return len(self.exhausted_nodes) > 0

    def get_exhausted_nodes(self):
        return copy.copy(self.exhausted_nodes)

    def notify_exhausted_node(self, node):
        self.exhausted_nodes.append(node)

    def is_node_exhausted(self, node):
        return node in self.exhausted_nodes

    def clear_exhausted_node(self, node):
        try:
            self.exhausted_nodes.remove(node)
        except:
            print('*** requested node.name:       ', node.name)
            print('*** requested node:            ', node)
            print('*** current exhausted list:')
            for i in self.exhausted_nodes:
                print('  * exhausted_node.name:  ', i.name)
                print('  * exhausted_node:       ', i)
            if not self.exhausted_nodes:
                print('  * no exhausted node')

            raise

    def exhausted_nodes_amount(self):
        return len(self.exhausted_nodes)

    def clear_all_exhausted_nodes(self):
        self.exhausted_nodes = []

    def update_node_refs(self, node_dico, ignore_frozen_state):

        exh_nodes = []
        new_nodes_to_corrupt = {}
        self.id_list = []
        for old_node, new_node in node_dico.items():
            self.id_list.append((id(old_node), id(new_node)))
            if old_node in self.exhausted_nodes:
                exh_nodes.append(new_node)
            if old_node in self.nodes_to_corrupt.keys():
                op = self.nodes_to_corrupt[old_node]
                del self.nodes_to_corrupt[old_node]
                new_nodes_to_corrupt[new_node] = op

        self.nodes_to_corrupt = new_nodes_to_corrupt

        if self.is_empty():
            return

        if ignore_frozen_state:
            self.exhausted_nodes = []
            self.env4NT.reset()
        else:
            self.exhausted_nodes = exh_nodes
            self.env4NT.update_node_ids(self.id_list)

    # def update_id_list(self):
    #     self.id_list = []
    #     for old_node, new_node in node_dico.items():
    #         self.id_list.append((id(old_node), id(new_node)))

    def register_djob(self, func, group, key, cleanup=None, args=None, prio=1):
        if self._sorted_jobs is None:
            self._sorted_jobs = {}
        if self._djob_keys is None: # this case trigger if basic djobs
                                    # have been registered first
            self._djob_keys = {}
            self._djob_groups = {}

        if self._sorted_jobs.get(prio, None) is None:
            self._sorted_jobs[prio] = {}
            self._djob_keys[prio] = set()
            self._djob_groups[prio] = set()
        if self._sorted_jobs[prio].get(id(group), None) is None:
            self._sorted_jobs[prio][id(group)] = {}

        assert(key not in self._sorted_jobs[prio][id(group)])
        self._sorted_jobs[prio][id(group)][key] = (func, args, cleanup)
        assert(key not in self._djob_keys[prio])
        self._djob_keys[prio].add(key)
        self._djob_groups[prio].add(group)


    def register_basic_djob(self, func, args, prio=1):
        if self._sorted_jobs is None:
            self._sorted_jobs = {}
        if self._sorted_jobs.get(prio, None) is None:
            self._sorted_jobs[prio] = []
        self._sorted_jobs[prio].append((func, args))

    def execute_basic_djobs(self, prio):
        assert(prio in self._sorted_jobs)
        jobs = copy.copy(self._sorted_jobs[prio])
        del self._sorted_jobs[prio] # func() may triggers this func,
                                    # thus we cleanup before
        for func, args in jobs:
            func(*args)

    def get_basic_djobs(self, prio):
        assert(prio in self._sorted_jobs)
        return self._sorted_jobs[prio]

    def cleanup_basic_djobs(self, prio):
        if prio not in self._sorted_jobs:
            return
        del self._sorted_jobs[prio]

    def djobs_exists(self, prio):
        if self._sorted_jobs and prio in self._sorted_jobs and \
           self._sorted_jobs[prio]:
            return True
        else:
            return False

    def is_djob_registered(self, key, prio):
        if self._djob_keys and prio in self._djob_keys:
            return key in self._djob_keys[prio]
        else:
            return False

    def get_all_djob_groups(self, prio):
        assert(self._djob_groups is not None)
        return copy.copy(self._djob_groups[prio])

    def get_djobs_by_gid(self, group_id, prio):
        assert(self._sorted_jobs[prio] is not None)
        assert(self._sorted_jobs[prio][group_id] is not None)
        return self._sorted_jobs[prio][group_id]

    def remove_djob(self, group, key, prio):
        self._djob_keys[prio].remove(key)
        del self._sorted_jobs[prio][id(group)][key]
        if not self._sorted_jobs[prio][id(group)]:
            self._djob_groups[prio].remove(group)

    def cleanup_remaining_djobs(self, prio):
        if prio not in self._sorted_jobs:
            return

        groups = self.get_all_djob_groups(prio=prio)
        if groups is not None:
            for gr in groups:
                gr_id = id(gr)
                if gr_id not in self._sorted_jobs[prio]:
                    continue
                for n in reversed(gr):
                    if id(n) in self._sorted_jobs[prio][gr_id]:
                        func, args, cleanup = self._sorted_jobs[prio][gr_id][id(n)]
                        cleanup(*args)

        del self._sorted_jobs[prio]
        del self._djob_keys[prio]
        del self._djob_groups[prio]

    def __copy__(self):
        new_env = type(self)()
        new_env.__dict__.update(self.__dict__)
        new_env.exhausted_nodes = copy.copy(self.exhausted_nodes)
        new_env.nodes_to_corrupt = copy.copy(self.nodes_to_corrupt)
        new_env.env4NT = copy.copy(self.env4NT)
        new_env._dm = copy.copy(self._dm)

        # DJobs are ignored in the Env copy, because they only matters
        # in the context of one node graph (Nodes + 1 unique Env) for performing delayed jobs
        # in that graph. Indeed, all delayed jobs are registered dynamically
        # (especially in the process of freezing a graph) and does not
        # provide information even in the case of a frozen graph cloning.
        # All DJobs information are ephemeral, they should only exist in the time frame of
        # a node graph operation (e.g., freezing, absorption). If DJobs exists while an Env()
        # is in the process of being copied, it is most probably a bug.
        #
        # WARNING: If DJobs need to evolve in the future to support copy, DJobGroup should be updated
        # during this copy for updating the nodes in its node_list attribute.
        # assert not self._sorted_jobs and not self._djob_keys and not self._djob_groups
        new_env._sorted_jobs = None
        new_env._djob_keys = None
        new_env._djob_groups = None
        # new_env._sorted_jobs = copy.copy(self._sorted_jobs)
        # new_env._djob_keys = copy.copy(self._djob_keys)
        # new_env._djob_groups = copy.copy(self._djob_groups)
        # new_env.id_list = copy.copy(self.id_list)
        # new_env.cpt = 0
        return new_env
import inspect
import string
import sys
import six
import framework.value_types as fvt
from framework.dmhelpers.generic import MH
from framework.error_handling import DataModelDefinitionError, CharsetError, \
    InitialStateNotFoundError, QuantificationError, StructureError, InconvertibilityError, \
    EscapeError, InvalidRangeError, EmptyAlphabetError
from framework.node import Node, NodeInternals_Empty, GenFuncCusto, NonTermCusto, FuncCusto, \
    NodeSemantics, SyncScope, SyncQtyFromObj, SyncSizeObj, NodeCondition, SyncExistenceObj, Env
        @initial
        class Initial(State):

            def _run(self, ctx):
                ctx.min = ""

            def advance(self, ctx):
                if ctx.input.isdigit():
                    return self.machine.Min
                else:
                    raise QuantificationError()

        @register
        class Min(State):

            def _run(self, ctx):
                ctx.min += ctx.input

            def advance(self, context):
                if context.input.isdigit():
                    return self.machine.Min
                elif context.input == ',':
                    return self.machine.Comma
                elif context.input == '}':
                    return self.machine.Final
                else:
                    raise QuantificationError()

        @register
        class Max(State):

            def _run(self, ctx):
                ctx.max += ctx.input

            def advance(self, context):
                if context.input.isdigit():
                    return self.machine.Max
                elif context.input == '}':
                    return self.machine.Final
                else:
                    raise QuantificationError()

        @register
        class Comma(Max):

            def _run(self, ctx):
                ctx.max = ""

        @register
        class Final(State):
            def _run(self, ctx):
                ctx.min = int(ctx.min)

                if ctx.max is None:
                    ctx.max = ctx.min
                elif len(ctx.max) == 0:
                    ctx.max = None
                else:
                    ctx.max = int(ctx.max)

                if ctx.max is not None and ctx.min > ctx.max:
                    raise QuantificationError(u"{X,Y}: X \u2264 Y constraint not respected.")

                ctx.flush()

            def advance(self, context):
                return None

        def advance(self, ctx):
            return self.machine.QtyState.advance(self, ctx)

    class Group(State):

        def advance(self, ctx):
            if ctx.input in (')', '}', ']'):
                raise StructureError(ctx.input)

            elif ctx.input in ('*', '+', '?'):
                return self.machine.QtyState
            elif ctx.input == '{':
                return self.machine.Brackets
            else:
                ctx.flush()

            if ctx.input == '|':
                return self.machine.Choice
            elif ctx.input is None:
                return self.machine.Final

            ctx.append_to_contents("")

            if ctx.input == '(':
                return self.machine.Parenthesis
            elif ctx.input == '[':
                return self.machine.SquareBrackets
            elif ctx.input == '.':
                return self.machine.Dot
            elif ctx.input == '\\':
                return self.machine.Escape
            else:
                return self.machine.Main

    @register
    class Parenthesis(StateMachine, Group):

        @initial
        class Initial(State):

            def _run(self, ctx):
                ctx.start_new_shape_from_buffer()
                if len(ctx.buffer) > 0:
                    ctx.flush()
                    ctx.append_to_contents("")

            def advance(self, ctx):
                if ctx.input in ('?', '*', '+', '{'):
                    raise QuantificationError()
                elif ctx.input in ('}', ']', None):
                    raise StructureError(ctx.input)
                elif ctx.input in ('(', '[', '.'):

                    raise InconvertibilityError()