"""
==============
Iofile package
==============
Provides Common file-io related attributes and methods 
for TheSyDeKinck
This package defines the CSF IO-file format and methods for all supported simulators.
Intention is to keep as much of the file io as general adn common as possible.
The simulator specific file-io functions should be written in simulator specific file-io
packages.
Initially written for Verilog file-io by Marko Kosunen, marko.kosunen@aalto.fi,
Yue Dai, 2018
Generalized for common file-io by Marko Kosunen, marko.kosunen@aalto.fi,
2019.
"""
import os
import sys
import random
import string
from abc import * 
from thesdk import *
import numpy as np
import pandas as pd
[docs]
class iofile(IO):
     '''
     Class to provide file IO for external simulators. 
     
     When created, adds an iofile object to the parents iofile_bundle attribute.
     Accessible as iofile_bundle.Members['name'].
 
     Example
     -------
     Initiated in parent as: 
         _=iofile(self,name='foobar')
     
     '''
     def __init__(self,parent=None,**kwargs):
         '''
         Parameters
         -----------
         parent: object, None 
             The parent object initializing the iofile instance. Default None
         
         **kwargs:  
                 name: str  
                     Name of the file. Appended with 
                     random string during the simulation.
                 param: str,  -g g_file
                     The string defining the testbench parameter to be be 
                     passed to the simulator at command line.
         '''
         if parent==None:
             self.print_log(type='F', msg="Parent of Verilog input file not given")
         try:  
             super(iofile,self).__init__(**kwargs)
             self.parent=parent
             self.rndpart=''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(16))
             self.name=kwargs.get('name') 
 
             # IO's are output by default
             # Currently less needed for Python, but used in Verilog
             self._dir=kwargs.get('dir','out')
             self._datatype=kwargs.get('datatype','int')
 
             self._iotype=kwargs.get('iotype','sample') # The file is a data file by default 
                                                   # Option: sample, event. file 
                                                   # Events are presented in time-value combinatios 
                                                   # time in the column 0
             self._ionames=kwargs.get('ionames',[]) #list of signal names associated with this io
             self._ioformat=kwargs.get('ioformat','%d') #by default, the io values are decimal integer numbers
 
             self.hasheader=kwargs.get('hasheader',False) # Headers False by default. 
                                                          # Do not generate things just 
                                                          # to remove them in the next step
             if hasattr(self.parent,'preserve_iofiles'):
                 self.preserve=parent.preserve_iofiles
             else:
                 self.preserve=False
         except:
             self.print_log(type='F', msg="IO-file definition failed")
 
         #TODO: Needs a check to eliminate duplicate entries to iofiles
         if hasattr(self.parent,'iofiles'):
             self.print_log(type='O',msg="Attribute iofiles has been replaced by iofile_bundle")
 
         if hasattr(self.parent,'iofile_bundle'):
             self.parent.iofile_bundle.new(name=self.name,val=self)
 
     # These propertios "extend" IO class, but do not need ot be member of it,
     # Furthermore IO._Data _must_ me bidirectional. Otherwise driver and target 
     # Must be defined separately
     @property 
     def dir(self):
         ''' Direction of the iofile, in | out
         '''
         if hasattr(self,'_dir'):
             return self._dir
         else:
             self._dir=None
         return self._dir
 
     @dir.setter
     def dir(self,value):
         self._dir=value
 
     @property
     def iotype(self):  # sample | event
         ''' Type of the IO: sample | event
             sample
                 IO is synchronized with a sampling clock. Each of the rows represents the data valua at known sample insatnce
             event
                 IO is asynchronous. The first colums of the file should be the time of the event
         '''
         if hasattr(self,'_iotype'):
             return self._iotype
         else:
             self._iotype='sample'
         return self._iotype
 
     @property
     def datatype(self):  #          
         ''' Type of the data. Controls the reading and writing of the data
              'complex' | 'int' | 'scomplex' | 'sint' | object
              int | sint:
                  Values of the data are handled as unsigned or signed integers
              complex | scomplex: 
                  It is assumed that each column of the Data represents a complex number with
                  integer real and imaginary parts.  This is typical for RTL simulations.
                  when the data is read or written, the columns are treated as Real Imag pairs. 
                  When writing, columns are split to two, when reading, adjacent colums are merged
              object:
                  Values are read and written as defined by the 'dtype' parameter 
                  of the read method. Default is object. 
         '''
         if hasattr(self,'_datatype'):
             return self._datatype
         else:
             self._datatype=None
         return self._datatype
 
     @datatype.setter
     def datatype(self,value):
         self._datatype=value
 
     @property
     def ionames(self): # list of associated ionames
         ''' Names of the DUT signals represented by the colums of the data. 
             Two elements per colums should be given, if the type is complex or scomplex.
         '''
         if hasattr(self,'_ionames'):
             return self._ionames
         else:
             self._ionames=[]
         return self._ionames
 
     @ionames.setter
     def ionames(self,value):
         self._ionames=value
 
 
     @property
     def file(self):
         ''' Name of the IO file to be read or written.
         '''
         if not hasattr(self,'_file'):
             self._file=self.parent.simpath +'/' + self.name \
                     
+ '_' + self.rndpart +'.txt'
         return self._file
     @file.setter
     def file(self,val):
         self._file=val
         return self._file
 
 
 
     # Relocate i.e. change parent. 
     # probably this could be automated
     # by using properties
[docs]
     def adopt(self,parent=None,**kwargs):
         '''Redefine the parent entity of the file. Afrects where the file is written during the simulation
         '''
         if parent==None:
             self.print_log(type='F', msg='Parent must be given for relocation')
         self.parent=parent
         if hasattr(self.parent,'iofile_bundle'):
             self.parent.iofile_bundle.new(name=self.name,val=self) 
 
     # File writing
