# -*- coding: utf-8 -*-
"""
Provide Python access via ctypes to ACCES's mPCIe-AIO Family DAQ devices installed using the AIOAIO.dll (Windows) library.
"""
from threading import Thread
from typing import *
from enum import IntEnum
from ctypes import *
AIO = cdll.LoadLibrary("aioaio")

ERROR_SUCCESS = 0
biAuto = 0
biNone = -1
ciAll = 0xFFFF_FFFF


class Range(IntEnum):
    NoChange = 0xFFFFFFFF,
    Unipolar10 = 0x30313055,  # FourCC("U010")
    Unipolar5 = 0x35303055,  # FourCC("U005")
    Bipolar10 = 0x3031E142,  # FourCC("B±10")
    Bipolar5 = 0x3530E142,  # FourCC("B±05")
    Bipolar2_5 = 0xBD32E142,  # FourCC("B±2½")
    Current420 = 0x30323449,  # FourCC("I420") # 4-20mA
    Code0 = 0
    Code1 = 1
    Code2 = 2
    Code3 = 3
    Code4 = 4
    Code5 = 5
    Code6 = 6
    Code7 = 7


class Sequencer(IntEnum):
    First = 0
    Second = 1


class IRQIndex(IntEnum):
    Only = 0


class ChunkingType(IntEnum):
    Auto = 0
    Size = 1
    Rate = 1


class CallbackType(IntEnum):
    Raw = 0
    Volts = 1
    Current = 2


def BRD_GetHighestBoardIndex():
    """Return the number of detected AIOAIO-using cards

    Returns:
        int : Highest Board Index, or -1 if no boards are detected
    """
    return AIO.BRD_GetHighestBoardIndex()  # UInt64


def BRD_GetName(iBoard: int):
    """Return the status & Unicode name of the device

    Args:
        iBoard (UInt64): index of the board to query

    Returns:
        (int, str): status, ModelName
    """
    namesize = c_ulong(50)
    actualNameSize = c_ulong(0)
    nameBuf = create_string_buffer(50)
    status = AIO.BRD_GetNameA(iBoard, namesize, nameBuf, byref(actualNameSize))
    return status, nameBuf.value.strip().decode()


def BRD_GetSerialNumber(iBoard: int):
    """Return the status & Serial Number of the device (SN is NYI)"""
    SN = c_ulonglong(0)
    status = AIO.BRD_GetSerialNumber(iBoard, byref(SN))
    return status, SN


def BRD_Reset(iBoard: int):
    """Perform a complete reset to power-on initial state of the specified device"""
    return AIO.BRD_Reset(iBoard)


def DAC_OutputCounts(iBoard: int,  ch: int=ciAll,  raw=0xFFFFFFFFFFFFFFFF):
    """Write a raw count value to one or all DACs"""
    return AIO.DAC_OutputCounts(iBoard, ch, raw)


def DAC_OutputV(iBoard: int,  ch:int=ciAll,  V:float=0.0):
    """Write a voltage to one or all DACs; requires that DAC_SetRangexxxx has been called"""
    return AIO.DAC_OutputV(iBoard, ch, V)


def DAC_SetRange1(iBoard: int,  ch: int=ciAll,  rangeCode: int = Range.NoChange):
    """Sets the output range (gain/polarity) of one or all DACs to the specified range"""
    return AIO.DAC_SetRange1(iBoard, ch, rangeCode)


def DAC_SetRangeAll(iBoard: int, rangeCodes: List[Range]):
    """Sets the output range (gain/polarity) of each DAC"""
    return AIO.DAC_SetRangeAll(iBoard, rangeCodes, rangeCodes)


def ADC_GetImmediateV(iBoard: int, iSequencer:Sequencer, ch: int, range: Range=Range.NoChange):
    """Acquires a single conversion result from the specified input channel; optionally: override the configured range"""
    Volts = c_double(0)
    status = AIO.ADC_GetImmediateV(iBoard, iSequencer, byref(Volts), ch, range)
    return status, Volts.value


