# -*- coding: utf-8 -*-
"""
Created on Fri Aug  4 13:42:31 2017

@author: Sophie

"""

import ctypes
import os
from enum import Enum
from enum import Flag

Netica = ctypes.WinDLL(os.path.abspath('../bin/Netica.dll'))

# Initialize classes for C pointers

class environ_ns(ctypes.Structure):
    pass

class report_ns(ctypes.Structure):
    pass
            
class stream_ns(ctypes.Structure):
    pass

class net_bn(ctypes.Structure):
    pass

class node_bn(ctypes.Structure):
    pass

class nodelist_bn(ctypes.Structure):
    pass

class caseset_cs(ctypes.Structure):
    pass

class learner_bn(ctypes.Structure):
    pass

class dbmgr_cs(ctypes.Structure):
    pass

class tester_bn(ctypes.Structure):
    pass

class randgen_ns(ctypes.Structure):
    pass

# Enumeration

class ErrorSeverity(Enum):
    NOTHING = 1
    REPORT = 2
    NOTICE = 3
    WARNING = 4
    ERROR = 5
    XXX = 6

class NodeType(Enum):
    CONTINUOUS = 1
    DISCRETE = 2
    TEXT = 3

class NodeKind(Enum):
    NATURE = 1
    CONSTANT = 2
    DECISION = 3
    UTILITY = 4
    DISCONNECTED = 5
    ADVERSARY = 6

class ReadingOption(Flag):
    NO_VISUAL_INFO = 0
    NO_WINDOW = 0x10
    MINIMIZED_WINDOW = 0x30
    REGULAR_WINDOW = 0x70
    
class LearningMethod(Enum):
    COUNTING = 1 
    EM = 3
    GRADIENT_DESCENT = 4

class SamplingMethod(Enum):
    DEFAULT = 0
    JOIN_TREE = 1
    FORWARD = 2


class NeticaError(Exception):
    def __init__(self, report): 
        pass

def checkerr():
    Netica.GetError_ns.restype = ctypes.POINTER(report_ns)
    report = Netica.GetError_ns(env, 5, None)
    if report:
        Netica.ErrorMessage_ns.restype = ctypes.c_char_p
        errmesg = Netica.ErrorMessage_ns(report)
        print('\n**** Netica Error ' + str(Netica.ErrorNumber_ns(report)) +  ': ' + errmesg.decode() + '\n')          
        Netica.ClearError_ns.restype = None
        Netica.ClearError_ns(report)
        raise NeticaError(errmesg.decode())
              
"""------------------------------Environ Class------------------------------"""
   
class Environ:
    
    def __init__(self, license_=None, environ=None, locn=None):
        
        if license_ is not None:
            license_ = ctypes.c_char_p(str.encode(license_))
        if locn is not None:
            locn = ctypes.c_char_p(str.encode(locn))
            
        Netica.NewNeticaEnviron_ns.restype = ctypes.POINTER(environ_ns)
        global env 
        env = Netica.NewNeticaEnviron_ns(license_, environ, locn)
        self.cptr = env
         
    def init_netica(self, mesg = None):         

        mesg = ctypes.c_char_p(b'')
                
        Netica.InitNetica2_bn.restype = ctypes.c_int
        return Netica.InitNetica2_bn(self.cptr, mesg), mesg.value.decode('utf-8')
    
        Netica.SetLanguage_ns.restype = ctypes.c_char_p
        Netica.SetLanguage_ns(self.cptr, "Python")
        
    def close_netica(self, mesg = None):
        """Exit Netica (i.e. make it quit).  
        
        If a human user has entered unsaved changes, this will check with the user first.  
        Like Netica-C CloseNetica_bn.
        """
        mesg = ctypes.c_char_p(b'')
        
        Netica.CloseNetica_bn.restype = ctypes.c_int
        return Netica.CloseNetica_bn(self.cptr, mesg), mesg.value.decode('utf-8')
    
    def new_net(self, name):
        """Create a new instance of the Net class"""
        return Net(name=name, env=self)
    
    def new_file_stream(self, filename, access=None):
        """Create a new instance of the Stream class"""
        return Stream(filename, self, access, 1)
    
    def new_memory_stream(self, name, access=None):
        """Create a new instance of the Stream class"""
        return Stream(name, self, access, None)
      
    def new_caseset(self, name):
        """Create a new instance of the Caseset class."""
        return Caseset(name, self)
    
    def new_learner(self, method):
        """Create a new instance of the Learner class"""
        return Learner(method, self)
    
    def new_dbmanger(self, connect_str, options):
        """Create a new instance of the DatabaseManager class"""
        return DatabaseManager(connect_str, options, self)
        
    def new_random_generator(self, seed, options):
        """Create a new instance of the RandomGenerator class
        
        Args:
            seed -- pass a positive (or zero) integer in the form of a string
            options -- pass either None or the string "Nondeterministic"
        """
        return RandomGenerator(seed, self, options)