[docs]
     def write(self,**kwargs):
         '''Method to write the file
         Sets the 'dir' attribute to 'in', because only input files are written.
         
         Parameters
         ----------
         
         **kwargs:
             data: numpy_array, self.Data
                Data to be written.       
             datatype: str, self.Datatype
                Datatype of the data.
             iotype: str, self.iotype
                IO type of the IO file.
         '''
         self.dir='in'  # Only input files are written
         #Parse the rows to split complex numbers
         data=kwargs.get('data',self.Data)
         datatype=kwargs.get('datatype',self.datatype)
         iotype=kwargs.get('iotype',self.iotype)
         header_line = []
         parsed=[]
         # Default is the data file
         if iotype=='sample':
             for i in range(data.shape[1]):
                 if i==0:
                    if np.iscomplex(data[0,i]) or np.iscomplexobj(data[0,i]) :
                        parsed=np.r_['1',np.real(data[:,i]).reshape(-1,1),
                                np.imag(data[:,i].reshape(-1,1))]
                        header_line.append('%s_%s_Real' %(self.name,i))
                        header_line.append('%s_%s_Imag' %(self.name,i))
                    else:
                        parsed=np.r_['1',data[:,i].reshape(-1,1)]
                        header_line.append('%s_%s' %(self.name,i))
                 else:
                    if np.iscomplex(data[0,i]) or np.iscomplexobj(data[0,i]) :
                        parsed=np.r_['1',parsed,np.real(data[:,i]).reshape(-1,1),
                                np.imag(data[:,i].reshape(-1,1))]
                        header_line.append('%s_%s_Real' %(self.name,i))
                        header_line.append('%s_%s_Imag' %(self.name,i))
                    else:
                        parsed=np.r_['1',parsed,data[:,i].reshape(-1,1)]
                        header_line.append('%s_%s' %(self.name,i))
 
             # Numbers are printed as intergers
             # These are verilog related, do not belong here
             if datatype in [ 'int', 'sint', 'complex', 'scomplex' ]:
                 df=pd.DataFrame(parsed,dtype='int')
             else:
                 df=pd.DataFrame(parsed,dtype=datatype)
 
             if self.hasheader:
                 df.to_csv(path_or_buf=self.file,sep="\t",
                         index=False,header=header_line)
             else:
                 df.to_csv(path_or_buf=self.file,sep="\t",
                         index=False,header=False)
         # Control file is a different thing
         elif iotype=='event':
             for i in range(data.shape[1]):
                 if i==0:
                    if np.iscomplex(data[0,i]) or np.iscomplexobj(data[0,i]) :
                        self.print_log(type='F', msg='Timestamp can not be complex.')
                    else:
                        parsed=np.r_['1',data[:,i].reshape(-1,1)]
                        header_line.append('Timestamp')
                 else:
                    if np.iscomplex(data[0,i]) or np.iscomplexobj(data[0,i]) :
                        parsed=np.r_['1',parsed,np.real(data[:,i]).reshape(-1,1),
                                np.imag(data[:,i].reshape(-1,1))]
                        header_line.append('%s_%s_Real' %(self.name,i))
                        header_line.append('%s_%s_Imag' %(self.name,i))
                    else:
                        parsed=np.r_['1',parsed,data[:,i].reshape(-1,1)]
                        header_line.append('%s_%s' %(self.name,i))
 
             if datatype in [ 'int', 'sint', 'complex', 'scomplex' ]:
                 df=pd.DataFrame(parsed,dtype='int')
             else:
                 df=pd.DataFrame(parsed,dtype=datatype)
             if self.hasheader:
                 df.to_csv(path_or_buf=self.file,sep="\t",index=False,header=header_line)
             else:
                 df.to_csv(path_or_buf=self.file,sep="\t",index=False,header=False)
         # Flush cached file system writes
         with open(self.file) as fd:
             os.fsync(fd) 
         
     # Reading
[docs]
     def read(self,**kwargs):
         ''' Method to read the file
         Parameters
         ----------
         **kwargs:
            datatype: str, self.datatype
                Controls if the data is read in as complex or real
            dtype: str, 'object'
                The datatype of the actual file. Default is object, 
                i.e data is first read to internal variable as string. 
                This is a help parameter to give more control over reading.
         '''
         fid=open(self.file,'r')
         self.datatype=kwargs.get('datatype',self.datatype)
         dtype=kwargs.get('dtype',object)
         try:
            readd = pd.read_csv(fid,dtype=dtype,sep='\t',header=None)
            #read method for complex signal matrix
            if self.datatype == 'complex' or self.datatype == 'scomplex':
                self.print_log(type="I", msg="Reading complex")
                rows=int(readd.values.shape[0])
                cols=int(readd.values.shape[1]/2)
                for i in range(cols):
                    if i==0:
                        self.Data=np.zeros((rows, cols),dtype=complex)
                        self.Data[:,i]=readd.values[:,2*i].astype('int')\
                                
+1j*readd.values[:,2*i+1].astype('int')
                    else:
                        self.Data[:,i]=readd.values[:,2*i].astype('int')\
                                
+1j*readd.values[:,2*i+1].astype('int')
    
            else:
                self.Data=readd.values
         except pd.errors.EmptyDataError:
            # File was empty
            self.print_log(type="W", msg="IOFile was empty! %s" %(self.file))
            self.Data = None
         fid.close() 
 
     # Remove the file when no longer needed
[docs]
     def remove(self):
         '''Remove the file
         '''
         if self.preserve:
             self.print_log(type="I", msg="Preserve_value is %s" %(self.preserve))
             self.print_log(type="I", msg="Preserving file %s" %(self.file))
         else:
             try:
                 os.remove(self.file)
             except:
                 pass