def ADC_AcquireEnd(iBoard: int, iSequencer):
    return AIO.ADC_AcquireEnd(iBoard, iSequencer)


def ADC_AcquireBlock(iBoard: int, iSequencer, sampleCt):
    """ """
    return AIO.ADC_AcquireBlock(iBoard, iSequencer, sampleCt)


def ADC_AcquireStream(iBoard: int, iSequencer):
    """ """
    return AIO.ADC_AcquireBlock(iBoard, iSequencer)


def ADC_ClearData(iBoard: int, iSequencer):
    """ """
    return AIO.ADC_ClearData(iBoard, iSequencer)


# chunk is in Hz or Samples
def ADC_ConfigureAcquire(iBoard: int, iSequencer, chunkingType, chunkAmount):
    """ """
    return AIO.ADC_ConfigureAcquire(iBoard, iSequencer, chunkingType, chunkAmount)


def ADC_GetDataRaw(iBoard: int, iSequencer, dataCt):
    """ """
    rawdata = (c_long * dataCt)()
    gotDataCount = c_long(0)
    status = AIO.ADC_GetDataRaw(
        iBoard, iSequencer, dataCt, byref(rawdata), byref(gotDataCount))
    return status, rawdata, gotDataCount.value


def ADC_GetDataCounts(iBoard: int, iSequencer, dataCt):
    """ """
    dataBuf = (c_short * dataCt)()
    statusBuf = (c_short * dataCt)()
    gotDataCount = c_long(0)
    status = AIO.ADC_GetDataCounts(iBoard, iSequencer, dataCt, byref(
        dataBuf), byref(statusBuf), byref(gotDataCount))
    return status, dataBuf, statusBuf, gotDataCount.value


def ADC_GetDataV(iBoard: int, iSequencer, dataCt):
    """ """
    dataBuf = (c_double * dataCt)()
    statusBuf = (c_short * dataCt)()
    gotDataCount = c_long(0)
    status = AIO.ADC_GetDataV(iBoard, iSequencer, dataCt, byref(
        gotDataCount), byref(dataBuf), byref(statusBuf), byref(gotDataCount))
    return status, dataBuf, statusBuf, gotDataCount.value


def ADC_PollData(iBoard: int, iSequencer):
    """ """
    return AIO.ADC_PollData(iBoard, iSequencer)


def ADC_GetRate(iBoard: int, iSequencer):
    """ """
    Hz = c_double(0)
    status = AIO.ADC_GetRate(iBoard, iSequencer, byref(Hz))
    return status, Hz.value


def ADC_SetRate(iBoard: int, iSequencer,  Hz):
    return AIO.ADC_SetRate(iBoard, iSequencer, c_double(Hz))


def ADC_SetCallback(iBoard: int, iSequencer, callbackType, callback):
    """ """


def ADC_SetChannelCount(iBoard: int, iSequencer,  numChannels):
    """ """
    return AIO.ADC_SetChannelCount(iBoard, iSequencer, numChannels)


def ADC_SetRange1(iBoard: int, iSequencer, channel=ciAll,  range=Range.Bipolar10):
    """ """
    return AIO.ADC_SetRange1(iBoard, iSequencer, channel, range)


def ADC_SetRangeAll(iBoard: int, iSequencer, rangeCt,  ranges):
    """ """
    return AIO.ADC_SetRangeAll(iBoard, iSequencer, channel, ranges)


def PIO_In8(cardNum: int, offset):
    """Reads a byte register"""
    return AIO.PIO_In8(cardNum, offset)


def PIO_In16(cardNum: int, offset):
    """Reads a UInt16 register"""
    return AIO.PIO_In16(cardNum, offset)


def PIO_In32(cardNum: int, offset):
    """Reads a UInt32 register"""
    return AIO.PIO_In32(cardNum, offset)


def PIO_Out8(cardNum: int, offset, data):
    """Writes a byte register"""
    return AIO.PIO_Out8(cardNum, offset, c_ubyte(data))