"""-------------------------------Stream Class------------------------------"""
    
class Stream:     

    def __init__(self, name, env, access, isfile=None):
        
        if access is not None:
                access = ctypes.c_char_p(str.encode(access)) 
        if isfile is not None:                
            Netica.NewFileStream_ns.restype = ctypes.POINTER(stream_ns)
            self.cptr = Netica.NewFileStream_ns(ctypes.c_char_p(str.encode(name)), env.cptr, access)                                 
        else:        
            Netica.NewMemoryStream_ns.restype = ctypes.POINTER(stream_ns)
            self.cptr = Netica.NewMemoryStream_ns(ctypes.c_char_p(str.encode(name)), env.cptr, access)
        checkerr()

    def delete(self):
        """Remove this Streamer. 
        
        Frees all resources the Streamer consumes (including memory).  
        Like Netica-C DeleteStream_ns.
        """
        Netica.DeleteStream_ns.restype = None
        Netica.DeleteStream_ns(self.cptr)
        self.cptr = None
        checkerr()
           
    def read_net(self, options):

        return Net(stream=self, options=options)

    def set_password(self, password):
        """Set the password that will be used to encrypt/decrypt anything written/read to this stream.  
        
        Pass None to remove the password
        Like Netica-C SetStreamPassword_ns.
        """
        if password is not None:
            password = ctypes.c_char_p(str.encode(password))
        Netica.SetStreamPassword_ns.restype = None
        Netica.SetStreamPassword_ns(self.cptr, password)
        checkerr()
        
    def get_contents(self, length=None):             
        """Return the contents as set from SetContents, read from file or generated by Netica.  
        
        Like Netica-C GetStreamContents_ns.
        """
        if length is not None:
            length = ctypes.c_long(length)
        Netica.GetStreamContents_ns.restype = ctypes.c_char_p
        contents = Netica.GetStreamContents_ns(self.cptr, length)         
        checkerr()
        return contents

"""--------------------------------Net Class--------------------------------"""

