import ctypes
import os
import numpy as np

class _OpaqueStruct(ctypes.Structure):
    """ creates a ctypes opaque struct type """
    pass

DDCFileHandle = ctypes.POINTER(_OpaqueStruct)          # ctypes file handle type.  nilibddc.h defines this as a typedef for a pointer to an opaque struct
DDCChannelGroupHandle = ctypes.POINTER(_OpaqueStruct)  # ... group handle type
DDCChannelHandle = ctypes.POINTER(_OpaqueStruct)       # ... channel handle type

class TDMSFile:

    def __init__(self, bin_path):
        os.environ['PATH'] += os.pathsep + bin_path   # put nilibddc.dll on the OS search path
        self.windll = ctypes.WinDLL("nilibddc.dll")   # __stdcall functions
        self.file_handle = None       

    def open(self, file_name):
        """ open the file and store the handle """
        c_function = self.windll.DDC_OpenFileEx                # int __stdcall DDC_OpenFileEx(const char *filePath, const char *fileType, int readOnly, DDCFileHandle *file);
        file_path = ctypes.c_char_p(file_name.encode())
        file_handle = DDCFileHandle()
        rc = c_function(file_path, ctypes.c_char_p(None), 1, ctypes.byref(file_handle))  # 3rd argument = 1 --> open in read-only mode
        if rc < 0:
            print ("DDC error while opening the file. Code", rc, ":", self._get_error_description(rc))

        self.file_handle = file_handle

    def close(self):
        """ close the file"""
        c_function = self.windll.DDC_CloseFile   # int __stdcall DDC_CloseFile(DDCFileHandle file);
        rc = c_function(self.file_handle)

    def read(self):     # assuming 1 group w/multiple channels
        """ read the data and return it in a dict"""

        n_groups, group_handles = self._read_group_handles()

        group_h = group_handles[0]
        print("group name:", self._read_group_property(group_h, property_string = "name"))
        nchannels, channel_handles = self._read_channel_handles(group_h)
        channel_data = {}
        for channel_h in channel_handles:
            name = self._read_channel_property(channel_h, "name")
            units = self._read_channel_property(channel_h, "unit_string")
            data, period, npoints, dtype = self._read_channel_data(channel_h)
            channel_data[name]=data
            print("channel:",name,"units:",units,"nb points:",npoints,"type:",dtype)

        return channel_data, period, npoints

    def _read_channel_data(self, channel_handle):
        """ read the data in a float64 channel into a numpy array"""
        
        # read the number of data points
        c_function = self.windll.DDC_GetNumDataValues       # int __stdcall DDC_GetNumDataValues(DDCChannelHandle channel, unsigned __int64 *numValues);
        npoints_c = ctypes.c_ulonglong()
        rc = c_function(channel_handle, ctypes.byref(npoints_c))
        npoints = npoints_c.value

        # read the data type
        c_function = self.windll.DDC_GetDataType            # int DDC_GetDataType (DDCChannelHandle channel, DDCDataType *dataType);
        dtype_c = ctypes.c_int()
        rc = c_function(channel_handle,ctypes.byref(dtype_c))
        dtype = dtype_c.value                          # dtype is a key into the data type enum key defined in nilibddc.h line 43

        # read the sampling period
        c_function = self.windll.DDC_GetChannelPropertyDouble
        property_c = ctypes.c_char_p("wf_increment".encode())
        period_c = ctypes.c_double()
        rc = c_function(channel_handle, property_c, ctypes.byref(period_c))
        period = period_c.value

        # read the data
        c_function = self.windll.DDC_GetDataValues          # int __stdcall DDC_GetDataValues(DDCChannelHandle channel, size_t indexOfFirstValueToGet, size_t numValuesToGet, void *values);
        data = np.zeros(npoints, np.dtype("float64"))
        contiguous_data = np.ascontiguousarray(data)
        data_c = contiguous_data.ctypes.data_as(ctypes.c_void_p)
        rc = c_function(channel_handle, 0, npoints, data_c)
        
        return data, period, npoints, dtype

    def _read_group_handles(self):
        """ read the array of group handles """

        # read the number of groups
        c_function = self.windll.DDC_GetNumChannelGroups             # int __stdcall DDC_GetNumChannelGroups(DDCFileHandle file, unsigned int *numChannelGroups);
        ngroups_c = ctypes.c_uint()
        rc = c_function(self.file_handle, ctypes.byref(ngroups_c)) 
        ngroups = ngroups_c.value

        # read the array of group handles
        c_function = self.windll.DDC_GetChannelGroups                # int __stdcall DDC_GetChannelGroups(DDCFileHandle file, DDCChannelGroupHandle channelGroupsBuf[], size_t numChannelGroups);
        group_handles = (DDCChannelGroupHandle * ngroups)()     # array of group handles
        rc = c_function(self.file_handle, group_handles, ngroups)

        return ngroups, group_handles

    def _read_channel_handles(self, group_handle):
        """ read the array of channel handles """

        # read the number of channels
        c_function = self.windll.DDC_GetNumChannels      # int __stdcall DDC_GetNumChannels(DDCChannelGroupHandle channelGroup, unsigned int *numChannels);
        nchannels_c = ctypes.c_uint()
        rc = c_function(group_handle, ctypes.byref(nchannels_c))
        nchannels = nchannels_c.value

        # read the array of channel handles
        c_function = self.windll.DDC_GetChannels                 # int __stdcall DDC_GetChannels(DDCChannelGroupHandle channelGroup,DDCChannelHandle channelsBuf[],size_t numChannels);
        channel_handles = (DDCChannelHandle * nchannels)()  # array of channel handles
        rc = c_function(group_handle, channel_handles, nchannels)

        return nchannels, channel_handles

    def _read_file_property(self, property_string):
        """ read a certain file-level property --- nb: the value must be a string"""

        property_c = ctypes.c_char_p(property_string.encode())

        # read the property's length
        c_function = self.windll.DDC_GetFileStringPropertyLength     # int __stdcall DDC_GetFileStringPropertyLength(DDCFileHandle file, const char *property, unsigned int *length)
        length_c = ctypes.c_uint()
        rc = c_function(self.file_handle, property_c , ctypes.byref(length_c))
        length = length_c.value

        # read the property's value
        c_function = self.windll.DDC_GetFileProperty                 # int __stdcall DDC_GetFileProperty(DDCFileHandle file, const char *property, void *value, size_t valueSizeInBytes);
        string_c = ctypes.create_string_buffer(length + 1)
        rc = c_function(self.file_handle, property_c, string_c, length +1)

        return string_c.value.decode()

    def _read_group_property(self, group_handle, property_string):
        """ read a certain group-level property --- nb: the value must be a string"""

        property_c = ctypes.c_char_p(property_string.encode())

        # read the property's length    
        c_function = self.windll.DDC_GetChannelGroupStringPropertyLength  # int __stdcall DDC_GetChannelGroupStringPropertyLength(DDCChannelGroupHandle channelGroup, const char *property, unsigned int *length);
        length_c = ctypes.c_uint()
        rc = c_function(group_handle, property_c , ctypes.byref(length_c))
        length = length_c.value

        # read the property's value
        c_function = self.windll.DDC_GetChannelGroupProperty              # int __stdcall DDC_GetChannelGroupProperty(DDCChannelGroupHandle channelGroup,const char *property,void *value,size_t valueSizeInBytes);
        string_c = ctypes.create_string_buffer(length + 1)
        rc = self.windll.DDC_GetChannelGroupProperty(group_handle, property_c, string_c, length + 1)     #int __stdcall DDC_GetChannelGroupProperty(DDCChannelGroupHandle channelGroup,const char *property,void *value,size_t valueSizeInBytes);

        return string_c.value.decode()

    def _read_channel_property(self, channel_handle, property_string):
        """ read a certain group-level property --- nb: the value must be a string"""
        
        property_c = ctypes.c_char_p(property_string.encode())

        # read the property's length
        c_function = self.windll.DDC_GetChannelStringPropertyLength   # int __stdcall DDC_GetChannelStringPropertyLength(DDCChannelHandle channel, const char *property, unsigned int *length);
        length_c = ctypes.c_uint()
        rc = c_function(channel_handle, property_c , ctypes.byref(length_c))
        length = length_c.value

        # read the property's value
        c_function = self.windll.DDC_GetChannelProperty              #int __stdcall DDC_GetChannelProperty(DDCChannelHandle channel,const char *property,void *value,size_t valueSizeInBytes); 
        string_c = ctypes.create_string_buffer(length + 1)
        rc = c_function(channel_handle, property_c, string_c, length + 1)

        return string_c.value.decode()

    def _get_error_description(self, error_code):
        """ return the DDC error string """
        cfunc = self.windll.DDC_GetLibraryErrorDescription    # const char * __stdcall DDC_GetLibraryErrorDescription(int errorCode);
        cfunc.restype = ctypes.c_char_p
        return cfunc(error_code).decode()

