# -*- coding: utf-8 -*-
"""
Created on Mon Mar 16 10:56:03 2020

@author: Sophie
"""

import ctypes
import weakref

from neticapy import enums
from neticapy import environ as envrn
from neticapy import net as nt
from neticapy import nodelist as ndlst
from neticapy import neticaerror as err
from neticapy.loaddll import Netica
  
def _create_node(cptr):
    """Function called by NeticaPy to create a node.
    
    cptr is the C pointer generated by a call to a Netica DLL function. 
    create_node uses the pointer to check whether a NeticaPy instance of this 
    node already exists, and returns any existing node. A new instance will be 
    created if the pointer isn't present in cptr_dict.
    """
    if envrn.dict_initialization:
        if cptr in envrn.cptr_dict:
            return envrn.cptr_dict[cptr]()
        else:
            return Node(cptr)

    elif envrn.userdata_initialization:
        existing_node = Netica.GetNodeUserData_bn(ctypes.c_void_p(cptr), 0)
        err.checkerr()
        if existing_node:
            py_node = ctypes.cast(existing_node, 
                                 ctypes.POINTER(ctypes.py_object)).contents.value
            return py_node
        else:
            return Node(cptr)
    else:
        return Node(cptr)

class Node:
    
    def __init__(self, cptr):
               
        self.cptr = cptr
        
        self.deleted_by_netica = False
        
        if envrn.dict_initialization:
            envrn.cptr_dict[cptr] = weakref.ref(self)
        
        elif envrn.userdata_initialization:
            cdata = ctypes.cast(ctypes.pointer(ctypes.py_object(self)), ctypes.c_void_p)
            Netica.SetNodeUserData_bn.restype = None
            Netica.SetNodeUserData_bn(ctypes.c_void_p(self.cptr), 0, cdata)
            err.checkerr()
    
    def __del__(self):
        """Remove this node, freeing all resources it consumes, including memory. 
        
        Like Netica-C DeleteNode_bn.
        """
        #PythonOnly
        if envrn.dict_initialization:
            if self.cptr is not None:
                if self.cptr in envrn.cptr_dict:
                    del envrn.cptr_dict[self.cptr]
        self.cptr = None
    
    def delete(self):
        """Remove this node, freeing all resources it consumes, including memory.  
        
        Like Netica-C DeleteNode_bn.
        """
        Netica.DeleteNode_bn.restype = None
        Netica.DeleteNode_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        if envrn.dict_initialization:
            if self.cptr is not None:
                del envrn.cptr_dict[self.cptr]
        self.cptr = None
        
    @property
    def net(self):
        """Return the BNet that this node is part of.  
        
        Like Netica-C GetNodeNet_bn.
        """        
        Netica.GetNodeNet_bn.restype = ctypes.c_void_p
        cptr = Netica.GetNodeNet_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        
        return nt._create_net(cptr)
    
    @property
    def name(self):
        """Name of this node.
        
        Restricted to 30 character alphanumeric.      
        See also 'Title' property.  
        Like Netica-C G/SetNodeName_bn
        """
        Netica.GetNodeName_bn.restype = ctypes.c_char_p
        name = Netica.GetNodeName_bn(ctypes.c_void_p(self.cptr)).decode()
        err.checkerr()
        return name
    
    @name.setter
    def name(self, name):
        """Name of this node.
        
        Restricted to 30 character alphanumeric.      
        See also 'Title' property.  
        Like Netica-C G/SetNodeName_bn
        """
        Netica.SetNodeName_bn.restype = None
        Netica.SetNodeName_bn(ctypes.c_void_p(self.cptr), ctypes.c_char_p(name.encode()))
        err.checkerr()
    
    @property
    def title(self):
        """Unrestricted label for this node.  

        Like Netica-C G/SetNodeTitle_bn.
        """
        Netica.GetNodeTitle_bn.restype = ctypes.c_char_p#.c_wchar_p
        title = Netica.GetNodeTitle_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return title.decode("utf-8", "ignore")
    
    @title.setter
    def title(self, title):
        """Unrestricted label for this node.  

        Like Netica-C G/SetNodeTitle_bn.
        """
        Netica.SetNodeTitle_bn.restype = None
        Netica.SetNodeTitle_bn(ctypes.c_void_p(self.cptr), ctypes.c_char_p(title.encode()))
        err.checkerr()
        
    @property
    def comment(self):
        """Documentation or description of this node.  
        
        Like Netica-C G/SetNodeComment_bn.
        """
        Netica.GetNodeComment_bn.restype = ctypes.c_char_p
        comment = Netica.GetNodeComment_bn(ctypes.c_void_p(self.cptr)).decode()
        err.checkerr()
        return comment
    
    @comment.setter
    def comment(self, comment):
        """Documentation or description of this node.  
        
        Like Netica-C G/SetNodeComment_bn.
        """
        Netica.SetNodeComment_bn.restype = None
        Netica.SetNodeComment_bn(ctypes.c_void_p(self.cptr), ctypes.c_char_p(comment.encode()))
        err.checkerr()

    @property
    def state_numbers(self):
        """***nodocs
        """
        clength = ctypes.c_int(0)
        Netica.GetNodeStateNumbers_bn.restype = ctypes.POINTER(ctypes.c_double)
        state_nums_pointer = Netica.GetNodeStateNumbers_bn(ctypes.c_void_p(self.cptr),
                                                           ctypes.byref(clength))
        err.checkerr()
        length = clength.value
        if state_nums_pointer:
            state_numbers = []
            for i in range(length):
                state_numbers.extend([state_nums_pointer[i]])
        else:
            state_numbers = None
        return state_numbers

    @state_numbers.setter
    def state_numbers(self, levels):
        """***nodocs
        """
        numstates = len(levels)
        Netica.SetNodeStateNumbers_bn.restype = None
        Netica.SetNodeStateNumbers_bn(ctypes.c_void_p(self.cptr), (ctypes.c_double*len(levels))(*levels), 
                                      ctypes.c_int(numstates), None)
        err.checkerr()

    def set_state_numbers(self, levels, options=None):
        """***nodocs
        """
        if options:
            options += ", change_num_states"
        else:
            options = "change_num_states"
        options = ctypes.c_char_p(options.encode())

        numstates = len(levels)
        Netica.SetNodeStateNumbers_bn.restype = None
        Netica.SetNodeStateNumbers_bn(ctypes.c_void_p(self.cptr), (ctypes.c_double*len(levels))(*levels), 
                                      ctypes.c_int(numstates), None)
        err.checkerr()

    @property
    def intervals(self):
        """***nodocs
        """
        clength = ctypes.c_int(0)
        Netica.GetNodeIntervals_bn.restype = ctypes.POINTER(ctypes.c_double)
        intervals_pointer = Netica.GetNodeIntervals_bn(ctypes.c_void_p(self.cptr),
                                                       ctypes.byref(clength))
        err.checkerr()
        length = clength.value
        if intervals_pointer:
            intervals = []
            for i in range(length):
                intervals.extend([intervals_pointer[i]])
        else:
            intervals = None
        return intervals

    @intervals.setter
    def intervals(self, levels):
        """***nodocs
        """
        numstates = len(levels) - 1
        Netica.SetNodeIntervals_bn.restype = None
        Netica.SetNodeIntervals_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(numstates),
                                   (ctypes.c_double*len(levels))(*levels))
        err.checkerr()
 
    '''
    def set_state_levels(self, levels):
        """Set state level.
        
        If this node is for a continuous variable, then this is a discretization 
        threshold, otherwise it is a state-number.  
        Like Netica-C SetNodeLevels_bn.
        """
        if self.node_type is 'DISCRETE':
            numstates = len(levels)
        elif self.node_type is 'CONTINUOUS':
            numstates = len(levels) - 1
        else:
            raise NeticaPyError("Netica Error 6500: Node must be discrete or continuos")
        Netica.SetNodeLevels_bn.restype = None
        Netica.SetNodeLevels_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(numstates), 
                                (ctypes.c_double*len(levels))(*levels))
        err.checkerr()  
       '''     
   
    @property
    def node_type(self):
        """Return whether this node is for a discrete or continuous variable.  
        
        If you want to detect discrete variables and continuous variables that 
        have been discretized, instead use num_states property not-equal zero.  
        Like Netica-C GetNodeType_bn.
        """
        Netica.GetNodeType_bn.restype = ctypes.c_int
        nodetype = Netica.GetNodeType_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return enums.get_node_type(nodetype)
 
    @property
    def kind(self):
        """How this node is being used (nature, decision, etc).  
        
        Like Netica-C G/SetNodeKind_bn.
        """
        Netica.GetNodeKind_bn.restype = ctypes.c_int
        nodekind = Netica.GetNodeKind_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return enums.get_node_kind(nodekind)
    
    @kind.setter
    def kind(self, kind):
        """How this node is being used (nature, decision, etc).  
        
        Like Netica-C G/SetNodeKind_bn.
        """
        Netica.SetNodeKind_bn.restype = None
        Netica.SetNodeKind_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(enums.set_node_kind(kind)))
        err.checkerr()
       
    @property
    def num_states(self):
        """Return the number of states this node has. 
        
        0 for undiscretized continuous nodes.  
        Like Netica-C GetNodeNumberStates_bn.
        """
        Netica.GetNodeNumberStates_bn.restype = ctypes.c_int
        node_num_states = Netica.GetNodeNumberStates_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return node_num_states 

    def set_state_name(self, state, statename):
        """Set the name of this node's state identified by the given index.
        
        Restricted to 30 character alphanumeric.  
        See also StateTitle.  
        Like Netica-C SetNodeStateName_bn.
        """
        Netica.SetNodeStateName_bn.restype = None
        Netica.SetNodeStateName_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(state), 
                                   ctypes.c_char_p(statename.encode()))
        err.checkerr()
        
    def get_state_name(self, state):
        """Return the name of this node's state identified by the given index.
        
        Restricted to 30 character alphanumeric.  
        See also StateTitle.  
        Like Netica-C GetNodeStateName_bn.
        """
        Netica.GetNodeStateName_bn.restype = ctypes.c_char_p
        statename = Netica.GetNodeStateName_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(state)).decode()
        err.checkerr()
        return statename
     
    @property
    def state_names(self):
        """The names of this node's states separated by commas.
        
        See also get_state_name/set_state_name (just one state).  
        Like Netica-C SetNodeStateNames_bn.
        """
        statenames = []
        Netica.GetNodeStateName_bn.restype = ctypes.c_char_p
        for i in range(self.num_states):
            statenames.append(Netica.GetNodeStateName_bn(ctypes.c_void_p(self.cptr), i).decode())
        err.checkerr()
        statenames = ", ".join(statenames)
        return statenames
    
    @state_names.setter
    def state_names(self, statenames):
        """The names of this node's states separated by commas.
        
        See also get_state_name/set_state_name (just one state).  
        Like Netica-C SetNodeStateNames_bn.
        """
        if statenames is not None:
            statenames = ctypes.c_char_p(statenames.encode())
        Netica.SetNodeStateNames_bn.restype = None
        Netica.SetNodeStateNames_bn(ctypes.c_void_p(self.cptr), statenames)
        err.checkerr()
    
    def set_state_title(self, state, title):
        """Set the unrestricted title of this node's state identified by the given index.  
        
        See also StateName.  
        Like Netica-C SetNodeStateTitle_bn.
        """
        Netica.SetNodeStateTitle_bn.restype = None
        Netica.SetNodeStateTitle_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(state), 
                                   ctypes.c_char_p(title.encode()))
        err.checkerr()      
    
    def get_state_title(self, state):
        """Get the unrestricted title of this node's state identified by the given index.  
        
        See also StateName.  
        Like Netica-C GetNodeStateTitle_bn.
        """
        Netica.GetNodeStateTitle_bn.restype = ctypes.c_char_p
        statetitle = Netica.GetNodeStateTitle_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(state)).decode()
        err.checkerr()
        return statetitle
    
    def set_state_comment(self, state, comment):
        """Set the comment of this node's state identified by the given index.  
        
        Like Netica-C SetNodeStateComment_bn.
        """
        Netica.SetNodeStateComment_bn.restype = None
        Netica.SetNodeStateComment_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(state), 
                                   ctypes.c_char_p(comment.encode()))
        err.checkerr()      
    
    def get_state_comment(self, state):
        """Return the comment of this node's state identified by the given index.  
        
        Like Netica-C GetNodeStateComment_bn.
        """
        Netica.GetNodeStateComment_bn.restype = ctypes.c_char_p
        statecomment = Netica.GetNodeStateComment_bn(ctypes.c_void_p(self.cptr), 
                                                     ctypes.c_int(state)).decode()
        err.checkerr()
        return statecomment
    
    def get_state_named(self, state):
        """Get the index of the state with the given name. 
        
        Warning: other nodes with this state name may use a different index.  
        Like Netica-C GetStateNamed_bn.
        """
        Netica.GetStateNamed_bn.restype = ctypes.c_int
        statenum = Netica.GetStateNamed_bn(ctypes.c_char_p(state.encode()), 
                                           ctypes.c_void_p(self.cptr))
        err.checkerr()
        if statenum < 0:
            return enums.get_state(statenum) 
        else:
            return statenum
    
    @property
    def parents(self):
        """Return list of the parent nodes of this node.  
        
        That is, those nodes which are at the beginning of a link that enters 
        this node.  
        Creates a constant NodeList, i.e. one that cannot be 
        modified or deleted.  To make modifications, duplicate this Nodelist, 
        using NodeList.copy.
        Like Netica-C GetNodeParents_bn.
        """
        Netica.GetNodeParents_bn.restype = ctypes.c_void_p
        cptr = Netica.GetNodeParents_bn(ctypes.c_void_p(self.cptr)) 
        err.checkerr()
        
        return ndlst._create_nodelist(cptr, is_const=True)
    
    @property
    def children(self):
        """Return list of the child nodes of this node.  
        
        That is, those nodes which are at the end of a link that exits this node.  
        Creates a constant NodeList, i.e. one that cannot be 
        modified or deleted.  To make modifications, duplicate this Nodelist, 
        using NodeList.copy.
        Like Netica-C GetNodeChildren_bn.
        """
        Netica.GetNodeChildren_bn.restype = ctypes.c_void_p
        cptr = Netica.GetNodeChildren_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        
        return ndlst._create_nodelist(cptr, is_const=True)  
    
    def set_input_name(self, link_index, name):
        """Set the name of this node's input (i.e. link) identified by the given index.  
        
        Like Netica-C SetNodeInputName_bn.
        """
        Netica.SetNodeInputName_bn.restype = None
        Netica.SetNodeInputName_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(link_index), 
                                   ctypes.c_char_p(name.encode()))
        err.checkerr()      
    
    def get_input_name(self, link_index):
        """Return the name of this node's input (i.e. link) identified by the given index.  
        
        Like Netica-C GetNodeInputName_bn.
        """
        Netica.GetNodeInputName_bn.restype = ctypes.c_char_p
        inputname = Netica.GetNodeInputName_bn(ctypes.c_void_p(self.cptr), 
                                                     ctypes.c_int(link_index)).decode()
        err.checkerr()
        return inputname
    
    def get_input_named(self, name):
        """Return the link index number of the link whose name is name.
        
        Returns -1 if there isn't one with that name (case sensitive comparison).
        Like Netica-C GetInputNamed_bn.
        """
        Netica.GetInputNamed_bn.restype = ctypes.c_int
        inputindex = Netica.GetInputNamed_bn(ctypes.c_char_p(name.encode()),
                                             ctypes.c_void_p(self.cptr))
        err.checkerr()
        return inputindex
 
    def set_input_delay(self, link_index, delay, dimension=0):
        """Set the time delay for the given link of this node in a dynamic Bayes net.  
        
        For link_index, can pass the name or the index of the link.
        Like Netica-C SetNodeInputDelay_bn.
        """
        if isinstance(link_index, str):
            link_index = self.get_input_named(link_index)
        if isinstance(delay, int) or isinstance(delay, float):
            delay = str(delay)
        Netica.SetNodeInputDelay_bn.restype = None
        Netica.SetNodeInputDelay_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(link_index),
                                    ctypes.c_int(dimension), ctypes.c_char_p(delay.encode()))
        err.checkerr()
  
    def set_persistence(self, dimension, persistence):
        """Set the amount of time that the value of this node can be considered 
        the same, before it must be recalculated in the next time slice of a 
        dynamic Bayes net.  
        
        Like Netica-C SetNodePersistence_bn.
        """
        Netica.SetNodePersistence_bn.restype = None
        Netica.SetNodePersistence_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(dimension),
                                     ctypes.c_char_p(persistence.encode()))
        err.checkerr()

    @property
    def equation(self):
        """Equation giving the probability of this node conditioned on its parent nodes, 
        or the value of this node as a function of its parents.  
        
        When setting, pass None for eqn to remove equation.
        Like Netica-C G/SetNodeEquation_bn.
        """
        Netica.GetNodeEquation_bn.restype = ctypes.c_char_p
        eqn = Netica.GetNodeEquation_bn(ctypes.c_void_p(self.cptr)).decode()
        err.checkerr()
        return eqn
    
    @equation.setter
    def equation(self, eqn):
        """Equation giving the probability of this node conditioned on its parent nodes, 
        or the value of this node as a function of its parents.  
        
        When setting, pass None for eqn to remove equation.
        Like Netica-C G/SetNodeEquation_bn.
        """
        if eqn is not None:
            eqn = ctypes.c_char_p(eqn.encode())
        Netica.SetNodeEquation_bn.restype = None
        Netica.SetNodeEquation_bn(ctypes.c_void_p(self.cptr), eqn)
        err.checkerr()
    
    def equation_to_table(self, num_samples, samp_unc, add_exist):
        """Build this node's CPT based on its equation.  
        
        See also the equation attribute.  
        Like Netica-C EquationToTable_bn.
        """
        Netica.EquationToTable_bn.restype = None
        Netica.EquationToTable_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(num_samples), 
                                  ctypes.c_ubyte(samp_unc), ctypes.c_ubyte(add_exist))
        err.checkerr()
        
    def delete_tables(self):
        """Delete the CPT, experience, and function tables for this node, if it has any.
        
        Like Netica-C DeleteNodeTables_bn.
        """
        Netica.DeleteNodeTables_bn.restype = None
        Netica.DeleteNodeTables_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()

    def has_table(self, complete=False):
        """Return True if node has a function table or a CPT table, otherwise False.
        
        It ignores experience tables.
        If complete is non-None, it is set to indicate whether node has a 
        complete table, i.e., none of the entries are undefined.
        Like Netica-C HasNodeTable_bn.
        """
        Netica.HasNodeTable_bn.retype = ctypes.c_bool
        has_table = Netica.HasNodeTable_bn(ctypes.c_void_p(self.cptr), 
                                           ctypes.c_bool(complete))
        err.checkerr()
        return bool(has_table)

    def reverse_link(self, parent):                 # May want to rename to reverse_link_from
        """Reverse the direction of the link from 'parent', keeping the overall 
        joint probabilities of the net unchanged.  
        
        For 'parent', you may pass the input name, input index, parent Node or 
        parent name.  
        Like Netica-C ReverseLink_bn.
        """    
        if isinstance(parent, str):
            parent_node = self.net.get_node_named(parent)
            if parent_node:
                index = self.parents.index_of(parent_node)                
            if parent_node is None or index is None:
                index = self.get_input_named(parent)
                if index == -1:
                    errmesg = "Function Node.reverse_link \
expects either the input name, input index, parent Node or parent \
name, but received a string that is not the name of any parent node or input"

                    err._raise_netica_error(6503, errmesg)
                
            link_index = ctypes.c_int(index)
        elif isinstance(parent, Node):
            link_index = ctypes.c_int(self.parents.index_of(parent))
        elif isinstance(parent, int):
            link_index = ctypes.c_int(parent)
        else:
            raise TypeError('A Node, str, or int is required (got type {})'.format(type(parent).__name__))
        Netica.ReverseLink_bn.restype = None
        Netica.ReverseLink_bn(link_index, ctypes.c_void_p(self.cptr))
        err.checkerr()  

    def is_deterministic(self):
        """Whether the value of this node given its parents is deterministic 
        (versus probabilistic).  
        
        Like Netica-C IsNodeDeterministic_bn.
        """
        Netica.IsNodeDeterministic_bn.retype = ctypes.c_bool
        is_deterministic = Netica.IsNodeDeterministic_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return bool(is_deterministic)
    
    def set_user_field(self, name, data, kind=0):
        """Attach user defined data to this net under category 'field_name'.  
        
        Like Netica-C SetNodeUserField_bn.
        """
        if isinstance(data, str):
            data = data.encode()
        if isinstance(data, int):
            data = str(data).encode()
        length = len(data)
        Netica.SetNodeUserField_bn.restype = None
        Netica.SetNodeUserField_bn(ctypes.c_void_p(self.cptr), ctypes.c_char_p(name.encode()),
                                  ctypes.c_char_p(data), ctypes.c_int(length), ctypes.c_int(kind))
        err.checkerr()      

    def get_user_field(self, name, return_type=None, kind=0):
        """Return user defined data to this net under category 'field_name'. 
        
        return_type can be set to 'str' or 'int' to convert the user field from
        a bytearray to a string or integer.
        Like Netica-C GetNodeUserField_bn.
        """
        clength = ctypes.c_int(0)
        Netica.GetNodeUserField_bn.restype = ctypes.POINTER(ctypes.c_ubyte)
        data_pointer = Netica.GetNodeUserField_bn(ctypes.c_void_p(self.cptr), ctypes.c_char_p(name.encode()),
                                         ctypes.byref(clength), ctypes.c_int(kind))
        err.checkerr()
        length = clength.value
        data = bytearray()
        
        for i in range(length):
            data.append(data_pointer[i])
        
        if return_type == 'str':
            data = data.decode()
        if return_type == 'int':
            data = int(data)
        
        return data

    def get_nth_user_field(self, index, kind=0):
        """Return the Nth element of user defined data from this net, and its 
        category 'field_name'.  
        
        Like Netica-C GetNodeNthUserField_bn.
        """
        clength = ctypes.c_int(0)
        cname = ctypes.c_char_p(b'')
        cvalue = ctypes.pointer(ctypes.c_ubyte(0))
        Netica.GetNodeNthUserField_bn.restype = None
        Netica.GetNodeNthUserField_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(index),
                                      ctypes.byref(cname), ctypes.byref(cvalue),
                                      ctypes.byref(clength), ctypes.c_int(kind))
        err.checkerr()
        length = clength.value
        name = cname.value
        data = bytearray()
        
        for i in range(length):
            data.append(cvalue[i])
        
        return name, data

    def set_vis_position(self, x, y, vis=None):
        """Set x, y to the coordinates of the center of node.
        

        This is useful when directly programming Netica Application, or before 
        writing a net to a file that will later be read by Netica Application.
        The min x is 84.0 pixels and the min y is 33.0 pixels.
        Like Netica-C SetNodeVisPosition_bn.
        """
        if vis is not None:
            vis = ctypes.c_char_p(vis.encode())
        Netica.SetNodeVisPosition_bn.restype = None
        Netica.SetNodeVisPosition_bn(ctypes.c_void_p(self.cptr), vis, 
                                     ctypes.c_double(x), ctypes.c_double(y))
        err.checkerr()

    def get_vis_position(self, vis=None):
        """Return x, y coordinates of the center of node.
        
        Set as it would appear in a visual display, e.g., in Netica Application.
        The min x is 84.0 pixels and the min y is 33.0 pixels.
        Like Netica-C GetNodeVisPosition_bn.
        """
        if vis is not None:
            vis = ctypes.c_char_p(vis.encode())
        cx = ctypes.c_double(0)
        cy = ctypes.c_double(0)
        Netica.GetNodeVisPosition_bn.restype = None
        Netica.GetNodeVisPosition_bn(ctypes.c_void_p(self.cptr), vis, 
                                     ctypes.byref(cx), ctypes.byref(cy))
        err.checkerr()
        return cx.value, cy.value

    def set_vis_style(self, style, vis=None):
        """The style to draw the node in the net diagram.
        
        style must be one of: "Default", "Absent", "Shape", "LabeledBox", 
        "BeliefBars", "BeliefLine", or "Meter" 
        Like Netica-C SetNodeVisStyle_bn.
        """
        if vis is not None:
            vis = ctypes.c_char_p(vis.encode())
        Netica.SetNodeVisStyle_bn.retype = None
        Netica.SetNodeVisStyle_bn(ctypes.c_void_p(self.cptr), vis, 
                                  ctypes.c_char_p(style.encode()))
        err.checkerr()

    def get_vis_style(self, vis=None):
        """Return the current "style descriptor" of node for any visual display, 
        e.g., in Netica Application.

        The returned string may be used as a parameter to SetNodeVisStyle_bn.
        Like Netica-C GetNodeVisStyle_bn.
        """
        if vis is not None:
            vis = ctypes.c_char_p(vis.encode())
        Netica.GetNodeVisStyle_bn.restype = ctypes.c_char_p
        style = Netica.GetNodeVisStyle_bn(ctypes.c_void_p(self.cptr), vis)
        err.checkerr()
        return style.decode()
    
    def add_link_from(self, parent):
        """Add a link from 'parent' node to this node.
        
        Like Netica-C AddLink_bn.
        """
        if not isinstance(parent, Node):
            raise TypeError('A Node is required (got type {})'.format(type(parent).__name__))
        
        Netica.AddLink_bn.restype = ctypes.c_int
        Netica.AddLink_bn(ctypes.c_void_p(parent.cptr), ctypes.c_void_p(self.cptr))
        err.checkerr()
   
    def delete_link_from(self, parent):
        """Delete the link entering this node from 'parent' node.  
        
        For 'parent' you may pass the input name, input index, parent Node or 
        parent name.  
        Like Netica-C DeleteLink_bn.
        """
        if isinstance(parent, str):
            parent_node = self.net.get_node_named(parent)
            if parent_node:
                index = self.parents.index_of(parent_node)                
            if parent_node is None or index is None:
                index = self.get_input_named(parent)
                if index == -1:
                    errmesg = "Function Node.delete_link_from \
expects either the input name, input index, parent Node or parent \
name, but received a string that is not the name of any parent node or input"

                    err._raise_netica_error(6501, errmesg)
                
            link_index = ctypes.c_int(index)
        elif isinstance(parent, Node):
            link_index = ctypes.c_int(self.parents.index_of(parent))
        elif isinstance(parent, int):
            link_index = ctypes.c_int(parent)
        else:
            raise TypeError('A Node, str, or int is required (got type {})'.format(type(parent).__name__))
        Netica.DeleteLink_bn.restype = None
        Netica.DeleteLink_bn(link_index, ctypes.c_void_p(self.cptr))
        err.checkerr()
     
    def switch_parent(self, parent, new_parent):
        """Switch the parent of the identified link with 'new_parent' node.  
        
        For 'parent' you may pass the input name, input index, parent Node or 
        parent name, and for 'new_parent', you may pass its name or Node.  
        Like Netica-C SwitchNodeParent_bn.
        """
        if isinstance(parent, str):
            try:
                parent_node = self.net.get_node_named(parent)
                index = self.parents.index_of(parent_node)
            except:
                index = self.get_input_named(parent)
                if index == -1:                  
                    errmesg = "Function Node.switch_parent \
expects either the input name, input index, parent Node or parent \
name, but received a string that is not the name of any parent node or input"

                    err._raise_netica_error(6502, errmesg)

            int_parent = ctypes.c_int(index)
        elif isinstance(parent, int):
            int_parent = ctypes.c_int(parent)
        else:
            int_parent = self.parents.index_of(parent)
            int_parent = ctypes.c_int(int_parent)
        
        if isinstance(new_parent, str):
            new_parent = self.net.get_node_named(new_parent).cptr
        elif new_parent is not None:
            if not isinstance(new_parent, Node):
                raise TypeError('A Node or string is required (got type {})'.format(type(new_parent).__name__))
            new_parent = new_parent.cptr
            
        Netica.SwitchNodeParent_bn.restype = None
        Netica.SwitchNodeParent_bn(int_parent, ctypes.c_void_p(self.cptr), 
                                   ctypes.c_void_p(new_parent))
        err.checkerr()
             
    def is_related(self, relation, node):
        """Return whether this node is related to 'node' by 'relation'.  
        
        For example Node.is_node_related("parent", other_node) returns whether 
        this node is a parent of other_node.  relation should be one of 
        "parent", "child", "ancestor", "descendent", "connected", 
        "markov_blanket", "d_connected".
        Like Netica-C IsNodeRelated_bn.
        """
        if not isinstance(node, Node):
            raise TypeError('A Node is required (got type {})'.format(type(node).__name__))
        Netica.IsNodeRelated_bn.restype = ctypes.c_bool
        is_related = Netica.IsNodeRelated_bn(ctypes.c_void_p(self.cptr),
                                             ctypes.c_char_p(relation.encode()),
                                             ctypes.c_void_p(node.cptr))
        return bool(is_related)
     
    def get_related_nodes(self, nodelist, relation):
        """Put in 'related_nodes' those nodes that bear 'relation' to this node.  
        
        See also Net.get_related_nodes.  
        Like Netica-C GetRelatedNodes_bn.
        """
        if not isinstance(nodelist, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(nodelist).__name__))
        Netica.GetRelatedNodes_bn.restype = None
        Netica.GetRelatedNodes_bn(ctypes.c_void_p(nodelist.cptr), 
                                  ctypes.c_char_p(relation.encode()),
                                  ctypes.c_void_p(self.cptr))
        err.checkerr()    
    
    def add_states(self, first_state, state_names, num_states, cpt_fill=-1):
        """Add 'num_states' states, with the first to go after 'first_state'.  
        
        'state_names' can be empty, or if the existing states have names, it 
        can be a comma-delimited list of names for the new states.  
        Like Netica-C AddNodeStates_bn.
        """
        if state_names is not None:
            state_names = ctypes.c_char_p(state_names.encode())
        Netica.AddNodeStates_bn.restype = None
        Netica.AddNodeStates_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(first_state), 
                                state_names, ctypes.c_int(num_states), ctypes.c_double(cpt_fill))
        err.checkerr()
   
    def remove_state(self, state):
        """Remove 'state' from this node.  
        
        For 'state' pass its index or name.  
        Like Netica-C RemoveNodeState_bn.
        """
        if isinstance(state, str):
            state_num = self.get_state_named(state)
            state = ctypes.c_int(state_num)
        elif isinstance(state, int):
            state = ctypes.c_int(state)
        else:
            raise TypeError('A string or int is required (got type {})'.format(type(state).__name__))
        Netica.RemoveNodeState_bn.restype = None
        Netica.RemoveNodeState_bn(ctypes.c_char_p(self.cptr), state)
        err.checkerr()

    def reorder_states(self, new_order):
        """Put this node's states in the order given by 'new_order', which must 
        be an array of the existing states, indexes or names.
        
        Like Netica-C ReorderNodeStates2_bn.
        """
        length = len(new_order)
        cnew_order = (ctypes.c_int*length)(*new_order)
        Netica.ReorderNodeStates2_bn.restype = None
        Netica.ReorderNodeStates2_bn(ctypes.c_void_p(self.cptr), cnew_order, 
                                     ctypes.c_int(length))
        err.checkerr()

    def enter_finding(self, state):
        """Enter a finding for this node as an int, boolean, or string.  
        
        Like Netica-C EnterFinding_bn.
        """
        Netica.EnterFinding_bn.restype = None
        if isinstance(state, str):
            state = self.get_state_named(state)
        Netica.EnterFinding_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(state))
        err.checkerr()
    
    def enter_finding_not(self, state):
        """Enter a negative finding for this node as an int, boolean, or string.
        
        Finding entered indicates that it's not in the given state.  
        Like Netica-C EnterFindingNot_bn.
        """
        Netica.EnterFindingNot_bn.restype = None
        if isinstance(state, str):
            state = self.get_state_named(state)
        Netica.EnterFindingNot_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(state))
        err.checkerr()  
    
    def enter_value(self, value):
        """Enters a numerical real-valued finding for this node.  
            
        Like Netica-C EnterNodeValue_bn.
        """
        Netica.EnterNodeValue_bn.restype = None
        Netica.EnterNodeValue_bn(ctypes.c_void_p(self.cptr), ctypes.c_double(value))
        err.checkerr()             

    def enter_likelihood(self, likelihood):
        """Enter a likelihood vector for this node.  
        
        It is an array of one probability per state that need not sum to 1.  
        Like Netica-C EnterNodeLikelihood2_bn.
        """
        length = len(likelihood)
        clikelihood = (ctypes.c_float*length)(*likelihood)
        Netica.EnterNodeLikelihood2_bn.restype = None
        Netica.EnterNodeLikelihood2_bn(ctypes.c_void_p(self.cptr), 
                                       clikelihood, ctypes.c_int(length))
        err.checkerr()
    
    def enter_calibration(self, calibration):
        """For use by Norsys only.  
        
        Pass an array of one probability per state that sum to 1.  
        Like Netica-C EnterNodeCalibration_bn.
        """
        ccalibration = (ctypes.c_float*len(calibration))(*calibration)
        Netica.EnterNodeCalibration_bn.restype = None
        Netica.EnterNodeCalibration_bn(ctypes.c_void_p(self.cptr), ccalibration,
                                       ctypes.c_int(len(calibration)))
        err.checkerr()

    def enter_interval_finding(self, low, high):
        """Enter the finding that the value of this node is somewhere between low and high.  
        
        Like Netica-C EnterIntervalFinding_bn.
        """
        Netica.EnterIntervalFinding_bn.restype = None
        Netica.EnterIntervalFinding_bn(ctypes.c_void_p(self.cptr), ctypes.c_double(low),
                                       ctypes.c_double(high))
        err.checkerr()

    def enter_gaussian_finding(self, mean, std_dev):
        """Enter a Gaussian, i.e. 'normal', distributed likelihood finding.  
        
        Like Netica-C EnterGaussianFinding_bn.
        """
        Netica.EnterGaussianFinding_bn.restype = None
        Netica.EnterGaussianFinding_bn(ctypes.c_void_p(self.cptr), ctypes.c_double(mean),
                                       ctypes.c_double(std_dev))
        err.checkerr()    
     
    def get_finding(self):
        """Return the state finding entered for this node.
        
        Returns a 'SpecialFinding' code if another kind of finding is entered.  
        Like Netica-C GetNodeFinding_bn.
        """
        Netica.GetNodeFinding_bn.restype = ctypes.c_int
        finding = Netica.GetNodeFinding_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        if finding < 0:
            finding = enums.get_finding(finding)
        return finding
   
    def get_value_entered(self):
        """Return the real value finding entered for this node.  
        
        Like Netica-C GetNodeValueEntered_bn.
        """
        Netica.GetNodeValueEntered_bn.restype = ctypes.c_double
        val_entered = Netica.GetNodeValueEntered_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return val_entered
    
    def get_likelihood(self):
        """Return the accumulated (likelihood and other) findings for this node 
        as a likelihood for 'state'.  
        
        Like Netica-C GetNodeLikelihood2_bn.
        """
        clength = ctypes.c_int(0)
        Netica.GetNodeLikelihood2_bn.restype = ctypes.POINTER(ctypes.c_float)
        likelihood_pointer = Netica.GetNodeLikelihood2_bn(ctypes.c_void_p(self.cptr),
                                                          ctypes.byref(clength))
        err.checkerr()     
        length = clength.value
        if likelihood_pointer:
            likelihood = []
            for i in range(length):
                likelihood.extend([likelihood_pointer[i]]) 
        else:
            likelihood = None
        return likelihood
    
    def retract_findings(self):
        """Retract findings previously entered for this node. 
        
        Retract state, real-valued and likelihood findings.  
        Like Netica-C RetractNodeFindings_bn.
        """
        Netica.RetractNodeFindings_bn.restype = None
        Netica.RetractNodeFindings_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()     
   
    def calc_state(self):
        """Gets this node's state calculated from neighboring nodes, if their 
        values can be found, and the relationship with those nodes will allow 
        it, e.g. is deterministic.  
        
        See also calc_value, get_beliefs.  
        Like Netica-C CalcNodeState_bn.
        """
        Netica.CalcNodeState.restype = ctypes.c_int
        state = Netica.CalcNodeState(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return state
     
    def calc_value(self):
        """Gets this node's value calculated from neighboring nodes, if their 
        values can be found, and the relationship with those nodes will allow 
        it, e.g. is deterministic.  
        
        See also calc_state.  
        Like Netica-C CalcNodeValue_bn.
        """
        Netica.CalcNodeValue_bn.restype = ctypes.c_double
        value = Netica.CalcNodeValue_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return value

    def is_belief_updated(self):
        """Return whether the beliefs for this node have already been 
        calculated by belief propagation.  
        
        Like Netica-C IsBeliefUpdated_bn.
        """
        Netica.IsBeliefUpdated_bn.retype = ctypes.c_bool
        is_updated = Netica.IsBeliefUpdated_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return bool(is_updated) 
        
    def get_beliefs(self):
        """Return a belief vector indicating the current probability for each state of node.
        
        Gets the current belief for each state of this nature node, taking into 
        account all findings entered in the net. See also CalcState, 
        GetExpectedValue, GetExpectedUtility.
        Like Netica-C GetNodeBeliefs2_bn.
        """ 
        clength = ctypes.c_int(0)
        Netica.GetNodeBeliefs2_bn.restype = ctypes.POINTER(ctypes.c_float)
        beliefs_pointer = Netica.GetNodeBeliefs2_bn(ctypes.c_void_p(self.cptr),
                                                    ctypes.byref(clength))
        err.checkerr()       
        length = clength.value
        if beliefs_pointer:
            beliefs = []
            for i in range(length):
                beliefs.extend([beliefs_pointer[i]])  
        else:
            beliefs = None
        return beliefs 
    
    def get_expected_value(self, num_moments=1):
        """Return the expected value for this real valued node.
        
        Passing higher values for num_moments (to a maximum of 4) will produce
        a tuple containing the moments of the funtion. For example, passing 2 
        will return the expected value and the standard deviation. Results are
        based on the current beliefsof the node (i.e. taking into account all 
        findings entered in the net).  
        
        See also get_beliefs, get_expected_utils.  
        Like Netica-C GetNodeExpectedValue_bn.
        """
        if num_moments < 1 or num_moments > 4:
            raise ValueError('num_moments must be between 1 and 4 inclusive')
        if num_moments > 1:
            std_dev = ctypes.c_double(0)
            std_dev_ref = ctypes.pointer(std_dev)
        else:
            std_dev_ref = None
        if num_moments > 2:
            x3 = ctypes.c_double(0)
            x3_ref = ctypes.pointer(x3)
        else:
            x3_ref = None
        if num_moments > 3:
            x4 = ctypes.c_double(0)
            x4_ref = ctypes.pointer(x4)
        else:
            x4_ref = None
        Netica.GetNodeExpectedValue_bn.restype = ctypes.c_double
        mean = Netica.GetNodeExpectedValue_bn(ctypes.c_void_p(self.cptr),
                                              std_dev_ref, x3_ref, x4_ref)
        err.checkerr()
        
        if num_moments == 1:
            return mean
        if num_moments > 1:
            return mean, std_dev.value
        if num_moments > 2:
            return mean, std_dev.value, x3.value
        if num_moments > 3:
            return mean, std_dev.value, x3.value, x4.value
    
    def enter_action(self, state):
        """***nodocs
        """
        Netica.EnterAction_bn.restype = None
        if isinstance(state, str):
            state = self.get_state_named(state)
        Netica.EnterAction_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(state))
        err.checkerr()  

    def enter_action_value(self, value):
        """***nodocs
        """
        Netica.EnterActionValue_bn.restype = None
        Netica.EnterActionValue_bn(ctypes.c_void_p(self.cptr), ctypes.c_double(value))
        err.checkerr()  

    def enter_action_randomized(self, probs):
        """***nodocs
        """
        length = len(probs)
        cprobs = (ctypes.c_float*length)(*probs)
        Netica.EnterActionRandomized_bn.restype = None
        Netica.EnterActionRandomized_bn(ctypes.c_void_p(self.cptr), 
                                        cprobs, ctypes.c_int(length))
        err.checkerr()
    
    def get_median_value(self, quantile=None):
        """Return the median value and quantile position and width
        
        If quantile is None (default) returns median value. If a value is 
        passed for quantile, returns the median value, quantile postion, and 
        quantile width for the specified quantile.
        Like Netica-C GetNodeMedianValue_bn.
        """
        return_median_only = False
        if quantile is None:
            return_median_only = True
        else:
            quantile = ctypes.c_double(quantile)
        quantile_position = ctypes.c_double(0)
        quantile_position_ref = ctypes.pointer(quantile_position)
        width = ctypes.c_double(0)
        width_ref = ctypes.pointer(width)
        Netica.GetNodeMedianValue_bn.restype = ctypes.c_double
        median_value = Netica.GetNodeMedianValue_bn(ctypes.c_void_p(self.cptr),
                                                    quantile_position_ref, width_ref,
                                                    quantile)
        err.checkerr()
        if return_median_only:
            return median_value
        else:
            return median_value, quantile_position.value, width.value
    
    def get_expected_utils(self):
        """Get the current expected utility for each choice of this decision node. 
        
        Takes into account all findings entered in the net.     
        See also get_beliefs, get_expected_value.  
        Like Netica-C GetNodeExpectedUtils2_bn.
        """
        clength = ctypes.c_int(0)
        Netica.GetNodeExpectedUtils2_bn.restype = ctypes.POINTER(ctypes.c_float)
        expectedutils_pointer = Netica.GetNodeExpectedUtils2_bn(ctypes.c_void_p(self.cptr),
                                                                ctypes.byref(clength))
        err.checkerr()       
        length = clength.value
        if expectedutils_pointer:
            expectedutils = []
            for i in range(length):
                expectedutils.extend([expectedutils_pointer[i]])
        else:
            expectedutils = None
        return expectedutils

    def set_func_state(self, parentstates, state):
        """Set the table entry giving the state of this node for the row 'parentstates'. 
        
        For parentstates, pass an array of state indexes or a comma-delimited 
        string of state names in the same order as this node's parents.  
        For continuous nodes, normally use RealFuncTable instead.  
        Like Netica-C SetNodeFuncState_bn.
        """
        # Currently untested
        if parentstates is None:
            cparentstates = parentstates
            
        elif isinstance(parentstates, list):
            cparentstates = (ctypes.c_int*len(parentstates))(*parentstates)
        
        elif isinstance(parentstates, str):
            statenames = parentstates.split(', ')
            parents = self.parents
            numparents = parents.length
            parent_states = []
            for i in range(numparents):
                nthparent = parents.get_nth_node(i)             
                state = nthparent.get_state_named(statenames[i])
                parent_states.append(state)  
            cparentstates = (ctypes.c_int*len(parent_states))(*parent_states)
        
        else:
            raise TypeError('A string, list, or None is required (got type {})'.format(type(parentstates).__name__))
            
        Netica.SetNodeFuncState_bn.restype = None
        Netica.SetNodeFuncState_bn(ctypes.c_void_p(self.cptr), cparentstates, 
                                   ctypes.c_int(state))
        err.checkerr() 
    
    def get_func_state(self, parentstates):
        """Return the table entry giving the state of this node for the row 'parentstates'. 
        
        For parentstates, pass an array of state indexes or a comma-delimited 
        string of state names in the same order as this node's parents.  
        For continuous nodes, normally use RealFuncTable instead.  
        Like Netica-C GetNodeFuncState_bn
        """
        if parentstates is None:
            cparentstates = parentstates
            
        elif isinstance(parentstates, list):
            cparentstates = (ctypes.c_int*len(parentstates))(*parentstates)
        
        elif isinstance(parentstates, str):
            statenames = parentstates.split(', ')
            parents = self.parents
            numparents = parents.length
            parent_states = []
            for i in range(numparents):
                nthparent = parents.get_nth_node(i)             
                state = nthparent.get_state_named(statenames[i])
                parent_states.append(state)  
            cparentstates = (ctypes.c_int*len(parent_states))(*parent_states)
        
        else:
            raise TypeError('A string, list, or None is required (got type {})'.format(type(parentstates).__name__))
        
        Netica.GetNodeFuncState_bn.restype = ctypes.c_int
        nodefuncstates = Netica.GetNodeFuncState_bn(ctypes.c_void_p(self.cptr), cparentstates)
        err.checkerr()
        return nodefuncstates
    
    def set_func_real(self, parentstates, val):
        """A table entry giving the real value of this node for the row 'parentstates' 
        
        For parentstates, pass an array of state indexes or a comma-delimited 
        string of state names in the same order as this node's parents.  
        Like Netica-C SetNodeFuncReal_bn.
        """   
        if parentstates is None:
            cparentstates = parentstates
            
        elif isinstance(parentstates, list):
            cparentstates = (ctypes.c_int*len(parentstates))(*parentstates)
        
        elif isinstance(parentstates, str):
            statenames = parentstates.split(', ')
            parents = self.parents
            numparents = parents.length
            parent_states = []
            for i in range(numparents):
                nthparent = parents.get_nth_node(i)             
                state = nthparent.get_state_named(statenames[i])
                parent_states.append(state)  
            cparentstates = (ctypes.c_int*len(parent_states))(*parent_states)
        
        else:
            raise TypeError('A string, list, or None is required (got type {})'.format(type(parentstates).__name__))
        
        Netica.SetNodeFuncReal_bn.restype = None
        Netica.SetNodeFuncReal_bn(ctypes.c_void_p(self.cptr), cparentstates, ctypes.c_double(val))
        err.checkerr()

    def get_func_real(self, parentstates):
        """A table entry giving the real value of this node for the row 'parentstates' 
        
        For parentstates, pass an array of state indexes or a comma-delimited 
        string of state names in the same order as this node's parents.  
        Like Netica-C GetNodeFuncReal_bn.
        """
        if parentstates is None:
            cparentstates = parentstates
            
        elif isinstance(parentstates, list):
            cparentstates = (ctypes.c_int*len(parentstates))(*parentstates)
        
        elif isinstance(parentstates, str):
            statenames = parentstates.split(', ')
            parents = self.parents
            numparents = parents.length
            parent_states = []
            for i in range(numparents):
                nthparent = parents.get_nth_node(i)             
                state = nthparent.get_state_named(statenames[i])
                parent_states.append(state)  
            cparentstates = (ctypes.c_int*len(parent_states))(*parent_states)
        
        else:
            raise TypeError('A string, list, or None is required (got type {})'.format(type(parentstates).__name__))
        Netica.GetNodeFuncState_bn.restype = ctypes.c_double
        nodefuncreal = Netica.GetNodeFuncState_bn(ctypes.c_void_p(self.cptr), cparentstates)
        err.checkerr()
        return nodefuncreal

    def set_table(self, which_table, parentstates, table):
        """***nodocs

        which_table must be one of "function_state", "function_real", "cpt", or "exper"
        Like Netica-C SetNodeTable_bn.
        """
        if parentstates is None:
            cparentstates = parentstates
            size_parentstates = 0
            
        elif isinstance(parentstates, list):
            cparentstates = (ctypes.c_int*len(parentstates))(*parentstates)
            size_parentstates = len(parentstates)
        
        elif isinstance(parentstates, str):
            statenames = parentstates.split(', ')
            parents = self.parents
            numparents = parents.length
            parent_states = []
            for i in range(numparents):
                nthparent = parents.get_nth_node(i)             
                state = nthparent.get_state_named(statenames[i])
                parent_states.append(state)  
            cparentstates = (ctypes.c_int*len(parent_states))(*parent_states)
            size_parentstates = len(parent_states)
        
        else:
            raise TypeError('A string, list, or None is required (got type {})'.format(type(parentstates).__name__))
            
        ctable = (ctypes.c_float*len(table))(*table)

        if table is None:
            size_table = 0
        else:    
            size_table = len(table)

        Netica.SetNodeTable_bn.restype = None
        Netica.SetNodeTable_bn(ctypes.c_void_p(self.cptr), ctypes.c_char_p(which_table.encode()), cparentstates,
                               ctypes.c_int(size_parentstates), ctable, ctypes.c_int(size_table))
        err.checkerr()
    
    def get_table(self, which_table, parentstates):
        """***nodocs

        which_table must be one of "function_state", "function_real", "cpt", or "exper"
        Like Netica-C GetNodeTable_bn.
        """
        if parentstates is None:
            cparentstates = parentstates
            size_parentstates = 0
            
        elif isinstance(parentstates, list):
            cparentstates = (ctypes.c_int*len(parentstates))(*parentstates)
            size_parentstates = len(parentstates)
        
        elif isinstance(parentstates, str):
            statenames = parentstates.split(', ')
            parents = self.parents
            numparents = parents.length
            parent_states = []
            for i in range(numparents):
                nthparent = parents.get_nth_node(i)             
                state = nthparent.get_state_named(statenames[i])
                parent_states.append(state)  
            cparentstates = (ctypes.c_int*len(parent_states))(*parent_states)
            size_parentstates = len(parent_states)
        
        else:
            raise TypeError('A string, list, or None is required (got type {})'.format(type(parentstates).__name__))
        
        csize_table = ctypes.c_int(0)

        Netica.GetNodeTable_bn.restype = ctypes.POINTER(ctypes.c_float)
        table_pointer = Netica.GetNodeTable_bn(ctypes.c_void_p(self.cptr), ctypes.c_char_p(which_table.encode()),
                                               cparentstates, ctypes.c_int(size_parentstates), None, ctypes.byref(csize_table))
        err.checkerr()
           
        size_table = csize_table.value

        if table_pointer:
            table = []
            for i in range(size_table):
                table.extend([table_pointer[i]]) 
        else:
            table = None
        return table
        
    def set_CPT(self, parentstates, probs):
        """Set probabilities for the states of this node given the passed parentstates.
        
        For parentstates, pass an array of state indexes or a comma-delimited 
        string of state names in the same order as this node's parents.    
        Like Netica-C SetNodeProbs_bn.
        """
        if parentstates is None:
            cparentstates = parentstates
            
        elif isinstance(parentstates, list):
            cparentstates = (ctypes.c_int*len(parentstates))(*parentstates)
        
        elif isinstance(parentstates, str):
            statenames = parentstates.split(', ')
            parents = self.parents
            numparents = parents.length
            parent_states = []
            for i in range(numparents):
                nthparent = parents.get_nth_node(i)             
                state = nthparent.get_state_named(statenames[i])
                parent_states.append(state)  
            cparentstates = (ctypes.c_int*len(parent_states))(*parent_states)
        
        else:
            raise TypeError('A string, list, or None is required (got type {})'.format(type(parentstates).__name__))
        
        cprobs = (ctypes.c_float*len(probs))(*probs)
        
        Netica.SetNodeProbs_bn.restype = None
        Netica.SetNodeProbs_bn(ctypes.c_void_p(self.cptr), cparentstates, cprobs)
        err.checkerr()

    def get_CPT(self, parentstates):
        """Get probabilities for the states of this node given the passed parentstates.
        
        For parentstates, pass an array of state indexes or a comma-delimited 
        string of state names in the same order as this node's parents.  
        Like Netica-C GetNodeProbs_bn.
        """
        if parentstates is None:
            cparentstates = parentstates
            
        elif isinstance(parentstates, list):
            cparentstates = (ctypes.c_int*len(parentstates))(*parentstates)
        
        elif isinstance(parentstates, str):
            statenames = parentstates.split(', ')
            parents = self.parents
            numparents = parents.length
            parent_states = []
            for i in range(numparents):
                nthparent = parents.get_nth_node(i)             
                state = nthparent.get_state_named(statenames[i])
                parent_states.append(state)  
            cparentstates = (ctypes.c_int*len(parent_states))(*parent_states)
        
        else:
            raise TypeError('A string, list, or None is required (got type {})'.format(type(parentstates).__name__))
        
        Netica.GetNodeProbs_bn.restype = ctypes.POINTER(ctypes.c_float)
        CPT_pointer = Netica.GetNodeProbs_bn(ctypes.c_void_p(self.cptr), cparentstates)
        err.checkerr()
           
        size = 1
        for n in range(self.parents.length):
            num_states = self.parents.get_nth_node(n).num_states
            # Include checks for if num_states is 0 or too large
            size *= num_states

        if CPT_pointer:
            CPT = []
            for i in range(size):
                CPT.extend([CPT_pointer[i]]) 
        else:
            CPT = None
        return CPT

    def set_experience(self, parentstates, experience):
        """Set the experience of this node as a function of its parent nodes.  
        
        For 'parent_states', pass a comma-delimited string listing them in the 
        same order as parents, or an array of their state indexes.
        Like Netica-C SetNodeExperience_bn.
        """
        if parentstates is None:
            cparentstates = parentstates
            
        elif isinstance(parentstates, list):
            cparentstates = (ctypes.c_int*len(parentstates))(*parentstates)
        
        elif isinstance(parentstates, str):
            statenames = parentstates.split(', ')
            parents = self.parents
            numparents = parents.length
            parent_states = []
            for i in range(numparents):
                nthparent = parents.get_nth_node(i)             
                state = nthparent.get_state_named(statenames[i])
                parent_states.append(state)  
            cparentstates = (ctypes.c_int*len(parent_states))(*parent_states)
        
        else:
            raise TypeError('A string, list, or None is required (got type {})'.format(type(parentstates).__name__))
        
        Netica.SetNodeExperience_bn.restype = None
        Netica.SetNodeExperience_bn(ctypes.c_void_p(self.cptr), cparentstates,
                                    ctypes.c_double(experience))
        err.checkerr()
    
    def get_experience(self, parentstates):
        """Return the "experience" of the node for the situation described by the parent states.
        
        For parentstates, pass an array of state indexes or a comma-delimited 
        string of state names in the same order as this node's parents.
        Like Netica-C GetNodeExperience_bn.
        """
        if parentstates is None:
            cparentstates = parentstates
            
        elif isinstance(parentstates, list):
            cparentstates = (ctypes.c_int*len(parentstates))(*parentstates)
        
        elif isinstance(parentstates, str):
            statenames = parentstates.split(', ')
            parents = self.parents
            numparents = parents.length
            parent_states = []
            for i in range(numparents):
                nthparent = parents.get_nth_node(i)             
                state = nthparent.get_state_named(statenames[i])
                parent_states.append(state)  
            cparentstates = (ctypes.c_int*len(parent_states))(*parent_states)
        
        else:
            raise TypeError('A string, list, or None is required (got type {})'.format(type(parentstates).__name__))
            
        Netica.GetNodeExperience_bn.restype = ctypes.c_double
        experience = Netica.GetNodeExperience_bn(ctypes.c_void_p(self.cptr), cparentstates)
        err.checkerr()
        return experience

    def add_to_nodeset(self, nodeset):
        """Add this node to the node-set named nodeset.
        
        Creates a new node-set if nodeset is not yet present in the net 
        containing node.
        Like Netica-C AddNodeToNodeset_bn.
        """
        Netica.AddNodeToNodeset_bn.restype = None
        Netica.AddNodeToNodeset_bn(ctypes.c_void_p(self.cptr), 
                                   ctypes.c_char_p(nodeset.encode()))
        err.checkerr()
    
    def remove_from_nodeset(self, nodeset):
        """Remove this node from the node-set named nodeset.
        
        Like Netica-C RemoveNodeFromNodeset_bn.
        """
        Netica.RemoveNodeFromNodeset_bn.restype = None
        Netica.RemoveNodeFromNodeset_bn(ctypes.c_void_p(self.cptr), 
                                        ctypes.c_char_p(nodeset.encode()))
        err.checkerr()

    def is_in_nodeset(self, nodeset):
        """Return whether this node is a member of nodeset.
        
        Like Netica-C IsNodeInNodeset_bn.
        """
        Netica.IsNodeInNodeset_bn.restype = ctypes.c_bool
        in_nodeset = Netica.IsNodeInNodeset_bn(ctypes.c_void_p(self.cptr),
                                               ctypes.c_char_p(nodeset.encode()))
        err.checkerr()
        return bool(in_nodeset)