class Net:
    
    def __init__(self, name=None, env=None, stream=None, options=None):
    
        if name is not None:          
            Netica.NewNet_bn.restype = ctypes.POINTER(name)   
            self.cptr = Netica.NewNet_bn(ctypes.c_char_p(str.encode(name)), env.cptr)
        elif stream is not None:
            Netica.ReadNet_bn.restype = ctypes.POINTER(net_bn)
            self.cptr = Netica.ReadNet_bn(stream.cptr, ctypes.c_int(options.value))
        checkerr()   

    def delete(self):
        """Remove this net. 
        
        Frees all resources it consumes (including memory).
        Like Netica-C DeleteNet_bn.
        """
        Netica.DeleteNet_bn.restype = None
        Netica.DeleteNet_bn(self.cptr)
        self.cptr = None
        checkerr()

    def write(self, file):
        """"Write this net to the indicated file (which may include directory path).  
        
        Like Netica-C WriteNet_bn.
        """
        Netica.WriteNet_bn.restype = None
        Netica.WriteNet_bn(self.cptr, file.cptr)
        checkerr()    

    @property
    def comment(self):
        """Get documentation or description of this net.
        
        Like Netica-C GetNetComment_bn.
        """
        Netica.GetNetComment_bn.restype = ctypes.c_char_p
        comment = Netica.GetNetComment_bn(self.cptr).decode()
        checkerr()
        return comment

    @comment.setter
    def comment(self, comment):
        """Set documentation or description of this net.
        
        Like Netica-C G/SetNetComment_bn.
        """
        Netica.SetNetComment_bn.restype = None
        Netica.SetNetComment_bn(self.cptr, ctypes.c_char_p(str.encode(comment)))
        checkerr()
    
    def new_node(self, name, states):
        
        if isinstance(states, int): 
            checkerr()
            return Node(name=name, net=self, num_states=states, newnode=True)
        
        if isinstance(states, str):          
            node = Node(name=name, net=self, num_states=len(states.split(",")), newnode=True)
            Netica.SetNodeStateNames_bn.restype = None
            Netica.SetNodeStateNames_bn(node.cptr, ctypes.c_char_p(str.encode(states)))
            checkerr()
            return node

    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 nodelist none, all nodes in net
        Netica.WriteNetFindings_bn.restype = ctypes.c_long
        Netica.WriteNetFindings_bn(nodes.cptr, file.cptr, ctypes.c_long(ID_num), ctypes.c_double(freq))
        checkerr()
    
    def compile_(self):                      
        """Compile this net for fast inference.
        
        Like Netica-C CompileNet_bn.
        """
        Netica.CompileNet_bn.restype = None
        Netica.CompileNet_bn(self.cptr)
        checkerr()

    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(self.cptr)
        checkerr()
        
    @property
    def autoupdate(self):
        """Return state of auto-update.
        
        Like Netica-C GetNetAutoUpdate_bn. 
        """
        Netica.GetNetAutoUpdate_bn.restype = ctypes.c_int
        autoupdate_state = Netica.GetNetAutoUpdate_bn(self.cptr)
        checkerr()
        return autoupdate_state  
    
    def set_auto_update(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 SetNetAutoUpdate_bn.
        """
        Netica.SetNetAutoUpdate_bn.restype = ctypes.c_int
        Netica.SetNetAutoUpdate_bn(self.cptr, auto_update)
        checkerr()

    def get_node_named(self, name):
        """Return a node from this net by name.  
        
        Like Netica-C GetNodeNamed_bn.
        """
        return Node(name=name, net=self, getnode=True)
    
    @property
    def nodes(self):
        return NodeList(net=self)

    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.  
        Like Netica-C CopyNodes_bn.
        """
        return NodeList(net=self, copy=nodes)

    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.
        """
        Netica.ReviseCPTsByCaseFile_bn.restype = None
        Netica.ReviseCPTsByCaseFile_bn(file.cptr, nodes.cptr, ctypes.c_int(updating), ctypes.c_double(degree))
        checkerr()

    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.
        """
        Netica.GenerateRandomCase_bn.restype = ctypes.c_int
        if rand is not None:
            rand = rand.cptr
        res = Netica.GenerateRandomCase_bn(nodes.cptr, ctypes.c_int(method.value), ctypes.c_double(num), rand)
        checkerr()
        return res
    
    def new_node_list(self, length):
        """Create a new empty NodeList (i.e. list of Nodes).  
        
        It will be initialized to 'nodes', which should be an array of BNodes, or empty.  
        Like Netica-C NewNodeList2_bn
        """
        return NodeList(net=self, length=length)
    
    def new_tester(self, test_nodes, unobsv_nodes, tests):
        """Create a Tester to performance test this net using a set of cases.  
        
        Like Netica-C NewNetTester_bn.
        """
        return Tester(test_nodes=test_nodes, unobsv_nodes=unobsv_nodes, tests=tests)
          
"""--------------------------------Node Class-------------------------------"""    
     
class Node:
    
    def __init__(self, name=None, net=None, num_states=None, newnode=None, getnode=None, nodelist=None, index=None):
         
        if newnode is not None:
            
            Netica.NewNode_bn.restype = ctypes.POINTER(node_bn)  
            self.cptr = Netica.NewNode_bn(ctypes.c_char_p(str.encode(name)), num_states, net.cptr)
        
        elif getnode is not None:
            
            Netica.GetNodeNamed_bn.restype = ctypes.POINTER(node_bn)
            self.cptr = Netica.GetNodeNamed_bn(ctypes.c_char_p(str.encode(name)), net.cptr)
        
        elif index is not None:
            """Called by NodeList.nth_node(index)"""
            Netica.NthNode_bn.restype = ctypes.POINTER(node_bn)
            self.cptr = Netica.NthNode_bn(nodelist.cptr, index)
        
        checkerr()

    @property
    def state_names(self):
        """Return the names of this node's states separated by commas.  
        
        See also statename (just one state).  
        Like Netica-C GetNodeStateName_bn.
        """
        statenames = []
        Netica.GetNodeStateName_bn.restype = ctypes.c_char_p
        for i in range(self.num_states):
            statenames.append(Netica.GetNodeStateName_bn(self.cptr, i).decode())
        checkerr()
        return statenames
    
    @state_names.setter
    def state_names(self, statenames):
        """Set the names of this node's states separated by commas.  
        
        Pass in a single string with new state names seperated by commas.
        See also statename (just one state).  
        Like Netica-C SetNodeStateNames_bn.
        """
        Netica.SetNodeStateNames_bn.restype = None
        Netica.SetNodeStateNames_bn(self.cptr, ctypes.c_char_p(str.encode(statenames)))
        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(self.cptr, state).decode()
        checkerr()
        return statename
    
    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(self.cptr, state, ctypes.c_char_p(str.encode(statename)))
        checkerr()
    
    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(str.encode(state)), self.cptr)
        checkerr()
        return statenum
    
    @property
    def name(self):
        """ Get name of this node.  

        Like Netica-C GetNodeName_bn.
        """
        Netica.GetNodeName_bn.restype = ctypes.c_char_p
        name = Netica.GetNodeName_bn(self.cptr).decode()
        checkerr()
        return name
    
    @property
    def title(self):
        """ Get unrestricted label for this node.  

        Like Netica-C GetNodeTitle_bn.
        """
        Netica.GetNodeTitle_bn.restype = ctypes.c_char_p
        title = Netica.GetNodeTitle_bn(self.cptr).decode()
        checkerr()
        return title
    
    @title.setter
    def title(self, title):
        """Unrestricted label for this node.  

        Like Netica-C SetNodeTitle_bn.
        """
        Netica.SetNodeTitle_bn.restype = None
        Netica.SetNodeTitle_bn(self.cptr, ctypes.c_char_p(str.encode(title)))
        checkerr()

    def add_link_from(self, parent):
        """Add a link from 'parent' node to this node.
        
        Like Netica-C AddLink_bn.
        """
        Netica.AddLink_bn.restype = ctypes.c_int
        Netica.AddLink_bn(parent.cptr, self.cptr)
        checkerr()
        
    @property
    def kind(self):
        """How this node is being used (nature, decision, etc).  
        
        Like Netica-C GetNodeKind_bn.
        """
        Netica.GetNodeKind_bn.restype = ctypes.c_int
        node_kind = NodeKind(Netica.GetNodeKind_bn(self.cptr)).name
        checkerr()
        return node_kind
    
    @kind.setter
    def kind(self, kind):
        """How this node is being used (nature, decision, etc).  
        
        Like Netica-C SetNodeKind_bn.
        """
        Netica.SetNodeKind_bn.restype = None
        Netica.SetNodeKind_bn(self.cptr, ctypes.c_int(kind.value))
        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
        node_type = NodeType(Netica.GetNodeType_bn(self.cptr)).name
        checkerr()
        return node_type
          
    @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(self.cptr)
        checkerr()
        return node_num_states
    
    @property
    def state_levels(self):
        """Return state level.
        
        If this node is for a continuous variable, then this is a discretization 
        threshold, otherwise it is an output level.  
        Like Netica-C GetNodeLevels_bn.
        """
        Netica.GetNodeLevels_bn.restype = ctypes.POINTER(ctypes.c_double)
        state_levels_pointer = Netica.GetNodeLevels_bn(self.cptr)
        checkerr()
        state_levels = []
        if state_levels_pointer:
            for i in range(self.num_states):
                state_levels.extend([state_levels_pointer[i]])      
            checkerr()
        return state_levels
    
    @state_levels.setter
    def state_levels(self, levels):
        """Set state level.
        
        If this node is for a continuous variable, then this is a discretization 
        threshold, otherwise it is an output level.  
        Like Netica-C SetNodeLevels_bn.
        """
        Netica.SetNodeLevels_bn.restype = None
        Netica.SetNodeLevels_bn(self.cptr, ctypes.c_int(len(levels)), (ctypes.c_double*len(levels))(*levels))
        checkerr()    
        
    def get_func_state(self, parentstates):
        """Return the table entry giving the state of this node for the row 'parent_states'. 
        
        (pass a Condition, array of state indexes or comma-delimited string of state names).  
        For continuous nodes, normally use RealFuncTable instead.  
        Like Netica-C G/SetNodeFuncState_bn
        """
        cparentstates = (ctypes.c_int*len(parentstates))(*parentstates)
        Netica.GetNodeFuncState_bn.restype = ctypes.c_int
        nodefuncstates = Netica.GetNodeFuncState_bn(self.cptr, cparentstates)
        checkerr()
        return nodefuncstates
    
    def set_func_real(self, parentstates, val):
        """A table entry giving the real value of this node for the row 'parent_states' 
        
        (Pass a Condition, array of state indexes or comma-delimited string of state names). 
        For discrete nodes, normally use StateFuncTable instead. 
        Like Netica-C G/SetNodeFuncReal_bn.
        """   
        cparentstates = (ctypes.c_int*len(parentstates))(*parentstates)
        Netica.SetNodeFuncReal_bn.restype = None
        Netica.SetNodeFuncReal_bn(self.cptr, cparentstates, ctypes.c_double(val))
        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.  
        
        Like Netica-C GetNodeEquation_bn.
        """
        Netica.GetNodeEquation_bn.restype = ctypes.c_char_p
        eqn = Netica.GetNodeEquation_bn(self.cptr).decode()
        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.  
        
        Pass None for eqn to remove equation.
        Like Netica-C SetNodeEquation_bn.
        """
        if eqn is not None:
            eqn = ctypes.c_char_p(str.encode(eqn))
        Netica.SetNodeEquation_bn.restype = None
        Netica.SetNodeEquation_bn(self.cptr, eqn)
        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(self.cptr, num_samples, ctypes.c_ubyte(samp_unc), ctypes.c_ubyte(add_exist))
        checkerr()
    
    @property
    def parents(self):
        """Return parent nodes of this node.  
        
        Create an instance of the NodeList class.
        That is, those nodes which are at the beginning of a link that enters this node.  
        Like Netica-C GetNodeParents_bn.
        """
        return NodeList(node=self)
    
    def set_CPT(self, parentstates, probs):
        """Set probabilities for the states of this node, 
        
        given the passed parent_states 
        (as a Condition, array of state indexes or comma-delimited string of state names in the same order as this node's parents).  
        Like Netica-C G/SetNodeProbs_bn.
        """
        if parentstates is None:
            cparentstates = parentstates
            
        if isinstance(parentstates, list):
            cparentstates = (ctypes.c_int*len(parentstates))(*parentstates)
        
        if isinstance(parentstates, str):
            statenames = parentstates.split(', ')
            parents = self.parents
            numparents = parents.length
            parent_states = []
            for i in range(numparents):
                nthparent = parents.nth_node(i)             
                state = nthparent.get_state_named(statenames[i])
                parent_states.append(state)  
            cparentstates = (ctypes.c_int*len(parent_states))(*parent_states)
        
        cprobs = (ctypes.c_float*len(probs))(*probs)
        
        Netica.SetNodeProbs_bn.restype = None
        Netica.SetNodeProbs_bn(self.cptr, cparentstates, cprobs)
        checkerr()

    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 GetNodeExpectedUtils_bn.
        """
        Netica.GetNodeExpectedUtils_bn.restype = ctypes.POINTER(ctypes.c_float)
        expectedutils_pointer = Netica.GetNodeExpectedUtils_bn(self.cptr)
        expectedutils = []
        for i in range(self.num_states):
            expectedutils.extend([expectedutils_pointer[i]]) 
        checkerr()
        return expectedutils
        
    def get_expected_value(self, std_dev, x3=None, x4=None):
        """Return the expected value (and standard deviation) for this real valued node, 
        based on its current beliefs (i.e. taking into account all findings entered in the net).  
        
        See also get_beliefs, get_expected_utils.  
        Like Netica-C GetNodeExpectedValue_bn.
        """
        Netica.GetNodeExpectedValue_bn.restype = ctypes.c_double
        expectedval = Netica.GetNodeExpectedValue_bn(self.cptr, ctypes.c_double(std_dev), x3, x4)
        checkerr()
        return expectedval   
    
    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.   
        Like Netica-C GetNodeBeliefs_bn.
        """
        #See also CalcState, GetExpectedValue, GetExpectedUtility. 
        Netica.GetNodeBeliefs_bn.restype = ctypes.POINTER(ctypes.c_float)
        beliefs_pointer = Netica.GetNodeBeliefs_bn(self.cptr)
        beliefs = []
        for i in range(self.num_states):
            beliefs.extend([beliefs_pointer[i]])      
        checkerr()
        return beliefs 

    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(self.cptr, ctypes.c_int(state))
        checkerr()
    
    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(self.cptr)
        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(self.cptr)
        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(self.cptr, ctypes.c_double(value))
        checkerr()
     

