# -*- coding: utf-8 -*-
"""
Created on Mon Mar 16 10:29:45 2020

@author: Sophie
"""

import ctypes
import weakref

from neticapy import enums
from neticapy import environ as envrn
from neticapy import stream as strm
from neticapy import node as nd
from neticapy import nodelist as ndlst
from neticapy import smallclasses
from neticapy import neticaerror as err
from neticapy.loaddll import Netica

def _create_net(cptr):
    """Function called by NeticaPy to create a net.
    
    cptr is the C pointer generated by a call to a Netica DLL function. 
    create_net uses the pointer to check whether a NeticaPy instance of this 
    net already exists, and returns any existing net. 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 Net(('from_create_net', cptr))
    
    elif envrn.userdata_initialization:
        existing_net = Netica.GetNetUserData_bn(ctypes.c_void_p(cptr), 0)
        err.checkerr()
        if existing_net:
            py_net = ctypes.cast(existing_net, 
                                 ctypes.POINTER(ctypes.py_object)).contents.value
            return py_net
        else:
            return Net(('from_create_net', cptr))
    else:
        return Net(('from_create_net', cptr))
    

class Net:
    
    def __init__(self, name, file=None, options=None, environ=None): # Someday, may allow name to be None when file None
          
        self.cptr = None # Initialize cptr for case where Netica raises an error during construction

        self.python_owner = True

        if environ is None:
            environ_cptr = envrn.env
        elif isinstance(environ, envrn.Environ):
            environ_cptr = environ.cptr
        else:
            raise TypeError('An Environ is required (got type {})'.format(type(environ).__name__))
        
        if isinstance(name, tuple):  # Could use options instead of tuple for name, and pass None for name
            indicator, passed_cptr = name
            if indicator == 'from_create_net':
                cptr = passed_cptr
            else:
                raise TypeError('A string is required (got type {})'.format(type(name).__name__))
                       
        elif file: 
            options = enums.set_reading_option(options)

            if isinstance(file, strm.Stream):
                Netica.ReadNet_bn.restype = ctypes.c_void_p
                cptr = Netica.ReadNet_bn(ctypes.c_void_p(file.cptr), ctypes.c_int(options))
                err.checkerr()
            elif isinstance(file, str):
                stream = strm.Stream(file)
                Netica.ReadNet_bn.restype = ctypes.c_void_p
                cptr = Netica.ReadNet_bn(ctypes.c_void_p(stream.cptr), ctypes.c_int(options))
                err.checkerr()
            else:
                raise TypeError('A Steam or filename is required (got type {})'.format(type(file).__name__))
            if name:
                Netica.SetNetName_bn.restype = None
                Netica.SetNetName_bn(ctypes.c_void_p(cptr), ctypes.c_char_p(name.encode()))
                err.checkerr()
                       
        else:
            Netica.NewNet_bn.restype = ctypes.c_void_p
            cptr = Netica.NewNet_bn(ctypes.c_char_p(name.encode()), 
                                    ctypes.c_void_p(environ_cptr))
            err.checkerr()
    
        self.cptr = cptr
        
        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.SetNetUserData_bn.restype = None
            Netica.SetNetUserData_bn(ctypes.c_void_p(self.cptr), 0, cdata)
            err.checkerr()
        
    def __del__(self):
        """Remove this net, freeing all resources it consumes, including memory.
        
        Like Netica-C DeleteNet_bn.
        """
        if self.python_owner:
            if envrn.env is not None:
                Netica.DeleteNet_bn.restype = None
                Netica.DeleteNet_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

    def copy(self, new_name, new_env, options=None):
        """Returns a duplicate of this net, but renamed to 'new_name'.  
        
        Options allows you to control what gets copied. It can be any 
        combination of the following strings, separated by commas: "no_nodes", 
        "no_links", "no_tables", and "no_visual".  
        Like Netica-C CopyNet_bn.
        """
        
        if new_name is not None:
            new_name = ctypes.c_char_p(new_name.encode())
        
        if new_env is not None:
            if not isinstance(new_env, envrn.Environ):
                raise TypeError('An Environ is required (got type {})'.format(type(new_env).__name__))
            new_env = ctypes.c_void_p(new_env.cptr)
        
        if options is None:
            options = ''
            
        Netica.CopyNet_bn.restype = ctypes.c_void_p
        cptr = Netica.CopyNet_bn(ctypes.c_void_p(self.cptr), new_name, 
                                 new_env, ctypes.c_char_p(options.encode()))
        err.checkerr()
        return _create_net(cptr)  
      
    def new_node(self, name, states, kind="NATURE"):   # Someday, may allow name to be None 
        """Add a new node to this net.  
        
        The node will start off as a nature node (Node.kind = "NATURE"),  
        but it may be changed by calling Node.kind.        
        Like Netica-C NewNode_bn.
        """

        if isinstance(states, str):
            num_states = len(states.split(","))
        else:
            num_states = states

        if name is not None:
            name = ctypes.c_char_p(name.encode())

        Netica.NewNode_bn.restype = ctypes.c_void_p
        cptr = Netica.NewNode_bn(name, num_states, ctypes.c_void_p(self.cptr))
        err.checkerr()

        node = nd._create_node(cptr)

        if isinstance(states, str):
            Netica.SetNodeStateNames_bn.restype = None
            Netica.SetNodeStateNames_bn(ctypes.c_void_p(node.cptr), 
                                        ctypes.c_char_p(states.encode()))
            err.checkerr()

        if kind != "NATURE":
            Netica.SetNodeKind_bn.restype = None
            Netica.SetNodeKind_bn(ctypes.c_void_p(node.cptr), 
                                  ctypes.c_int(enums.set_node_kind(kind)))
            err.checkerr()
        
        return node 
        
    def copy_nodes(self, nodes, options=None):
        """Duplicate the nodes (and links between them) in the list passed, 
        and return a list of the new nodes.  
        
        The originals may be from a different net.
        options can be any combination of the following strings, 
        separated by commas: "no_links", "no_tables".
        Like Netica-C CopyNodes_bn.
        """
        
        if options is not None:
            options = ctypes.c_char_p(options.encode())    
        
        if not isinstance(nodes, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(nodes).__name__))      
        
        Netica.CopyNodes_bn.restype = ctypes.c_void_p
        cptr = Netica.CopyNodes_bn(ctypes.c_void_p(nodes.cptr), 
                                   ctypes.c_void_p(self.cptr), options)
        err.checkerr()
        
        return ndlst._create_nodelist(cptr)

    @property
    def file_name(self):
        """Return the file name, including directory, that this net was last 
        read from or written to.  
        
        Like Netica-C GetNetFileName_bn.
        """
        Netica.GetNetFileName_bn.restype = ctypes.c_char_p
        file_name = Netica.GetNetFileName_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return file_name.decode()     

    @property
    def name(self):
        """Name of this net (restricted to 30 character alphanumeric).  
        
        See also 'Title' property.  
        Like Netica-C G/SetNetName_bn.
        """
        Netica.GetNetName_bn.restype = ctypes.c_char_p
        name = Netica.GetNetName_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return name.decode()

    @name.setter
    def name(self, name):
        """Name of this net (restricted to 30 character alphanumeric).  
        
        See also 'Title' property.
        Like Netica-C G/SetNetName_bn.
        """
        Netica.SetNetName_bn.restype = None
        Netica.SetNetName_bn(ctypes.c_void_p(self.cptr), ctypes.c_char_p(name.encode()))
        err.checkerr()
    
    @property
    def title(self):
        """Unrestricted label for this net.  
        
        See also 'Name' property.  
        Like Netica-C G/SetNetTitle_bn.
        """
        Netica.GetNetTitle_bn.restype = ctypes.c_char_p
        title = Netica.GetNetTitle_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return title.decode()

    @title.setter
    def title(self, title):
        """Unrestricted label for this net.  
        
        See also 'Name' property.  
        Like Netica-C G/SetNetTitle_bn.
        """
        Netica.SetNetTitle_bn.restype = None
        Netica.SetNetTitle_bn(ctypes.c_void_p(self.cptr), ctypes.c_char_p(title.encode()))
        err.checkerr()    
    
    @property
    def comment(self):
        """Documentation or description of this net.
        
        Like Netica-C G/SetNetComment_bn.
        """
        Netica.GetNetComment_bn.restype = ctypes.c_char_p
        comment = Netica.GetNetComment_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return comment.decode()

    @comment.setter
    def comment(self, comment):
        """Documentation or description of this net.
        
        Like Netica-C G/SetNetComment_bn.
        """
        Netica.SetNetComment_bn.restype = None
        Netica.SetNetComment_bn(ctypes.c_void_p(self.cptr), ctypes.c_char_p(comment.encode()))
        err.checkerr()
  
    @property
    def elimination_order(self):
        """Ordered list of all the nodes in this net (except constant and utility 
        nodes), used to guide compiling to find an efficient junction tree.
        
        Getter creates a constant NodeList, i.e. one that cannot be 
        modified or deleted.  To make modifications, duplicate this Nodelist, 
        using NodeList.copy.  Funtion returns None if there is no order 
        currently associated with the net
        Like Netica-C G/SetNetElimOrder_bn.
        """
        Netica.GetNetElimOrder_bn.restype = ctypes.c_void_p
        cptr = Netica.GetNetElimOrder_bn(ctypes.c_void_p(self.cptr)) 
        if cptr:
            return ndlst._create_nodelist(cptr, is_const=True)
        else:
            return None

    @elimination_order.setter
    def elimination_order(self, elim_order):
        """Ordered list of all the nodes in this net (except constant and utility 
        nodes), used to guide compiling to find an efficient junction tree.
        
        Getter creates a constant NodeList, i.e. one that cannot be 
        modified or deleted.  To make modifications, duplicate this Nodelist, 
        using NodeList.copy.  Funtion returns None if there is no order 
        currently associated with the net
        Like Netica-C G/SetNetElimOrder_bn.
        """
        Netica.SetNetElimOrder_bn.restype = ctypes.c_int
        Netica.SetNetElimOrder_bn(ctypes.c_void_p(self.cptr), ctypes.c_void_p(elim_order.cptr))
        err.checkerr()
         
    @property
    def autoupdate(self):
        """Turn auto-update on (1) or off (0).
        
        May be computationally heavy if an auto update is needed.
        If =1 then beliefs are automatically recalculated whenever a finding 
        is entered into this net, if =0 they aren't.  
        Like Netica-C G/SetNetAutoUpdate_bn. 
        """
        Netica.GetNetAutoUpdate_bn.restype = ctypes.c_int
        autoupdate_state = Netica.GetNetAutoUpdate_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return autoupdate_state  
    
    @autoupdate.setter
    def autoupdate(self, auto_update):           
        """Turn auto-update on (1) or off (0).
        
        May be computationally heavy if an auto update is needed.
        If =1 then beliefs are automatically recalculated whenever a finding 
        is entered into this net, if =0 they aren't.  
        Like Netica-C G/SetNetAutoUpdate_bn.
        """
        Netica.SetNetAutoUpdate_bn.restype = ctypes.c_int
        Netica.SetNetAutoUpdate_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(auto_update))
        err.checkerr()
        
    def set_user_field(self, name, data, kind=0):
        """Attach user defined data to this net under category 'field_name'.  
        
        Like Netica-C SetNetUserField_bn.
        """
        if isinstance(data, str):
            data = data.encode()
        if isinstance(data, int):
            data = str(data).encode()
        length = len(data)
        Netica.SetNetUserField_bn.restype = None
        Netica.SetNetUserField_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 GetNetUserField_bn.
        """
        clength = ctypes.c_int(0)
        Netica.GetNetUserField_bn.restype = ctypes.POINTER(ctypes.c_ubyte)
        data_pointer = Netica.GetNetUserField_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 GetNetNthUserField_bn.
        """
        clength = ctypes.c_int(0)
        cname = ctypes.c_char_p(b'')
        cvalue = ctypes.pointer(ctypes.c_ubyte(0))
        Netica.GetNetNthUserField_bn.restype = None
        Netica.GetNetNthUserField_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

    @property
    def nodes(self):
        """Return a list consisting of all the nodes in this net.  
        
        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 GetNetNodes2_bn.
        """
        Netica.GetNetNodes2_bn.restype = ctypes.c_void_p
        cptr = Netica.GetNetNodes2_bn(ctypes.c_void_p(self.cptr), None)
        err.checkerr()
  
        return ndlst._create_nodelist(cptr, is_const=True)
    
    def get_nodes(self, options=None):
        """Return a list consisting of all the nodes in this net.  
        
        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 GetNetNodes2_bn.
        """
        if options is not None:
            options = ctypes.c_char_p(options.encode())
        Netica.GetNetNodes2_bn.restype = ctypes.c_void_p
        cptr = Netica.GetNetNodes2_bn(ctypes.c_void_p(self.cptr), options)
        err.checkerr()
        
        return ndlst._create_nodelist(cptr, is_const=True)

    def get_related_nodes(self, related_nodes, relation, nodes):
        """Put in 'related_nodes' those nodes that bear 'relation' to any of 
        the nodes in 'nodes'.  
        
        See also Node.get_related_nodes.  
        Like Netica-C GetRelatedNodesMult_bn.
        """
        if not isinstance(nodes, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(nodes).__name__))
        if not isinstance(related_nodes, ndlst.NodeList):
            raise TypeError('A Nodelist is required (got type {})'.format(type(related_nodes).__name__))
        
        Netica.GetRelatedNodesMult_bn.restype = None
        Netica.GetRelatedNodesMult_bn(ctypes.c_void_p(related_nodes.cptr), 
                                      ctypes.c_char_p(relation.encode()), 
                                      ctypes.c_void_p(nodes.cptr))
        err.checkerr()
   
    def write(self, file):
        """"Write this net to the indicated file (which may include directory path).  
        
        Like Netica-C WriteNet_bn.
        """
        if isinstance(file, strm.Stream):
            Netica.WriteNet_bn.restype = None
            Netica.WriteNet_bn(ctypes.c_void_p(self.cptr), ctypes.c_void_p(file.cptr))
            err.checkerr()    
        elif isinstance(file, str):
            stream = strm.Stream(file)
            Netica.WriteNet_bn.restype = None
            Netica.WriteNet_bn(ctypes.c_void_p(self.cptr), ctypes.c_void_p(stream.cptr))
            err.checkerr()    
        else:
            raise TypeError('A Steam or filename is required (got type {})'.format(type(file).__name__))
        
    def write_findings(self, nodes, file, ID_num, freq):
        """Write a case from the node list to the given stream.
        
        Also writes the case's ID number and frequency, if those parameters are 
        passed.  
        Like Netica-C WriteNetFindings_bn.
        """
        if nodes is not None:
            if not isinstance(nodes, ndlst.NodeList):
                raise TypeError('A NodeList is required (got type {})'.format(type(nodes).__name__))
            nodes = ctypes.c_void_p(nodes.cptr)
            
        if isinstance(file, strm.Stream):
            file = file
        elif isinstance(file, str):
            file = strm.Stream(file)
        else:
            raise TypeError('A Steam or filename is required (got type {})'.format(type(file).__name__))
            
        Netica.WriteNetFindings_bn.restype = ctypes.c_longlong
        Netica.WriteNetFindings_bn(nodes, ctypes.c_void_p(file.cptr), 
                                   ctypes.c_longlong(ID_num), ctypes.c_double(freq))
        err.checkerr()
    
    def read_findings(self, case_posn, stream, add, nodes, ID_num, freq):
        """Read a case from the given stream into the node list.
        
        Also reads and returns the case's ID number and frequency, if that is 
        present.  
        Like Netica-C ReadNetFindings2_bn.
        """
        if case_posn is not None:
            c_case_posn = ctypes.c_longlong(enums.set_case_position(case_posn))
            case_posn_ref = ctypes.pointer(c_case_posn)
        else:
            case_posn_ref = case_posn
        
        if ID_num is not None:
            c_ID_num = ctypes.c_longlong(ID_num)
            ID_num_ref = ctypes.pointer(c_ID_num)
        else:
            ID_num_ref = ID_num
        
        if freq is not None:
            c_freq = ctypes.c_double(freq)
            freq_ref = ctypes.pointer(c_freq)
        else:
            freq_ref = freq
        
        if not isinstance(stream, strm.Stream):
            raise TypeError('A Stream is required (got type {})'.format(type(stream).__name__))
        if not isinstance(nodes, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(nodes).__name__))
        
        Netica.ReadNetFindings2_bn.restype = None
        Netica.ReadNetFindings2_bn(case_posn_ref, ctypes.c_void_p(stream.cptr), 
                                   ctypes.c_ubyte(add), ctypes.c_void_p(nodes.cptr),
                                   ID_num_ref, freq_ref)
        err.checkerr()
        
        if case_posn is not None:
            case_posn = c_case_posn.value 
            if case_posn == -13:
                case_posn = "NO_MORE_CASES"
        if ID_num is not None:
            ID_num = c_ID_num.value
        if freq is not None:
            freq = c_freq.value
        
        return case_posn, ID_num, freq
    
    def map_state_list(self, src_states, src_nodes, dest_states, dest_nodes):
        """***nodocs
        """
        if not isinstance(src_nodes, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(src_nodes).__name__))
        if not isinstance(dest_nodes, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(dest_nodes).__name__))
        src_length = len(src_states)
        csrc_states = (ctypes.c_int*src_length)(*src_states)
        dest_length = len(dest_states)
        cdest_states = (ctypes.c_int*dest_length)(*dest_states)
        
        Netica.MapStateList2_bn.restype = None
        Netica.MapStateList2_bn(csrc_states, ctypes.c_int(src_length), 
                                ctypes.c_void_p(src_nodes.cptr), cdest_states,
                                ctypes.c_int(dest_length), ctypes.c_void_p(dest_nodes))
        err.checkerr()

    
    def revise_CPTs_by_finding(self, nodes, degree, updating=0):
        """Revise the CPTs of these nodes to account for the currently entered 
        case, i.e. findings. 
        
        Like Netica-C ReviseCPTsByFindings_bn.
        """
        if not isinstance(nodes, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(nodes).__name__))
        Netica.ReviseCPTsByFindings_bn.restype = None
        Netica.ReviseCPTsByFindings_bn(ctypes.c_void_p(nodes.cptr), ctypes.c_int(updating),
                                       ctypes.c_double(degree))
        err.checkerr()

    def revise_CPTs_by_case_file(self, file, nodes, degree, updating=0):
        """Revise the CPTs of these nodes, to account for the cases in the given file.
        
        Like Netica-C ReviseCPTsByCaseFile_bn.
        """
        if isinstance(file, strm.Stream):
            file = file
        elif isinstance(file, str):
            file = strm.Stream(file)
        else:
            raise TypeError('A Steam or filename is required (got type {})'.format(type(file).__name__))
        if not isinstance(nodes, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(nodes).__name__))
        
        Netica.ReviseCPTsByCaseFile_bn.restype = None
        Netica.ReviseCPTsByCaseFile_bn(ctypes.c_void_p(file.cptr), ctypes.c_void_p(nodes.cptr), 
                                       ctypes.c_int(updating), ctypes.c_double(degree))
        err.checkerr()

    def absorb_nodes(self, nodes):
        """Absorb nodea, so that they are removed from this net, but the net's 
        overall joint probabilities for the remaining nodes is unchanged.  
        
        nodes should be a modifyable NodeList.
        Like Netica-C AbsorbNodes2_bn.
        """
        if not isinstance(nodes, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(nodes).__name__))
            
        Netica.AbsorbNodes2_bn.restype = None
        Netica.AbsorbNodes2_bn(ctypes.c_void_p(nodes.cptr))
        err.checkerr()   
        
#        No longer neccessary with newest version of API 
#        
#        Create a new empty nodelist to replace the damaged one
#        Netica.NewNodeList2_bn.restype = ctypes.c_void_p
#        new_cptr = Netica.NewNodeList2_bn(ctypes.c_int(0), 
#                                          ctypes.c_void_p(self.cptr))
#        err.checkerr()
#        if dict_initialization:
#            weakref = cptr_dict[nodes.cptr]
#            del cptr_dict[nodes.cptr]
#            cptr_dict[new_cptr] = weakref
#        nodes.cptr = new_cptr

    def retract_findings(self):
        """Retract all findings entered in this net (from all nodes except 'constant' nodes).
        
        Like Netica-C RetractNetFindings_bn.
        """
        Netica.RetractNetFindings_bn.restype = None
        Netica.RetractNetFindings_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()

    def compile_net(self):                      
        """Compile this net for fast inference.
        
        Like Netica-C CompileNet_bn.
        """
        Netica.CompileNet_bn.restype = None
        Netica.CompileNet_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()

    def uncompile(self):
        """Reverse the effect of the Compile function.  
        
        Used just to reduce memory consumption of a net, by releasing the 
        junction tree, etc.  
        Like Netica-C UncompileNet_bn.
        """
        Netica.UncompileNet_bn.restype = None
        Netica.UncompileNet_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()

    def get_size_compiled(self, method=0):
        """Return the number of bytes required for the internal structures, 
        including junction tree, of a compiled net.  
        
        Maximum inference time is linearly related to this quantity.  
        Like Netica-C SizeCompiledNet_bn.
        """
        Netica.SizeCompiledNet_bn.restype = ctypes.c_double
        size = Netica.SizeCompiledNet_bn(ctypes.c_void_p(self.cptr), 
                                         ctypes.c_int(method))
        err.checkerr()
        return size

    def findings_probability(self):                     # May want to make this a property
        """Return the joint probability of all the findings entered so far. 
        
        See also get_joint_probability.
        Like Netica-C FindingsProbability_bn.
        """
        Netica.FindingsProbability_bn.restype = ctypes.c_double
        prob = Netica.FindingsProbability_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return prob    

    def get_expected_utility(self):
        """Return the overall expected utility for all the utility nodes.
        
        Assumes an optimal strategy is followed, given any findings entered.  
        Like Netica-C GetNetExpectedUtility_bn.
        """
        Netica.GetNetExpectedUtility_bn.retype = ctypes.c_float
        utility = Netica.GetNetExpectedUtility_bn(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return utility

    def get_joint_probability(self, nodes, states):
        """Return the joint probability that these nodes are in the passed 
        node states, given the findings currently entered in the net.  
        
        states must have one entry for each node in the BNodes, and in the same 
        order.  
        Like Netica-C GetJointProbability_bn.
        """
        if not isinstance(nodes, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(nodes).__name__))
        length = len(states)
        cstates = (ctypes.c_int*length)(*states)
        Netica.GetJointProbability_bn.restype = ctypes.c_double
        joint_prob = Netica.GetJointProbability_bn(ctypes.c_void_p(nodes.cptr), 
                                                   cstates, ctypes.c_int(length)) 
        err.checkerr()
        return joint_prob

    def get_most_probable_config(self, nodes, nth=0):
        """Return an array of states, one for each node, that indicates the most 
        probable configuration.  
        
        Currently only works if 'nodes' was created with the NodeList returned
        from Net.nodes. 
        Like Netica-C MostProbableConfig2_bn.
        """
        if not isinstance(nodes, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(nodes).__name__))
        length = nodes.length
        config = [0] * length
        cconfig = (ctypes.c_int*length)(*config) 
        clength = ctypes.c_int(length)
        Netica.MostProbableConfig2_bn.restype = None
        Netica.MostProbableConfig2_bn(ctypes.c_void_p(nodes.cptr), ctypes.byref(cconfig), 
                                      ctypes.byref(clength), ctypes.c_int(nth))
        err.checkerr()
        
        ret_length = clength.value
        for i in range(ret_length):
            config[i] = cconfig[i]
        
        return config

    def generate_random_case(self, nodes, method, num, rand):    
        """Generate a random case by simulation.
        
        Enters findings into the passed node list.  
        Like Netica-C GenerateRandomCase_bn.
        """
        if not isinstance(nodes, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(nodes).__name__))
        if num is None:
            num = 0
        if rand is not None:
            if not isinstance(rand, smallclasses.RandomGenerator):
                raise TypeError('A RandomGenerator is required (got type {})'.format(type(rand).__name__))
            rand = ctypes.c_void_p(rand.cptr)
        Netica.GenerateRandomCase_bn.restype = ctypes.c_int
        res = Netica.GenerateRandomCase_bn(ctypes.c_void_p(nodes.cptr), 
                                           ctypes.c_int(enums.set_sampling_method(method)), 
                                           ctypes.c_double(num), rand)
        err.checkerr()
        return res

    def get_all_nodesets(self, include_system, vis=None):
        """Return a comma-separated list of all node-set names.  
        
        Like Netica-C GetAllNodesets_bn.
        """
        if vis is not None:
            vis = ctypes.c_char_p(vis.encode())
        Netica.GetAllNodesets_bn.restype = ctypes.c_char_p
        nodesets = Netica.GetAllNodesets_bn(ctypes.c_void_p(self.cptr), 
                                            ctypes.c_bool(include_system), vis)
        err.checkerr()
        return nodesets.decode()

    def get_nodeset_color(self, nodeset, vis=None):
        """Get the color of the given node-set as an integer.
        
        Represented as 8 bits red, 8 bits green, 8 bits blue.  To convert the 
        returned integer to RGB values, one can use:
            Blue =  RGBint & 255
            Green = (RGBint >> 8) & 255
            Red =   (RGBint >> 16) & 255
        Like Netica-C SetNodesetColor_bn.
        """
        if vis is not None:
            vis = ctypes.c_char_p(vis.encode())
        Netica.SetNodesetColor_bn.restype = ctypes.c_int
        color = Netica.SetNodesetColor_bn(ctypes.c_char_p(nodeset.encode()), 
                                          ctypes.c_int(enums.QUERY_ns),
                                          ctypes.c_void_p(self.cptr), vis)
        err.checkerr()
        return color
    
    def set_nodeset_color(self, nodeset, color, vis=None):
        """Set the color of the given node-set as an integer.
        
        Represented as 8 bits red, 8 bits green, 8 bits blue.  To convert to an 
        integer, use the formula n = r256^2 + g256 + b.
        Like Netica-C SetNodesetColor_bn.
        """
        if vis is not None:
            vis = ctypes.c_char_p(vis.encode())
        Netica.SetNodesetColor_bn.restype = ctypes.c_int
        Netica.SetNodesetColor_bn(ctypes.c_char_p(nodeset.encode()), ctypes.c_int(color),
                                  ctypes.c_void_p(self.cptr), vis)
        err.checkerr()

    def reorder_nodesets(self, nodeset_order, vis=None):
        """Make the node-sets in the comma-delimited list passed the highest priority ones.
        
        The first in the list is the very highest priority.  To restore the old 
        setting, pass it the list returned by GetAllNodesets.  
        Like Netica-C ReorderNodesets_bn.
        """
        if vis is not None:
            vis = ctypes.c_char_p(vis.encode())
        Netica.ReorderNodesets_bn.restype = None
        Netica.ReorderNodesets_bn(ctypes.c_void_p(self.cptr), 
                                  ctypes.c_char_p(nodeset_order.encode()), vis)
        err.checkerr()

    def set_num_undos_kept(self, count_limit, memory_limit, options=None):
        """Turn on the ability to undo operations, and set how many to save in memory.  
        
        Like Netica-C SetNetNumUndos_bn.
        """
        if count_limit is None:
            count_limit = -1
        if memory_limit is None:
            memory_limit = -1
        if options is not None:
            options = ctypes.c_char_p(options.encode())
        Netica.SetNetNumUndos_bn.restype = None
        Netica.SetNetNumUndos_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(count_limit),
                                 ctypes.c_double(memory_limit), options)

    def undo_last_operation(self, to_when=-1):
        """Undoes the last operation done on this net, whether it was done by program or GUI user.  
        
        Call this repeatedly to undo earlier operations.  Pass -1 for to_when.  
        See also RedoOperation().  
        Like Netica-C UndoNetLastOper_bn.
        """
        Netica.UndoNetLastOper_bn.restype = ctypes.c_int
        succeeded = Netica.UndoNetLastOper_bn(ctypes.c_void_p(self.cptr),
                                              ctypes.c_longlong(to_when))
        err.checkerr()
        return succeeded

    def redo_operation(self, to_when=-1):
        """Re-does an operation that was undone with UndoLastOperation(), if there is one.  
        
        May be called repeatedly to redo several operations.  Pass -1 for 
        to_when.  
        Like Netica-C RedoNetOper_bn.
        """
        Netica.RedoNetOper_bn.restype = ctypes.c_int
        succeeded = Netica.RedoNetOper_bn(ctypes.c_void_p(self.cptr),
                                          ctypes.c_longlong(to_when))
        err.checkerr()
        return succeeded

    def create_custom_report(self, sel_nodes, templat, options=None):
        """Create a text or HTML report on nodes or the overall net, based on 
        the template passed in.  
        
        Like Netica-C CreateCustomReport_bn.
        """
        if sel_nodes is not None:
            if not isinstance(sel_nodes, ndlst.NodeList):
                raise TypeError('A NodeList is required (got type {})'.format(type(sel_nodes).__name__))
            sel_nodes = ctypes.c_void_p(sel_nodes.cptr)
        if options is not None:
            options = ctypes.c_char_p(options.encode())
            
        Netica.CreateCustomReport_bn.restype = ctypes.c_char_p
        report = Netica.CreateCustomReport_bn(ctypes.c_void_p(self.cptr), sel_nodes, 
                                              ctypes.c_char_p(templat.encode()), options)
        err.checkerr()
        return report.decode()
            
    def control_caching(self, command, value, nodes):
        # ***nodocs - low priority, don't document
        if nodes is not None:
            if not isinstance(nodes, ndlst.NodeList):
                raise TypeError('A NodeList is required (got type {})'.format(type(nodes).__name__))
            nodes = ctypes.c_void_p(nodes.cptr)
        Netica.ControlNetCaching_bn.restype = ctypes.c_char_p
        res = Netica.ControlNetCaching_bn(ctypes.c_void_p(self.cptr), ctypes.c_char_p(command.encode()), 
                                          ctypes.c_char_p(value.encode()), nodes)
        err.checkerr()
        return res

    def expand_time_series(self, result_time, burn_time, dimn=0, options=None):
        """Create a time-expanded version of this dynamic Bayes net, showing 
        time slices for the given time range.  
        
        Like Netica-C ExpandNet_bn.
        """
        # Currently untested - not in wide use
        if options is not None:
            options = ctypes.c_char_p(options.encode())
        Netica.ExpandNet_bn.restype = ctypes.c_void_p
        cptr = Netica.ExpandNet_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(dimn),
                            ctypes.c_double(result_time), ctypes.c_double(burn_time),
                            options)
        err.checkerr()
        
        return _create_net(cptr)

    def get_node_at_time(self, name, time):
        """Returns the node of this expanded dynamic Bayes net (DBN) whose name 
        starts with 'name' and whose value is valid at the point in time 'time' 
        (which is an array of length one consisting of a single number, the time).  
        
        Like Netica-C GetNodeAtTime_bn.
        """
        # Currently untested - not in wide use
        Netica.GetNodeAtTime_bn.restype = ctypes.c_void_p
        cptr = Netica.GetNodeAtTime_bn(ctypes.c_void_p(self.cptr), 
                                       ctypes.c_char_p(name.encode()), 
                                       ctypes.c_double(time))
        err.checkerr()
        
        return nd._create_node(cptr)  
    
    def set_random_gen(self, rand, is_private):
        """***nodocs
        """
        if not isinstance(rand, smallclasses.RandomGenerator):
            raise TypeError('A RandomGenerator is required (got type {})'.format(type(rand).__name__))
        Netica.SetNetRandomGen_bn.restype = None
        Netica.SetNetRandomGen_bn(ctypes.c_void_p(self.cptr), ctypes.c_void_p(rand.cptr),
                                  ctypes.c_bool(is_private))
        err.checkerr()

    def get_node_named(self, name):               # May want to rename to get_node 
        """Return a node from this net by name.  
        
        Like Netica-C GetNodeNamed_bn. 
        """
        Netica.GetNodeNamed_bn.restype = ctypes.c_void_p
        cptr = Netica.GetNodeNamed_bn(ctypes.c_char_p(name.encode()), 
                                      ctypes.c_void_p(self.cptr))
        err.checkerr()
        
        if cptr:
            return nd._create_node(cptr)            
        else:
            return None

    def list_to_nodelist(self, list_of_nodes):
        """Convert a Python List of nodes into a NodeList with the same ordering.
        
        Can pass in a list of: nodes, node names, or some combination of the two.
        """
        # ***** In progress
        # # should this direct to the factory methods?
        # length = len(list_of_nodes)

        # #(nodelist_bn*) SetNodelistNodes_bn (nodelist_bn* nodelist, node_bn* const* nodes, int length, environ_ns* env);
        # #clist_of_nodes = (ctypes.c_void_p*len(list_of_nodes))(ctypes.c_void_p(node.cptr) for node in list_of_nodes)
        # node_pointers = [] 
        # for node in list_of_nodes:
        #     # node_pointers.append(ctypes.c_void_p(node.cptr))
        #     node_pointers.append(node.cptr)
        # print("node_pointers", node_pointers)
        # cnode_pointers = (ctypes.c_void_p*length)(*node_pointers)
        # #clist_of_nodes = (ctypes.c_void_p*length)(*node_pointers)
        # #ctypes.cast(node_pointers, ctypes.POINTER) 
        # #print("clist_of_nodes",clist_of_nodes)
        

        # Netica.SetNodeListNodes_bn.restype = ctypes.c_void_p
        # cptr = Netica.SetNodeListNodes_bn(None, cnode_pointers, ctypes.c_int(length), envrn.env)
        # err.checkerr()
        # ndlst._create_nodelist(cptr)

        #IMPORT (nodelist_bn*) SetNodelistNodes_bn (nodelist_bn* nodelist, node_bn* const* nodes, int length, environ_ns* env);

        # Older version, before SetNodelistNodes_bn implemented
        # What if list is of nodes?
        nodelist = ndlst.NodeList(len(list_of_nodes), self)
        for idx, val in enumerate(list_of_nodes):           
            if isinstance(val, str):
                val = self.get_node_named(val)            
            nodelist.set_nth_node(val, idx)        
        return nodelist

    def add_nodes_from_caseset(self, cases, column_names=None, state_order=None, nodes_added=None, options=None):
        """Add one node for each column in cases.

        Goes through entire casefile to add all possible states the nodes could have. 
        If you wish to limit the number of nodes that get added, pass a comma 
        separated string for column_names. 
        Like Netica-C AddNodesFromCaseset_bn.
        """
        if column_names is not None:
            column_names = ctypes.c_char_p(column_names.encode())
        if state_order is not None:
            state_order = ctypes.c_void_p(state_order.cptr)
        if nodes_added is not None:
            nodes_added = ctypes.c_void_p(nodes_added)
        if options is not None:
            options = ctypes.c_char_p(options.encode())

        Netica.AddNodesFromCaseset_bn.restype = None
        Netica.AddNodesFromCaseset_bn(ctypes.c_void_p(self.cptr), ctypes.c_void_p(cases.cptr), 
                                      column_names, state_order, nodes_added, options)
        err.checkerr()
        
    def add_nodes_from_database(self, dbmgr, column_names, tables, condition, options=None):
        """***nodocs
        
        Like Netica-C AddNodesFromDB_bn.
        """
        if column_names is not None:
            column_names = ctypes.c_char_p(column_names.encode())
        if tables is not None:
            tables = ctypes.c_char_p(tables.encode())
        if condition is not None:
            condition = ctypes.c_char_p(condition.encode())
        if options is not None:
            options = ctypes.c_char_p(options.encode())
        
        if not isinstance(dbmgr, smallclasses.DatabaseManager):
            raise TypeError('A DatabaseManager is required (got type {})'.format(type(dbmgr).__name__))
            
        Netica.AddNodesFromDB_bn.restype = None
        Netica.AddNodesFromDB_bn(ctypes.c_void_p(dbmgr.cptr), ctypes.c_void_p(self.cptr),
                                 column_names, tables, condition, options)
        err.checkerr()
      
    def learn_tan_structure(self, nodes, target, cases, report=None, options=None):
        """Find the optimal tree augmented naive bayes (TAN) link structure.
        
        Add links between nodes in order to best predict or classify the target 
        node for data coming from the distributions of cases. Find the optimal tree
        augmented naive bayes (TAN) link structure to classify or predict the 
        target variable given information on the varieables in nodes. report and 
        options are for future expansion.
        Like Netica-C LearnTanStructure_bn.
        """
        if report is not None:
            report = ctypes.c_void_p(report.cptr)
        if options is not None:
            options = ctypes.c_char_p(options.encode())
        if not isinstance(nodes, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(nodes).__name__))
        if not isinstance(target, nd.Node):
            raise TypeError('A Node is required (got type {})'.format(type(target).__name__))
        if not isinstance(cases, smallclasses.Caseset):
            raise TypeError('A Caseset is required (got type {})'.format(type(cases).__name__))
        
        Netica.LearnTanStructure_bn.restype = None
        Netica.LearnTanStructure_bn(ctypes.c_void_p(nodes.cptr), ctypes.c_void_p(target.cptr), 
                                    ctypes.c_void_p(cases.cptr), report, options)
        err.checkerr()

    def create_diagram(self, nodes, dest_file, vis=None, options=None):
        """***nodocs
        """
        if vis is not None:
            vis = ctypes.c_char_p(vis.encode())
        if options is not None:
            options = ctypes.c_char_p(options.encode())
        if not isinstance(nodes, ndlst.NodeList):
            raise TypeError('A NodeList is required (got type {})'.format(type(nodes).__name__))
        if not isinstance(dest_file, strm.Stream):
            raise TypeError('A Stream is required (got type {})'.format(type(dest_file).__name__))
        Netica.CreateDiagram_bn.restype = None
        Netica.CreateDiagram_bn(ctypes.c_void_p(self.cptr), ctypes.c_void_p(nodes.cptr), vis, 
                                ctypes.c_void_p(dest_file.cptr), options)
        err.checkerr()
            