# -*- coding: utf-8 -*-
"""
Created on Thu Mar 19 10:49:41 2020

@author: Sophie
"""

import ctypes
import weakref

from neticapy import environ as envrn
from neticapy import node as nd
from neticapy import neticaerror as err
from neticapy.loaddll import Netica

def _create_nodelist(cptr, is_const=None):
    """Function called by NeticaPy to create a nodelist.
    
    cptr is the C pointer generated by a call to a Netica DLL function. 
    _create_nodelist uses the pointer to check whether a NeticaPy instance of 
    this nodelist already exists, and returns any existing nodelist. 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 NodeList(('from_create_nodelist', cptr, is_const), None)
    else:
        return NodeList(('from_create_nodelist', cptr, is_const), None)
    

class NodeList:
    
    def __init__(self, length, net): # What should we do if net is none?
        
        self.cptr = None # Initialize cptr for case where Netica raises an error during construction
        self.is_const = False

        if isinstance(length, tuple):  # Could use options instead of tuple in name?
            indicator, passed_cptr, is_const = length
            if indicator == 'from_create_nodelist':
                cptr = passed_cptr
                self.is_const = is_const
            else:
                pass # raise some error
            
        else:   # elif? Is this robust enough?
            Netica.NewNodeList2_bn.restype = ctypes.c_void_p
            cptr = Netica.NewNodeList2_bn(ctypes.c_int(length), 
                                          ctypes.c_void_p(net.cptr))
            err.checkerr()
        
        self.cptr = cptr
        
        if envrn.dict_initialization:
            envrn.cptr_dict[cptr] = weakref.ref(self)       
        
    def __del__(self):
        """Remove this node list, freeing all resources it consumes, including memory.  
        
        Like Netica-C DeleteNodeList_bn.
        """
        if not self.is_const:
            if envrn.env is not None:
                Netica.DeleteNodeList_bn.restype = None
                Netica.DeleteNodeList_bn(ctypes.c_void_p(self.cptr))
                err.checkerr()
        if envrn.dict_initialization:
            del envrn.cptr_dict[self.cptr]
    
    def clear(self):
        """Remove all entries from this node list, so it becomes empty.  
        
        Like Netica-C ClearNodeList_bn.
        """
        Netica.ClearNodeList_bn.restype = None
        Netica.ClearNodeList_bn(ctypes.c_void_p(self.cptr))
    
    @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(ctypes.c_void_p(self.cptr))
        err.checkerr()
        return length

    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.
        """
        if not isinstance(node, nd.Node):
            raise TypeError('A Node is required (got type {})'.format(type(node).__name__))
        if index is None:
            index = -10
        Netica.AddNodeToList_bn.restype = None
        Netica.AddNodeToList_bn(ctypes.c_void_p(node.cptr), ctypes.c_void_p(self.cptr), 
                                ctypes.c_int(index))
        err.checkerr()
        
    def remove(self, index):
        """Remove (and return) the node at the position indicated by 'index'.
        
        Index can be an integer, Node, or node name. 
        Passing 'LAST_ENTRY' for index removes it from the end. 
        Like Netica-C RemoveNthNode_bn.
        """
        if isinstance(index, str):
            if index == 'LAST_ENTRY':
                index = -10
            else:
                net = self.get_nth_node(0).net
                node = net.get_node_named(index)
                index = self.index_of(node)
        elif isinstance(index, nd.Node):
            index = self.index_of(index)

        Netica.RemoveNthNode_bn.restype = ctypes.c_void_p
        cptr = Netica.RemoveNthNode_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(index))
        err.checkerr()
        
        return nd._create_node(cptr)
        
    def get_nth_node(self, index):                              # Previously called nth_node
        """Return the nth node in this list (first is 0).  
        
        Can also access by name string instead of integer index. 
        Like Netica-C GetNthNode_bn.
        """
        if isinstance(index, str):
            index = self.index_of(index)
        Netica.GetNthNode_bn.restype = ctypes.c_void_p
        cptr = Netica.GetNthNode_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(index))
        err.checkerr()
        
        return nd._create_node(cptr)
    
    def set_nth_node(self, node, index):  
        """Put node at position index of list nodes without changing the length of the list.
        
        Can also access by name string instead of integer index.
        Like Netica-C SetNthNode_bn.
        """
        if isinstance(index, str):
            index = self.index_of(index)
        if not isinstance(node, nd.Node):
            raise TypeError('A Node is required (got type {})'.format(type(node).__name__))    
        Netica.SetNthNode_bn.restype = None
        Netica.SetNthNode_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(index), 
                             ctypes.c_void_p(node.cptr))
        err.checkerr()

    def index_of(self, node, start_index=0):
        """Return the position (index) of 'node' in this list  
        
        Return the index of the first occuence of 'node', or None if it is not 
        found. If 'start_index' is passed, the search starts on that index and 
        goes to the end.  
        Like Netica-C IndexOfNodeInList_bn.
        """
        if isinstance(node, str):
            net = self.get_nth_node(0).net
            node = net.get_node_named(node)
            if node is None:
                errmesg = "Function NodeList.index_of received a \
                string that was not the name of any node in the Net."
                
                err._raise_netica_error(6504, errmesg)
            
        if not isinstance(node, nd.Node):
            raise TypeError('A Node or str is required (got type {})'.format(type(node).__name__))
        
        Netica.IndexOfNodeInList_bn.restype = ctypes.c_int
        index = Netica.IndexOfNodeInList_bn(ctypes.c_void_p(node.cptr), 
                                            ctypes.c_void_p(self.cptr), ctypes.c_int(start_index))
        err.checkerr()
        if index == -1:
            return None
        else:
            return index
    
    def copy(self):
        """Duplicate the list nodes, and return the duplicate list.
        
        This only makes a copy of the list; if you want to duplicate the nodes 
        as well, use Net.copy_nodes.
        Like Netica-C DupNodeList_bn.
        """            
        Netica.DupNodeList_bn.restype = ctypes.c_void_p
        cptr = Netica.DupNodeList_bn(ctypes.c_void_p(self.cptr)) 
        err.checkerr()
        
        return _create_nodelist(cptr)

    @property
    def nodes(self): # We also have Net.nodes
        """Return a python list of all the nodes in this NodeList.
        """
        # ***** In progress
        # num_nodes = self.length
        # Netica.GetNodeListNodes_bn.restype = ctypes.c_void_p
        # Netica.GetNodeListNodes_bn(ctypes.c_void_p(self.cptr), ctypes.c_int(num_nodes))

        
        #PythonOnly
        num_nodes = self.length
        py_list = []
        for i in range(num_nodes):
            py_list.append(self.get_nth_node(i))       
        return py_list
        
        #IMPORT (node_bn* const*) GetNodeListNodes_bn (const nodelist_bn* nodelist, int* num_nodes);

                                    