"""------------------------------NodeList Class-----------------------------"""         
    
class NodeList:
    
    def __init__(self, net=None, node=None, copy=None, length=None):
              
        if node is not None:
            Netica.GetNodeParents_bn.restype = ctypes.POINTER(nodelist_bn)
            self.cptr = Netica.GetNodeParents_bn(node.cptr)  
        elif copy is not None:
            Netica.CopyNodes_bn.restype = ctypes.POINTER(nodelist_bn)
            self.cptr = Netica.CopyNodes_bn(copy.cptr, net.cptr, options=None)
        elif length is not None:
            Netica.NewNodeList2_bn.restype = ctypes.POINTER(nodelist_bn)
            self.cptr = Netica.NewNodeList2_bn(ctypes.c_int(length), net.cptr)
        elif net is not None:
            Netica.GetNetNodes2_bn.restype = ctypes.POINTER(nodelist_bn)
            self.cptr = Netica.GetNetNodes2_bn(net.cptr, None)  # Add options
        checkerr()
    
    def delete(self):
        """Remove this node list, freeing all resources it consumes (including memory).  
        
        Like Netica-C DeleteNodeList_bn.
        """
        Netica.DeleteNodeList_bn.restype = None
        Netica.DeleteNodeList_bn(self.cptr)
        self.cptr = None
        checkerr()
    
    @property
    def length(self):
        """Return the number of nodes and empty entries in this list.  
        
        Like Netica-C LengthNodeList_bn.
        """
        Netica.LengthNodeList_bn.restype = ctypes.c_int
        length = Netica.LengthNodeList_bn(self.cptr)
        checkerr()
        return length

    def nth_node(self, index):
        """Return the nth node in this list (first is 0).  
        
        Can also access by name string instead of integer index.  
        Like Netica-C NthNode_bn/SetNthNode_bn.
        """
        return Node(nodelist=self, index=index)
    
    def add(self, node, index):
        """Insert the given node at the given index.  
        
        Passing None for index adds it to the end.  
        Like Netica-C AddNodeToList_bn.
        """
        Netica.AddNodeToList_bn.restype = None
        Netica.AddNodeToList_bn(node.cptr, self.cptr, index)
        checkerr()