def PIO_Out16(cardNum: int, offset, data):
    """Writes a UInt16 register"""
    return AIO.PIO_Out16(cardNum, offset, c_ushort(data))


def PIO_Out32(cardNum: int, offset, data):
    """Writes a UInt32 register"""
    return AIO.PIO_Out32(cardNum, offset, c_ulong(data))

# function PIO_INS32(iBoard: TBoardIndex; Offset: TUInt32; DataCt: TUInt32; pData: TPUInt32): TWinErrCode; cdecl;


def PIO_INS8(cardNum, offset, dataCt):
    dataBuf = (c_uint8*dataCt)()
    status = AIO.PIO_INS8(cardNum, offset, dataCt, byref(dataBuf))
    return status, dataBuf

def PIO_INS16(cardNum, offset, dataCt):
    dataBuf = (c_uint16*dataCt)()
    status = AIO.PIO_INS16(cardNum, offset, dataCt, byref(dataBuf))
    return status, dataBuf

def PIO_INS32(cardNum, offset, dataCt):
    dataBuf = (c_uint32*dataCt)()
    status = AIO.PIO_INS32(cardNum, offset, dataCt, byref(dataBuf))
    return status, dataBuf


def PIO_WaitForIRQ(iBoard, irq=IRQIndex.Only):
    """
    This function deadlocks the calling thread until an IRQ occurs or the request is aborted.

    It's intended for multi-threaded IRQ handling, where the calling application spawns a separate thread to service IRQs quickly.
    Returns:
        nonzero if it returned because an IRQ occurred 
        Zero if it returned because the wait was aborted or there was an error. If the wait was aborted, GetLastError() will return ERROR_OPERATION_ABORTED; If someone is already waiting for an IRQ on that card when you call WaitForIRQ() it will return ERROR_INVALID_FUNCTION. (Other errors will return other error codes.)    
    """
    return AIO.PIO_WaitForIRQ(iBoard, irq)


def PIO_PollForIRQ(iBoard, irq=IRQIndex.Only):
    """
    This function checks if an IRQ is pending.

    Returns:        

    """
    return AIO.PIO_PollForIRQ(iBoard, irq)


def displayBoardInfo(index):
    status, name = BRD_GetName(index)
    print(index, status, name)
    print("Card found at cardNum {0} = {1}".format(index, name))


if __name__ == "__main__":  # main code section
    print("AIOAIO Python introductory sample using ctypes")

    highestIndex = BRD_GetHighestBoardIndex()

    if highestIndex == biNone:
        _ = input("No valid AIOAIO devices were detected.\nPlease confirm your device is installed\nand the drivers are installed correctly (check Device Manager).")
        exit()

    boardIndex = 0
    while True:
        status, name = BRD_GetName(boardIndex)
        print("Board found at boardIndex 0 is", name)
        boardIndex = boardIndex + 1
        if boardIndex > highestIndex:
            break


