# -*- coding: utf-8 -*-
from __future__ import print_function

from collections import OrderedDict

import numpy as np
import six

from acq4.devices.DAQGeneric.taskGUI import DAQGenericTaskGui
from acq4.devices.Device import Device, DeviceTask
from pyqtgraph import siFormat
from pyqtgraph.debug import Profiler
from acq4.util import Qt
from acq4.util.Mutex import Mutex
from acq4.util.debug import printExc
from pyqtgraph.metaarray import MetaArray, axis

Ui_Form = Qt.importTemplate('.DeviceTemplate')


class DataMapping:
    """Class that maps values between the voltages on a DAQ channel and the physically measured units.
    By default, this class applies a simple linear scale and offset for analog channels. Digital channels
    may optionally be inverted.
        Vout = Value * scale - offset
        Value = (Vin + offset) * scale
    This class may be subclassed to allow any arbitrary mapping (eg, calibration curves, etc.)
    """

    def __init__(self, device, chans=None):
        """When mapping initializes, it immediately grabs the scale and offset for each channel
        specified in chans (or all channels if None). This means that the mapping is only valid
        as long as these values have not changed."""
        self.device = device
        self.scale = {}
        self.offset = {}
        if chans is None:
            chans = device.listChannels()
        if isinstance(chans, six.string_types):
            chans = [chans]
        for ch in chans:
            self.scale[ch] = device.getChanScale(ch)
            self.offset[ch] = device.getChanOffset(ch)

    def mapToDaq(self, chan, data):
        scale = self.scale[chan]
        offset = self.offset[chan]
        return (data * scale) - offset

    def mapFromDaq(self, chan, data):
        scale = self.scale[chan]
        offset = self.offset[chan]
        return (data + offset) * scale


class ChannelHandle(object):
    def __init__(self, dev, channel):
        self.dev = dev
        self.channel = channel


class DAQGeneric(Device):
    """
    Config format:
        channels:
            ChannelName1:
                device: 'DaqDeviceName'
                channel: '/Dev1/ao0'
                type: 'ao'
                units: 'A'
                scale: 200 * mV / nA
            ChannelName2:
                device: 'DaqDeviceName'
                channel: '/Dev1/ai3'
                type: 'ai'
                mode: 'nrse'
                units: 'A'
                scale: 200 * nA / mV
            ChannelName3:
                device: 'DaqDeviceName'
                channel: '/Dev1/line7'
                type: 'di'
                invert: True
        
    """
    sigHoldingChanged = Qt.Signal(object, object)

    def __init__(self, dm, config, name):
        Device.__init__(self, dm, config, name)
        self._DGLock = Mutex(Qt.QMutex.Recursive)  ## protects access to _DGHolding, _DGConfig
        ## Do some sanity checks here on the configuration

        # 'channels' key is expected; for backward compatibility we just use the top-level config.
        config = config.get('channels', config)
        self._DGConfig = config
        self._DGHolding = {}
        for ch in config:
            if config[ch]['type'][0] != 'a' and ('scale' in config[ch] or 'offset' in config[ch]):
                raise Exception("Scale/offset only allowed for analog channels. (%s.%s)" % (name, ch))

            if 'scale' not in config[ch]:
                config[ch]['scale'] = 1  ## must be int to prevent accidental type conversion on digital data
            if 'offset' not in config[ch]:
                config[ch]['offset'] = 0
            if config[ch].get('invert', False):
                if config[ch]['type'][0] != 'd':
                    raise Exception("Inversion only allowed for digital channels. (%s.%s)" % (name, ch))
                config[ch]['scale'] = -1
                config[ch]['offset'] = -1

            # print "chan %s scale %f" % (ch, config[ch]['scale'])
            if 'holding' not in config[ch]:
                config[ch]['holding'] = 0.0

            ## It is possible to create virtual channels with no real hardware connection
            if 'device' not in config[ch]:
                # print "Assuming channel %s is virtual:" % ch, config[ch]
                config[ch]['virtual'] = True

            ## set holding value for all output channels now
            if config[ch]['type'][1] == 'o':
                self.setChanHolding(ch, config[ch]['holding'])
            # self._DGHolding[ch] = config[ch]['holding']

        dm.declareInterface(name, ['daqChannelGroup'], self)
        for ch in config:
            dm.declareInterface(name + "." + ch, ['daqChannel'], ChannelHandle(self, ch))

    def mapToDAQ(self, channel, data):
        mapping = self.getMapping(chans=[channel])
        return mapping.mapToDaq(channel, data)

    def mapFromDAQ(self, channel, data):
        mapping = self.getMapping(chans=[channel])
        return mapping.mapFromDaq(channel, data)

    def getMapping(self, chans=None):
        return DataMapping(self, chans)

    def createTask(self, cmd, parentTask):
        return DAQGenericTask(self, cmd, parentTask)

    def getConfigParam(self, param):
        return self._DGConfig.get(param, None)

    def setChanHolding(self, channel, level=None, block=True, mapping=None):
        """Define and set the holding values for this channel
        If *block* is True, then return only after the value has been set on the DAQ.
        If *block* is False, then simply schedule the change to take place when the DAQ is available.
        *mapping* is a DataMapping object which tells the device how to translate *level* into
            a voltage on the physical DAQ channel. If *mapping* is None, then it will use self.getMapping(*channel*)
            to determine the correct mapping.
        """
        prof = Profiler(disabled=True)
        with self._DGLock:
            prof('lock')
            # print "set holding", channel, level
            ### Set correct holding level here...
            if level is None:
                level = self._DGHolding[channel]
                if level is None:
                    raise Exception("No remembered holding level for chann
el %s" % channel)
            else:
                self._DGHolding[channel] = level

            if mapping is None:
                mapping = self.getMapping(channel)
    
        val = mapping.mapToDaq(channel, self._DGHolding[channel])
            prof('map')
            # print "Set holding for channel %s: %f => %f" % (channel, self._DGHolding[channel], val)

            chConf = self._DGConfig[channel]
            isVirtual = chConf.get('virtual', False)
            if not isVirtual:
                daq = chConf['device']
                chan = chConf['channel']
                daqDev = self.dm.getDevice(daq)
            prof('get dev')

        ## release DGLock before setChannelValue
        if not isVirtual:
            if block:
                daqDev.setChannelValue(chan, val, block=True)
            else:
                daqDev.setChannelValue(chan, val, block=False,
                                       delaySetIfBusy=True)  ## Note: If a task is running, this will not be set until it completes.
        prof('set channel value')
        self.sigHoldingChanged.emit(channel, level)
        prof('emit')

    def getChanHolding(self, chan):
        with self._DGLock:
            return self._DGHolding[chan]

    def getChannelValue(self, channel, block=True, raw=False):
        with self._DGLock:
            daq = self._DGConfig[channel]['device']
            chan = self._DGConfig[channel]['channel']
            mode = self._DGConfig[channel].get('mode', None)

        ## release _DGLock before getChannelValue
        daqDev = self.dm.getDevice(daq)
        val = daqDev.getChannelValue(chan, mode=mode, block=block)
        if not raw:
            return self.mapFromDAQ(channel, val)
        else:
            return val

    def reconfigureChannel(self, chan, config):
        """Allows reconfiguration of channel properties (including the actual DAQ channel name)"""
        with se