"""------------------------------Caseset Class------------------------------"""

class Caseset:

    def __init__(self, name, env):
        """Create an initially empty set of cases.  
        
        'name' argument is not required (i.e. may be empty).  
        Like Netica-C NewCaseset_cs.
        """
        Netica.NewCaseset_cs.restype = ctypes.POINTER(caseset_cs)
        self.cptr = Netica.NewCaseset_cs(ctypes.c_char_p(str.encode(name)), env.cptr)
        checkerr()

    def delete(self):
        """Remove this Caseset, freeing all resources it consumes (including memory).  
               
        Will not delete files this Caseset refers to.  
        Like Netica-C DeleteCaseset_cs.
        """
        Netica.DeleteCaseset_cs.restype = None
        Netica.DeleteCaseset_cs(self.cptr)
        checkerr()
        
    def add_file(self, file, degree, options=None):
        """Add all the cases contained in 'file'.  
        
        'file' is the name of a stream
        'degree' (normally 1) is a multiplier for the multiplicty of each case.  
        'options' must be empty or None.  
        Like Netica-C AddFileToCaseset_cs.
        """
        Netica.AddFileToCaseset_cs.restype = None
        Netica.AddFileToCaseset_cs(self.cptr, file.cptr, ctypes.c_double(degree), options)
        checkerr()

    def write(self, file, options=None):
        """Write all the cases to the indicated file (which may be on disk or in memory). 
        
        'options' must be empty or NULL.  
        Like Netica-C WriteCaseset_cs.
        """
        Netica.WriteCaseset_cs.restype = None
        Netica.WriteCaseset_cs(self.cptr, file.cptr, options)
        checkerr()