# Each of the following If blocks is effectively a distinct sample program demonstrating different techniques for acquiring data

    if True:
        # ADC_GetImmediateV sample code
        for ch in range(8):
            # acquire from the first sequencer, channels 0-7
            status, V = ADC_GetImmediateV(0, 0, ch, Range.Bipolar10)
            # acquire from the second sequencer, channels 8-15
            status2, V2 = ADC_GetImmediateV(0, 1, ch, Range.Bipolar10)
            print(
                f"CH {ch:2d} voltage: {V:10F}V status= {status:8X}\tCH {ch+8:2d} voltage: {V2:10F}V status= {status2:8X}")

    if False:
        # ADC_AcquireBlock() sample code
        status = BRD_Reset(0)
        print(f"BRD_Reset() returned {status}")

        status = ADC_SetRange1(0, 0, ciAll, Range.Bipolar10)
        print(f"ADC_SetRange1() returned {status}")

        status = ADC_SetChannelCount(0, 0, 1)
        print(f"ADC_SetChannelCount() returned {status}")

        targetHz = 1_000_000
        status = ADC_SetRate(0, 0, targetHz)
        print(f"ADC_SetRate() returned {status}")
        status, Hz = ADC_GetRate(0, 0)
        print(f"ADC_GetRate() returned {status}")
        print(
            f"Acquiring at {Hz}, asked for {targetHz} (error of {abs((1-targetHz/Hz)*100):F}%)")

        status = ADC_ConfigureAcquire(0, 0, ChunkingType.Size, 10)
        print(f"ADC_ConfigureAcquire() returned {status}")

        # acquire 20 samples from board 0 sequencer 0
        status = ADC_AcquireStream(0, 0)
        print(f"ADC_AcquireBlock() returned {status}")

        input("wait a bit then hit ENTER")

        if ADC_PollData(0, 0) != 0x00:
            print("Data is not yet available")

        status, count, dataBuf, statusBuf = ADC_GetDataV(0, 0, 20)
        print(
            f"ADC_GetDataV returned status={status}, count={count}, dataBuf={dataBuf[0]}")
        ADC_AcquireEnd(0, 0)

    if False:
        # ADC Continuous mode direct RelInPortL x 4000 mode
        # board reset
        RelOutPortB(0, 0x34, 0xff)
        # read base clock frequency
        BaseClock = RelInPortL(0, 0x18)
        print(f"BaseClock is {BaseClock}")
        iSequencer = 0
        GainCode = 1
        StartStopCode = 0xFCEF
        targetHz = 10000.0
        # BRD_Reset(0)
        RelOutPortL(0, 0x38 + 0x04*iSequencer, 0x11111111 *
                    GainCode)       # gain codes for 8 channels
        # divisor for ADC iSequencer
        RelOutPortL(0, 0x10 + 0x04*iSequencer, round(BaseClock/targetHz))
        # command to start on ADC Sequencer
        RelOutPort(0, 0x04+2*iSequencer, StartStopCode | 1)

        while True:
            FIFOdata = RelInPortL(0, 0x40 + 0x04*iSequencer)  # points in FIFO
            if FIFOdata == 0:
                print("no data in FIFO")
            if FIFOdata < 4000:
                continue

            for i in range(4000):
                # ADC FIFO data for ADC iSequencer
                ADData = RelInPortL(0, 0x20 + 0x4 * iSequencer)
                if ADData & 0x00000200:
                    continue
                Counts = ADData >> 16
                Volts = Counts / 65536 * 20 - 10
                if i % 100 == 0:
                    print(f"Volts = {Volts:F}")

    if False:
        # ADC Continuous mode direct PIO INSL 4000 mode
        RelOutPortB(0, 0x34, 0xff)  # BRD_Reset()
        BaseClock = RelInPortL(0, 0x18)  # read base clock frequency
        print(f"BaseClock is {BaseClock}")
        iSequencer = 0
        GainCode = 1
        StartStopCode = 0xFCEF
        targetHz = 10000.0
        # BRD_Reset(0)
        RelOutPortL(0, 0x38 + 0x04*iSequencer, 0x11111111 *
                    GainCode)            # gain codes for 8 channels
        # divisor for ADC iSequencer
        RelOutPortL(0, 0x10 + 0x04*iSequencer, round(BaseClock/targetHz))
        # command to start on ADC Sequencer
        RelOutPort(0, 0x04 + 2*iSequencer, StartStopCode | 1)

        while True:
            FIFOdata = RelInPortL(0, 0x40 + 0x04*iSequencer)  # points in FIFO
            if FIFOdata == 0:
                print("no data in FIFO")
            if FIFOdata < 4000:
                continue

            # ADC FIFO data for ADC iSequencer
            status, ADData = PIO_INS32(0, 0x20 + 0x4 * iSequencer, 4000)
            for i in range(4000):
                Counts = ADData[i] >> 16
                Volts = Counts / 65536 * 20 - 10
                if i % 100 == 0:
                    print(f"Volts = {Volts:F}")

    print("\nAIOAIO Boards found:", boardIndex)

    _ = input("\nDone. Press ENTER to exit.")
