# -*- coding: utf-8 -*-
# Part of the psychopy.iohub library.
# Copyright (C) 2012-2016 iSolver Software Solutions
# Distributed under the terms of the GNU General Public License (GPL).

from past.builtins import xrange
import sys

from ... import AnalogInputDevice, MultiChannelAnalogInputEvent
from .... import ioDeviceError, Computer, Device

from ctypes import *
from .constants import *
from .....errors import print2err, printExceptionDetailsToStdErr

currentSec = Computer.getTime


class AnalogInput(AnalogInputDevice):
    """The Measurement Computing Implementation for the ioHub AnalogInput
    Device type."""
    _DAQ_GAIN_OPTIONS = dict()
    _DAQ_GAIN_OPTIONS['BIP10VOLTS'] = BIP10VOLTS

    _SUPPORTED_OPTIONS = dict()
    _SUPPORTED_OPTIONS['DEFAULTOPTION'] = DEFAULTOPTION

    _SUPPORTED_MODELS = dict()
    _SUPPORTED_MODELS['USB-1208FS'] = 'USB-1208FS'
    _SUPPORTED_MODELS['USB-1616FS'] = 'USB-1616FS'

    _SAMPLE_BLOCK_TRANSFER_SIZE = dict()
    _SAMPLE_BLOCK_TRANSFER_SIZE['USB-1208FS'] = 31
    _SAMPLE_BLOCK_TRANSFER_SIZE['USB-1616FS'] = 62

    _DLL = None

    _newDataTypes = [('gain', 'i4'), ('options', 'i4')]

    __slots__ = [e[0] for e in _newDataTypes] + ['_memory_handle',
                                                 '_device_status',
                                                 '_sample_data_buffer',
                                                 '_input_sample_buffer_size',
                                                 '_current_sample_buffer_index',
                                                 '_samples_received_count',
                                                 '_last_sample_buffer_index',
                                                 '_local_sample_buffer',
                                                 '_local_sample_count_created',
                                                 '_last_start_recording_time_pre',
                                                 '_last_start_recording_time_post',
                                                 '_a2d_resolution']

    def __init__(self, *args, **kwargs):
        AnalogInputDevice.__init__(self, *args, **kwargs)

        self.channel_sampling_rate = c_long(self.channel_sampling_rate)
        self.device_number = int(self.device_number)
        self.device_number = c_int(self.device_number)

        if self.model_name not in self._SUPPORTED_MODELS:
            print2err(
                'AnalogInput Model %s is not supported. Supported models are %s, using model_name parameter.' %
                (self.model_name, str(
                    list(self._SUPPORTED_MODELS.keys())),))
            raise ioDeviceError(
                self, 'AnalogInput Model not supported: %s' %
                (self.model_name))

        if self.model_name in self._SAMPLE_BLOCK_TRANSFER_SIZE:
            self._input_sample_buffer_size = self._SAMPLE_BLOCK_TRANSFER_SIZE[
                self.model_name] * self.input_channel_count
        else:
            print2err(
                'AnalogInput Model %s has no block transfer size specified. Supported models are %s, using model_name parameter.' %
                (self.model_name, str(
                    list(self._SAMPLE_BLOCK_TRANSFER_SIZE.keys())),))
            raise ioDeviceError(
                self, 'AnalogInput Model not supported: %s' %
                (self.model_name))

        if self.gain in self._DAQ_GAIN_OPTIONS:
            self.gain = c_int(self._DAQ_GAIN_OPTIONS[self.gain])
        else:
            print2err(
                'AnalogInput gain value [%s] is not supported. Supported gain values are %s, using the gain parameter.' %
                (str(
                    list(self._DAQ_GAIN_OPTIONS.keys())),
                 ))
            raise ioDeviceError(
                self, 'AnalogInput gain not supported: %s' %
                (self.gain))

        if self.input_channel_count != 8:
            print2err('AnalogInput input_channel_count must be 8.')
            raise ioDeviceError(
                self, 'AnalogInput input_channel_count must be 8.')

        # load the MC DLL
        try:
            _DLL = windll.LoadLibrary('cbw32.dll')
        except Exception:
            _DLL = windll.LoadLibrary('cbw64.dll')

        AnalogInput._DLL = _DLL

        # get the MC API software version number
        _version = c_float(CURRENTREVNUM)
        _DLL.cbDeclareRevision(byref(_version))

        self.software_version = str(_version)

        # Initiate error handling
        # Parameters:
        #   PRINTALL :all warnings and errors encountered will be printed
        #   DONTSTOP :program will continue even if error occurs.
        #   Note that STOPALL and STOPFATAL are only effective in
        #   Windows applications, not Console applications.
        _DLL.cbErrHandling(c_int(PRINTALL), c_int(DONTSTOP))

        # get the analog input resolution of the device model.
        self._a2d_resolution = c_int(0)
        _DLL.cbGetConfig(
            c_int(BOARDINFO),
            self.device_number,
            0,
            c_int(BIADRES),
            byref(
                self._a2d_resolution))

        # set the device options based on the device model_name
        if self.model_name == 'USB-1208FS':
            self.options = NOCONVERTDATA + BACKGROUND + CONTINUOUS + CALIBRATEDATA
        elif self.model_name == 'USB-1616FS':
            self.options = BACKGROUND + CONTINUOUS

        # init AnalogInput device memory handle to 0
        self._memory_handle = 0

        # Currently AnalogInput device gets data from a fixed set of input channels:
        # Analog Inputs 0 - 7
        starting_channel_number = c_int(0)
        ending_channel_number = c_int(self.input_channel_count - 1)
        save_channels = range(
            starting_channel_number.value,
            ending_channel_number.value + 1)
        save_channels = tuple(save_channels)

        # initialize various counters and index values for use during data
        # collection
        self._current_sample_buffer_index = c_long(0)
        self._last_sample_buffer_index = c_long(0)
        self._samples_received_count = c_long(0)
        self._local_sample_count_created = 0

        # define a class to hold the local copy of sample data from the analog
        # input device.
        class AnalogInputSampleArray(Structure):
            _fields_ = [('low_channel', c_int),
                        ('high_channel', c_int),
                        ('save_channels', POINTER(c_int)),
                        ('input_channel_count', c_int),
                        ('save_channel_count', c_int),
                        ('count', c_int),
                        ('indexes', POINTER(c_uint)),
                        ('values', POINTER(c_uint16))]

            @staticmethod
            def create(low, high, save_channels, asize):
                dsb = AnalogInputSampleArray()
                dsb.indexes = (c_uint * asize)()
                dsb.values = (c_uint16 * asize)()
                dsb.count = asize
                dsb.low_channel = low
                dsb.high_channel = high
                dsb.input_channel_count = c_int(len(save_channels))
                dsb.save_channel_count = c_int(len(save_channels))
                dsb.save_channels = (
                    c_int *
                    dsb.save_channel_count)(
                    *
                    save_channels)
                return dsb

            def zero(self):
                for d in xrange(self.count):
                    self.indexes[d] = 0
                    self.values[d] = 0
                    # self.channels[d]=0

        self._local_sample_buffer = AnalogInputSampleArray.create(
            starting_channel_number,
            ending_channel_number,
            save_channels,
            self._input_sample_buffer_size)

        self._device_status = c_short(IDLE)

        self.enableEventReporting(False)
        if self.isReportingEvents():
            self.enableEventReporting(True)

    def enableEventReporting(self, enable):
        current = self.isReportingEvents()
        if current == enable:
            return current

        if AnalogInputDevice.enableEventReporting(self, enable) is True:
            try:
                # set sample buffers of the correct type and size based on the
                # a2d resolution
                if self._a2d_resolution.value > 12:
                    #ioHub.print2err("** Using cbWinBufAlloc for 16 bit card **")
                    self._memory_handle = self._DLL.cbWinBufAlloc(
                        c_int(self._input_sample_buffer_size))
                    self._sample_data_buffer = cast(
                        self._memory_handle, POINTER(c_uint16))
                else:
                    self._memory_handle = self._DLL.cbWinBufAlloc(
                        c_int(self._input_sample_buffer_size))
                    self._sample_data_buffer = cast(
                        self._memory_handle, POINTER(c_uint16))

                 # Make sure memory handle to sample buffer is a valid pointer
                if self._memory_handle == 0:
                    print2err('\nERROR ALLOCATING DAQ MEMORY: out of memory\n')
                    sys.exit(1)
            except Exception:
                print2err('------------- Error creating buffers -----------')
                printExceptionDetailsToStdErr()
                print2err('-------------------------------------------------')

            # set the device status to RUNNING while events are steaming
            self._device_status = c_short(RUNNING)

            # start streaming analog input data from the device
            self._last_start_recording_time_pre = currentSec()
            try:
                self._DLL.cbAInScan(self.device_number,
                                    self._local_sample_buffer.low_channel,
                                    self._local_sample_buffer.high_channel,
                                    c_int(self._input_sample_buffer_size),
                                    byref(self.channel_sampling_rate),
                                    self.gain,
                                    self._memory_handle,
                                    self.options)
            except Exception:
                print2err('------------- Error calling cbAInScan -----------')
                printExceptionDetailsToStdErr()
                print2err('-------------------------------------------------')
            self._last_start_recording_time_post = currentSec()

        else:
            # this should be taking board ID and AIFUNCTION
            self._DLL.cbStopBackground(self.device_number)
            # but when ever I give it second param ctypes throws
            # a `4 bytes too much`error
            self._device_status = c_short(IDLE)
            self._local_sample_buffer.zero()
            self._current_sample_buffer_index = c_long(0)
            self._last_sample_buffer_index = c_long(0)
            self._samples_received_count = c_long(0)
            self._local_sample_count_created = 0
            self._last_start_recording_time_pre = 0.0
            self._last_start_recording_time_post = 0.0

            if self._memory_handle != 0:
                result = self._DLL.cbWinBufFree(self._memory_handle)
                if result != 0:
                    print2err(
                        'ERROR calling cbWinBufFree TO FREE DAQ MEMORY: {0}'.format(result))

        return enable

    def _poll(self):
        if AnalogInputDevice._poll(self):
            return self._scanningPoll()
        else:
            return False

    def _scanningPoll(self):
        if self._device_status.value == RUNNING:
            self._DLL.cbGetStatus(
                self.device_number, byref(
                    self._device_status), byref(
                    self._samples_received_count), byref(
                    self._current_sample_buffer_index))  # ,AIFUNCTION)

            logged_time = currentSec()

            currentIndex = self._current_sample_buffer_index.value
            currentSampleCount = self._samples_received_count.value

            if currentSampleCount > 0 and currentIndex > 0:
                lastIndex = self._last_sample_buffer_index.value
                samples = self._local_sample_buffer

                if lastIndex != currentIndex:
                    self._last_sample_buffer_index = c_long(currentIndex)

                    if lastIndex > currentIndex:
                        for v in xrange(
                                lastIndex, self._input_sample_buffer_size):
                            self._saveScannedEvent(logged_time, samples, v)
                        lastIndex = 0

                    for v in xrange(lastIndex, currentIndex):
                        self._saveScannedEvent(logged_time, samples, v)
        else:
            print2err('Error: MC DAQ not responding. Exiting...')
            self.getConfiguration['_ioServer'].shutDown()
            sys.exit(1)

    def _saveScannedEvent(self, logged_time, samples, sample_index):
        sample_channel = self._local_sample_count_created % samples.input_channel_count
        samples.values[sample_index] = self._sample_data_buffer[sample_index]
        samples.indexes[
            sample_index] = self._local_sample_count_created / samples.input_channel_count
        if sample_channel == samples.input_channel_count - 1 and self.isReportingEvents():
            mce = self._createMultiChannelEventList(
                logged_time, samples, sample_index - sample_channel)
            self._addNativeEventToBuffer(mce)
        self._local_sample_count_created += 1

    def _createMultiChannelEventList(self, logged_time, samples, index):
        # For the AnalogInput device, sample time stamps are not
        # provided for the samples, but we will use the device_time field
        # to store a simulated device_time:
        #   = device sample number for analog input 0 / channel_sampling_rate
        device_time = float(
            samples.indexes[index]) / float(self.channel_sampling_rate.value)

        # ioHub time = device_time + ioHub time when scan start function
        # returned.
        time = device_time + self._last_start_recording_time_post

        # The confidence interval is set to the time taken for the start scan call to run,
        # since we do not know when during the start scan call samples actually
        # started being read by the device.
        confidence_interval = self._last_start_recording_time_post - \
            self._last_start_recording_time_pre

        # Delay is set to the time difference between the time the _poll method
        # was called that resulted in the event being created and the calculated
        # ioHub time.

        # TODO: Note that this timing logic assumes that samples start being created
        # by the device sometime **during** the call to the start scan function of
        # the device. This is an **assumption only**.
        # The actual delay from when the start scan method is called and when the first
        # sample event is received from the device should be checked and used if possible to
        # make the time attribute more accurate.
        delay = logged_time - time

        daqEvent = [0,    # exp id
                    0,              # session id
                    0,  # device id (not currently used)
                    Device._getNextEventID(),  # event id
                    MultiChannelAnalogInputEvent.EVENT_TYPE_ID,    # event type
                    device_time,   # device time
                    logged_time,  # logged time
                    time,       # hub time
                    confidence_interval,  # confidence interval
                    delay,        # delay
                    0,                       # filter_id
                    float(samples.values[index]),         # analog input 0
                    float(samples.values[index + 1]),       # analog input 1
                    float(samples.values[index + 2]),       # analog input 2
                    float(samples.values[index + 3]),       # analog input 3
                    float(samples.values[index + 4]),       # analog input 4
                    float(samples.values[index + 5]),       # analog input 5
                    float(samples.values[index + 6]),       # analog input 6
                    float(samples.values[index + 7])       # analog input 7
                    ]
        return daqEvent

    def _close(self):
        #/* The BACKGROUND operation must be explicitly stopped
        # Parameters:
        # BoardNum    :the number used by CB.CFG to describe this board
        # FunctionType: A/D operation (AIFUNCTION)*/

        # this should be taking board ID and AIFUNCTION
        # but when ever I give it second param ctypes throws
        # a `4 bytes too much`error
        try:
            ulStat = self._DLL.cbStopBackground(c_int32(self.device_number))
        except TypeError:
            ulStat = self._DLL.cbStopBackground(self.device_number)

        ulStat = self._DLL.cbWinBufFree(
            cast(self._memory_handle, POINTER(c_void_p)))

    def __del__(self):
        try:
            self._close()
        except Exception:
            pass