"""-----------------------------DatabaseManager Class-----------------------------"""

class DatabaseManager:
    
    def __init__(self, connect_str, options, env):
        
        Netica.NewDaManager_cs.restype = ctypes.POINTER(dbmgr_cs)
        self.cptr = Netica.NewDBManager_cs(ctypes.c_char_p(str.encode(connect_str)), options, env.cptr)
        checkerr()
        
    def delete(self):
        """"Remove this DatabaseManager.
        
        Closes connections and frees all the resources it consumes 
        (including memory).  
        Like Netica-C DeleteDBManager_cs.
        """
        Netica.DeleteDBManager_cs.restype = None
        Netica.DeleteDBManager_cs(self.cptr)
        checkerr()

    def execute_db_sql(self, sql_cmd, options):
        """Execute the passed SQL statement.  
        
        Like Netica-C ExecuteDBSql_cs.
        """
        Netica.ExecuteDBSql_cs.restype = None
        Netica.ExecuteDBSql_cs(self.cptr, ctypes.c_char_p(str.encode(sql_cmd)), options)
        checkerr()
        
    def insert_findings(self, nodes, column_names, tables, options):
        """Add a new record to the database, consisting of the findings in 'nodes'.  
        
        If 'column_names' non-empty, it contains the database names for each of the variables in 'nodes'.  
        'tables' empty means use default table.  
        Like Netica-C InsertFindingsIntoDB_bn.
        """
        Netica.InsertFindingsIntoDB_bn.restype = None
        Netica.InsertFindingsIntoDB_bn(self.cptr, nodes.cptr, ctypes.c_char_p(str.encode(column_names)),
                                       ctypes.c_char_p(str.encode(tables)), options)
        checkerr()
        
"""------------------------------Learner Class------------------------------"""  
      
class Learner:
    
    def __init__(self, method, env, options=None):
        """Create a new learner, which will learn using 'method'. 
        
        (one of \"EM\", \"gradient.  
        Like Netica-C NewLearner_bn.
        """
        Netica.NewLearner_bn.restype = ctypes.POINTER(learner_bn)
        self.cptr = Netica.NewLearner_bn(ctypes.c_int(method.value), options, env.cptr)
        checkerr()
    
    def delete(self):
        """Remove this Learner, freeing all resources it consumes (including memory). 
        
        Like Netica-C DeleteLearner_bn.
        """
        Netica.DeleteLearner_bn.restype = None
        Netica.DeleteLearner_bn(self.cptr)
        checkerr()
      
    @property
    def max_iterations(self):
        """Return parameter to control termination.  
        
        The maximum number of learning step iterations.  
        Like Netica-C SetLearnerMaxIters_bn.
        """
        Netica.SetLearnerMaxIters_bn.restype = ctypes.c_int
        maxiters = Netica.SetLearnerMaxIters_bn(self.cptr, ctypes.c_int(-1))
        checkerr()
        return maxiters
    
    @max_iterations.setter
    def max_iterations(self, max_iters):
        """Set parameter to control termination.  
        
        The maximum number of learning step iterations.  
        Like Netica-C SetLearnerMaxIters_bn.
        """
        Netica.SetLearnerMaxIters_bn.restype = ctypes.c_int
        Netica.SetLearnerMaxIters_bn(self.cptr, ctypes.c_int(max_iters))
        checkerr()
    
    @property
    def max_tolerance(self):
        """Return parameter to control termination.  
        
        When the log likelihood of the data given the model improves by less 
        than this, the model is considered close enough.  
        Like Netica-C SetLearnerMaxTol_bn.
        """
        Netica.SetLearnerMaxTol_bn.restype = ctypes.c_double
        maxtol = Netica.SetLearnerMaxTol_bn(self.cptr, ctypes.c_double(-1)) # QUERY_ns
        checkerr()
        return maxtol
    
    @max_tolerance.setter
    def max_tolerance(self, max_tol):
        """Set parameter to control termination.  
        
        When the log likelihood of the data given the model improves by less 
        than this, the model is considered close enough.  
        Like Netica-C SetLearnerMaxTol_bn.
        """
        Netica.SetLearnerMaxTol_bn.restype = ctypes.c_double
        Netica.SetLearnerMaxTol_bn(self.cptr, ctypes.c_double(max_tol))
        checkerr()
      
    def learn_CPTs(self, nodes, cases, degree):
        """Modifie the CPTs (and experience tables) for the nodes in 'nodes', 
        to take into account the case data from 'cases' (with multiplicity multiplier 'degree', which is normally 1).  
        Like Netica-C LearnCPTs_bn.
        """
        Netica.LearnCPTs_bn.restype = None
        Netica.LearnCPTs_bn(self.cptr, nodes.cptr, cases.cptr, ctypes.c_double(degree))
        checkerr()

"""------------------------------Tester Class-------------------------------"""  
    
class Tester:
    
    def __init__(self, test_nodes, unobsv_nodes, tests):
        Netica.NewNetTester_bn.restype = ctypes.POINTER(tester_bn)
        self.cptr = Netica.NewNetTester_bn(test_nodes.cptr, unobsv_nodes.cptr, tests)
        checkerr()
        
    def delete(self):
        """Remove this net tester.
        
        Frees all resources it consumes (including memory).  
        Like Netica-C DeleteNetTester_bn.
        """
        Netica.DeleteNetTester_bn.restype = None
        Netica.DeleteNetTester_bn(self.cptr)
        checkerr()

    def test_with_cases(self, cases):
        """Scan through the data in 'cases', and for each case check the values 
        in the case for test_nodes against the predictions made by the net 
        based on the other values in the case.  
        
        test_nodes is set with BNet.NewNetTester.  
        Like Netica-C TestWithCaseset_bn
        """
        Netica.TestWithCaseset_bn.restype = None
        Netica.TestWithCaseset_bn(self.cptr, cases.cptr)
        checkerr()

    def error_rate(self, node):
        """Return the fraction of test cases where the net predicted the wrong state.  
        
        Like Netica-C GetTestErrorRate_bn.
        """
        Netica.GetTestErrorRate_bn.restype = ctypes.c_double
        Netica.GetTestErrorRate_bn(self.cptr, node.cptr)
        checkerr()

    def log_loss(self, node):
        """The 'logarithmic loss', which for each case takes into account the 
        prediction probability the net gives to the state that turns out to be correct.  
        
        Ranges from 0 (perfect score) to infinity.  
        Like Netica-C GetTestLogLoss_bn.
        """
        Netica.GetTestLogLoss_bn.restype = ctypes.c_double
        Netica.GetTestLogLoss_bn(self.cptr, node.cptr)
        checkerr()

    def quadratic_loss(self, node):
        """The 'quadratic loss', also known as 'Brier score' for 'node' under 
        the test performed by TestWithCases.  
        
        Like Netica-C GetTestQuadraticLoss_bn.
        """
        Netica.GetTestQuadraticLoss_bn.restype = ctypes.c_double
        Netica.GetTestQuadraticLoss_bn(self.cptr, node.cptr)
        checkerr()
        

"""--------------------------Random Generator Class-------------------------"""

class RandomGenerator:
    
    def __init__(self, seed, env, options):

        Netica.NewRandomGenerator_ns.restype = ctypes.POINTER(randgen_ns)
        if options is not None:
            options = ctypes.c_char_p(str.encode(options))
        self.cptr = Netica.NewRandomGenerator_ns(ctypes.c_char_p(str.encode(seed)), env.cptr, options)
        checkerr()
        
    def delete(self):

        Netica.DeleteRandomGen_ns.restype = None
        Netica.DeleteRandomGen_ns(self.cptr)
        checkerr()
        
    def get_state(self, options=None):
        """Return a string to use as a seed
        
        Returns a string that can be used as a seed to NewRandomGenerator_ns to create a random generator that 
        will produce exactly the same pseudo-random results as this one will.
        So you can save the state of a random generator to later make an equivalent generator.  
        Options for future expansion
        Like Netica-C GetRandomGenState_ns.
        """
        Netica.GetRandomGenState_ns.restype = ctypes.c_char_p
        state = Netica.GetRandomGenState_ns(self.cptr, )
        checkerr()
        return state   