# -*- coding: utf-8 -*-
"""
ALPHA VERSION, PRELIMINARY, MANY NOT-YET-IMPLEMENTED FUNCTIONS AND FEATURES, 
CONTACT ACCES I/O PRODUCTS FOR SUPPORT IF NEEDED
Provide Python access via ctypes to ACCES's PCI, PCI Express, ISA, PC/104, 
and related DAQ devices installed using the AIOWDM.dll (Windows) library.

Consult https://accesio.com/MANUALS/AIOWDM%20Software%20Reference.html for additional
information about the underlying functionality exposed by the API.
"""
from typing import *
from threading import Thread
from ctypes import *

AIOWDM = cdll.LoadLibrary("aiowdm")

# Constants
WDG_ACTION_IGNORE = 0
WDG_ACTION_DISABLE = 1
WDG_ACTION_SOFT_RESTART = 2
WDG_ACTION_MOSTLY_SOFT_RESTART = 4
PCI_WDG_CSM_RATE = 2.08333
P104_WDG_CSM_RATE = 2.08333
ISA_WDG_CSM_RATE = 0.894886

BAR0 = 0x80
BAR1 = 0x81
BAR2 = 0x82
BAR3 = 0x83
BAR4 = 0x84
BAR5 = 0x85

rcNoChange = 0xFFFFFFFF
rcUnipolar10 = 0x30313055  # "U010"
rcUnipolar5 = 0x35303055   # "U005"
rcBipolar10 = 0x3031E142   # "B±10"
rcBipolar5 = 0x3530E142    # "B±05"
rcBipolar2_5 = 0xBD32E142  # "B±2½"
rcCurrent420 = 0x30323449  # "I420"
rcCurrent020 = 0x30323049  # "I020"
rcCurrent024 = 0x34323049  # "I024"

rcCode0 = 0
rcCode1 = 1
rcCode2 = 2
rcCode3 = 3
rcCode4 = 4
rcCode5 = 5
rcCode6 = 6
rcCode7 = 7
rcCode8 = 8
rcCode9 = 9
rcCode10 = 10


// Digital Integration Features API Functions

def DIF_ConfigurePulseClock(cardNum: int, clockHz: float) -> int:
    """Configure the pulse clock frequency for the specified card."""
    clockHz_c = c_double(clockHz)
    AIOWDM.DIF_ConfigurePulseClock.argtypes = [c_uint, POINTER(c_double)]
    AIOWDM.DIF_ConfigurePulseClock.restype = c_uint
    result = AIOWDM.DIF_ConfigurePulseClock(cardNum, byref(clockHz_c))
    return result

def DIF_Measure(cardNum: int, iBit: int, loSide: float, hiSide: float, dutyCycle: float, frequency: float) -> int:
    """Measure the pulse characteristics of the specified card."""
    loSide_c, hiSide_c, dutyCycle_c, frequency_c = c_double(loSide), c_double(hiSide), c_double(dutyCycle), c_double(frequency)
    AIOWDM.DIF_Measure.argtypes = [c_uint, c_uint, POINTER(c_double), POINTER(c_double), POINTER(c_double), POINTER(c_double)]
    AIOWDM.DIF_Measure.restype = c_uint
    result = AIOWDM.DIF_Measure(cardNum, iBit, byref(loSide_c), byref(hiSide_c), byref(dutyCycle_c), byref(frequency_c))
    return result

def DIF_Generate(cardNum: int, iBit: int, activeSide: float, inactiveSide: float, pulseCount: int) -> int:
    """Generate a pulse with the specified characteristics."""
    AIOWDM.DIF_Generate.argtypes = [c_uint, c_uint, c_double, c_double, c_ushort]
    AIOWDM.DIF_Generate.restype = c_uint
    result = AIOWDM.DIF_Generate(cardNum, iBit, activeSide, inactiveSide, pulseCount)
    return result

def DIF_ConfigureGeneration(cardNum: int, iBit: int, activeSide: float, inactiveSide: float, pulseCount: int) -> int:
    """Configure pulse generation parameters."""
    AIOWDM.DIF_ConfigureGeneration.argtypes = [c_uint, c_uint, c_double, c_double, c_ushort]
    AIOWDM.DIF_ConfigureGeneration.restype = c_uint
    result = AIOWDM.DIF_ConfigureGeneration(cardNum, iBit, activeSide, inactiveSide, pulseCount)
    return result

def DIF_ConfigureCounting(cardNum: int, iBit: int, countFalling: bool, countRising: bool) -> int:
    """Configure the pulse counting mode."""
    AIOWDM.DIF_ConfigureCounting.argtypes = [c_uint, c_uint, c_bool, c_bool]
    AIOWDM.DIF_ConfigureCounting.restype = c_uint
    result = AIOWDM.DIF_ConfigureCounting(cardNum, iBit, countFalling, countRising)
    return result

def DIF_Count(cardNum: int, iBit: int) -> int:
    """Get the current pulse count."""
    count = c_uint()
    AIOWDM.DIF_Count.argtypes = [c_uint, c_uint, POINTER(c_uint)]
    AIOWDM.DIF_Count.restype = c_uint
    result = AIOWDM.DIF_Count(cardNum, iBit, byref(count))
    return count.value

def DIF_CountOverflowIRQEnable(cardNum: int, iBit: int, enabled: bool) -> int:
    """Enable or disable the count overflow IRQ."""
    AIOWDM.DIF_CountOverflowIRQEnable.argtypes = [c_uint, c_uint, c_bool]
    AIOWDM.DIF_CountOverflowIRQEnable.restype = c_uint
    result = AIOWDM.DIF_CountOverflowIRQEnable(cardNum, iBit, enabled)
    return result

def DIF_IRQEdgeEnable(cardNum: int, iBit: int, fallingIRQ: bool, risingIRQ: bool) -> int:
    """Enable IRQ on the specified edges."""
    AIOWDM.DIF_IRQEdgeEnable.argtypes = [c_uint, c_uint, c_bool, c_bool]
    AIOWDM.DIF_IRQEdgeEnable.restype = c_uint
    result = AIOWDM.DIF_IRQEdgeEnable(cardNum, iBit, fallingIRQ, risingIRQ)
    return result


# DAC Functions

def DAC_GetNumDacs(cardNum: int) -> int:
    """Return the number of DACs available on the specified card."""
    numDacs = c_ushort()
    AIOWDM.DAC_GetNumDacs.argtypes = [c_long, POINTER(c_ushort)]
    AIOWDM.DAC_GetNumDacs.restype = c_uint
    AIOWDM.DAC_GetNumDacs(cardNum, byref(numDacs))
    return numDacs.value


def DAC_GetMaxCounts(cardNum: int) -> int:
    """Return the maximum counts for DAC on the specified card."""
    maxCounts = c_ulong()
    AIOWDM.DAC_GetMaxCounts.argtypes = [c_long, POINTER(c_ulong)]
    AIOWDM.DAC_GetMaxCounts.restype = c_uint
    AIOWDM.DAC_GetMaxCounts(cardNum, byref(maxCounts))
    return maxCounts.value


def DAC_Output(cardNum: int, iDac: int, value: float) -> int:
    """Output a value to the specified DAC."""
    AIOWDM.DAC_Output.argtypes = [c_long, c_ushort, c_double]
    AIOWDM.DAC_Output.restype = c_uint
    return AIOWDM.DAC_Output(cardNum, iDac, value)


def DAC_Outputs(cardNum: int, values: List[float]) -> int:
    """Output a list of values to DACs."""
    numValues = len(values)
    values_array = (c_double * numValues)(*values)
    AIOWDM.DAC_Outputs.argtypes = [c_long, c_ushort, POINTER(c_double)]
    AIOWDM.DAC_Outputs.restype = c_uint
    return AIOWDM.DAC_Outputs(cardNum, numValues, values_array)


def DAC_SetRange(cardNum: int, iDac: int, rangeCode: int) -> int:
    """Set the range for the specified DAC."""
    AIOWDM.DAC_SetRange.argtypes = [c_long, c_ushort, c_ulong]
    AIOWDM.DAC_SetRange.restype = c_uint
    return AIOWDM.DAC_SetRange(cardNum, iDac, rangeCode)


def DAC_SetRanges(cardNum: int, rangeCodes: List[int]) -> int:
    """Set the ranges for multiple DACs."""
    numRangeCodes = len(rangeCodes)
    rangeCodes_array = (c_ulong * numRangeCodes)(*rangeCodes)
    AIOWDM.DAC_SetRanges.argtypes = [c_long, c_ushort, POINTER(c_ulong)]
    AIOWDM.DAC_SetRanges.restype = c_uint
    return AIOWDM.DAC_SetRanges(cardNum, numRangeCodes, rangeCodes_array)


def DAC_GetRange(cardNum: int, iDac: int) -> int:
    """Get the range for the specified DAC."""
    rangeCode = c_ulong()
    AIOWDM.DAC_GetRange.argtypes = [c_long, c_ushort, POINTER(c_ulong)]
    AIOWDM.DAC_GetRange.restype = c_uint
    AIOWDM.DAC_GetRange(cardNum, iDac, byref(rangeCode))
    return rangeCode.value


def DAC_GetRanges(cardNum: int, numRangeCodes: int) -> List[int]:
    """Get the ranges for multiple DACs."""
    rangeCodes_array = (c_ulong * numRangeCodes)()
    AIOWDM.DAC_GetRanges.argtypes = [c_long, POINTER(c_ushort), POINTER(c_ulong)]
    AIOWDM.DAC_GetRanges.restype = c_uint
    pNumRangeCodes = c_ushort(numRangeCodes)
    AIOWDM.DAC_GetRanges(cardNum, byref(pNumRangeCodes), rangeCodes_array)
    return list(rangeCodes_array)


def DAC_ValueFromCounts(cardNum: int, iDac: int, counts: int) -> float:
    """Convert DAC counts to a value."""
    value = c_double()
    AIOWDM.DAC_ValueFromCounts.argtypes = [c_long, c_ushort, c_ulong, POINTER(c_double)]
    AIOWDM.DAC_ValueFromCounts.restype = c_uint
    AIOWDM.DAC_ValueFromCounts(cardNum, iDac, counts, byref(value))
    return value.value


def DAC_OutputCount(cardNum: int, iDac: int, counts: int) -> int:
    """Output a specified number of counts to the specified DAC."""
    AIOWDM.DAC_OutputCount.argtypes = [c_long, c_ushort, c_ulong]
    AIOWDM.DAC_OutputCount.restype = c_uint
    return AIOWDM.DAC_OutputCount(cardNum, iDac, counts)


def DAC_OutputCounts(cardNum: int, counts: List[int]) -> int:
    """Output counts to multiple DACs."""
    numCounts = len(counts)
    counts_array = (c_ulong * numCounts)(*counts)
    AIOWDM.DAC_OutputCounts.argtypes = [c_long, c_ushort, POINTER(c_ulong)]
    AIOWDM.DAC_OutputCounts.restype = c_uint
    return AIOWDM.DAC_OutputCounts(cardNum, numCounts, counts_array)


def DAC_ReloadCalibrationData(cardNum: int) -> int:
    """Reload DAC calibration data."""
    AIOWDM.DAC_ReloadCalibrationData.argtypes = [c_long]
    AIOWDM.DAC_ReloadCalibrationData.restype = c_uint
    return AIOWDM.DAC_ReloadCalibrationData(cardNum)


def DAC_EnableCalibration(cardNum: int, enable: bool) -> int:
    """Enable or disable DAC calibration."""
    AIOWDM.DAC_EnableCalibration.argtypes = [c_long, c_ushort]
    AIOWDM.DAC_EnableCalibration.restype = c_uint
    return AIOWDM.DAC_EnableCalibration(cardNum, c_ushort(enable))


def DAC_GetCalibrationData(cardNum: int, bufLength: int) -> List[float]:
    """Get DAC calibration data."""
    buf = (c_double * bufLength)()
    AIOWDM.DAC_GetCalibrationData.argtypes = [c_long, POINTER(c_ushort), POINTER(c_double)]
    AIOWDM.DAC_GetCalibrationData.restype = c_uint
    pBufLength = c_ushort(bufLength)
    AIOWDM.DAC_GetCalibrationData(cardNum, byref(pBufLength), buf)
    return list(buf)


def DAC_WriteCalibrationData(cardNum: int, bufLength: int, buf: List[float], toEEPROM: bool) -> int:
    """Write DAC calibration data."""
    buf_array = (c_ubyte * bufLength)(*buf)
    AIOWDM.DAC_WriteCalibrationData.argtypes = [c_long, POINTER(c_ushort), POINTER(c_ubyte), c_ushort]
    AIOWDM.DAC_WriteCalibrationData.restype = c_uint
    pBufLength = c_ushort(bufLength)
    return AIOWDM.DAC_WriteCalibrationData(cardNum, byref(pBufLength), buf_array, c_ushort(toEEPROM))


def DAC_ConfigureSimultaneousMode(cardNum: int, bSimultaneous: bool, bUpdate: bool) -> int:
    """Configure DAC simultaneous mode."""
    AIOWDM.DAC_ConfigureSimultaneousMode.argtypes = [c_long, c_long, c_long]
    AIOWDM.DAC_ConfigureSimultaneousMode.restype = c_uint
    return AIOWDM.DAC_ConfigureSimultaneousMode(cardNum, int(bSimultaneous), int(bUpdate))


def DAC_ConfigureVoltageRestrictMode(cardNum: int, bRestricted: bool) -> int:
    """Configure DAC voltage restrict mode."""
    AIOWDM.DAC_ConfigureVoltageRestrictMode.argtypes = [c_long, c_long]
    AIOWDM.DAC_ConfigureVoltageRestrictMode.restype = c_uint
    return AIOWDM.DAC_ConfigureVoltageRestrictMode(cardNum, int(bRestricted))

# Existing Functions

def GetNumCards() -> int:
    """Return the number of detected, installed, AIOWDM-using cards."""
    AIOWDM.GetNumCards.restype = c_int
    return AIOWDM.GetNumCards()


def QueryCardInfo(cardNum: int) -> tuple[int, int, int, str]:
    """
    Return status, PID, {baseaddress}, and card model name.

    It is virtually never necessary to use the baseaddress value due to the
    new "Relative" functions in the API: instead use RelOutPort*() and RelInPort*().
    """
    PID = c_ulong(0)
    base = c_ulong(0)
    namesize = c_ulong(50)
    nameBuf = create_string_buffer(50)
    AIOWDM.QueryCardInfo.argtypes = [
        c_int,
        POINTER(c_ulong),
        POINTER(c_ulong),
        POINTER(c_ulong),
        c_char_p,
    ]
    AIOWDM.QueryCardInfo.restype = c_uint
    status = AIOWDM.QueryCardInfo(cardNum, byref(PID), byref(base), byref(namesize), nameBuf)
    return status, PID.value, base.value, nameBuf.value.strip().decode()


def WaitForIRQ(cardNum: int) -> c_uint:
    """
    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.)
    """
    AIOWDM.WaitForIRQ.argtypes = [c_uint]
    AIOWDM.WaitForIRQ.restype = c_uint
    return AIOWDM.WaitForIRQ(cardNum)


def AbortRequest(cardNum: int) -> c_uint:
    """Aborts WaitForIRQ and COSWaitForIRQ allowing the thread to terminate"""
    AIOWDM.AbortRequest.argtypes = [c_uint]
    AIOWDM.AbortRequest.restype = c_uint
    return AIOWDM.AbortRequest(cardNum)


def CloseCard(cardNum: int) -> c_uint:
    """Aborts pending Waits and waits for completion.  Should be reliably called
    before your process ends"""
    AIOWDM.CloseCard.argtypes = [c_uint]
    AIOWDM.CloseCard.restype = c_uint
    return AIOWDM.CloseCard(cardNum)


def COSWaitForIRQ(cardNum: int, PPIs, data) -> tuple[int, Any]:
    """Blocks the calling thread until an IRQ occurs, and returns data read from
    the device in the ISR"""
    dataBuf = (c_ubyte * PPIs * 3)()
    AIOWDM.COSWaitForIRQ.argtypes = [c_uint, c_uint, POINTER(c_ubyte)]
    AIOWDM.COSWaitForIRQ.restype = c_uint
    status = AIOWDM.COSWaitForIRQ(cardNum, PPIs, dataBuf)
    return status, dataBuf


def RelInPortB(cardNum: int, offset) -> c_uint:
    """Reads a byte register"""
    AIOWDM.RelInPortB.argtypes = [c_uint, c_uint]
    AIOWDM.RelInPortB.restype = c_uint
    return AIOWDM.RelInPortB(cardNum, offset)


def RelInPort(cardNum: int, offset) -> c_uint:
    """Reads a UInt16 register"""
    AIOWDM.RelInPort.argtypes = [c_uint, c_uint]
    AIOWDM.RelInPort.restype = c_uint
    return AIOWDM.RelInPort(cardNum, offset)


def RelInPortL(cardNum: int, offset) -> c_uint:
    """Reads a UInt32 register"""
    AIOWDM.RelInPortL.argtypes = [c_uint, c_uint]
    AIOWDM.RelInPortL.restype = c_uint
    return AIOWDM.RelInPortL(cardNum, offset)


def RelOutPortB(cardNum: int, offset, data) -> c_uint:
    """Writes a byte register"""
    AIOWDM.RelOutPortB.argtypes = [c_uint, c_uint, c_ubyte]
    AIOWDM.RelOutPortB.restype = c_uint
    return AIOWDM.RelOutPortB(cardNum, offset, c_ubyte(data))


def RelOutPort(cardNum: int, offset, data) -> c_uint:
    """Writes a UInt16 register"""
    AIOWDM.RelOutPort.argtypes = [c_uint, c_uint, c_ushort]
    AIOWDM.RelOutPort.restype = c_uint
    return AIOWDM.RelOutPort(cardNum, offset, c_ushort(data))


def RelOutPortL(cardNum: int, offset, data) -> c_uint:
    """Writes a UInt32 register"""
    AIOWDM.RelOutPortL.argtypes = [c_uint, c_uint, c_uint]
    AIOWDM.RelOutPortL.restype = c_uint
    return AIOWDM.RelOutPortL(cardNum, offset, c_ulong(data))


def InPortB(address: int) -> c_uint:
    """Reads a byte register; deprecated, use RelInPortB instead when viable"""
    AIOWDM.InPortB.argtypes = [c_uint]
    AIOWDM.InPortB.restype = c_uint
    return AIOWDM.InPortB(address)


def InPort(address: int) -> c_uint:
    """Reads a UInt16 register; deprecated, use RelInPort instead when viable"""
    AIOWDM.InPort.argtypes = [c_uint]
    AIOWDM.InPort.restype = c_uint
    return AIOWDM.InPort(address)


def InPortL(address: int) -> c_uint:
    """Reads a UInt32 register; deprecated, use RelInPortL instead when viable"""
    AIOWDM.InPortL.argtypes = [c_uint]
    AIOWDM.InPortL.restype = c_uint
    return AIOWDM.InPortL(address)


def OutPortB(address: int, data) -> c_uint:
    """Writes a byte register; deprecated, use RelOutPortB instead when viable"""
    AIOWDM.OutPortB.argtypes = [c_uint, c_ubyte]
    AIOWDM.OutPortB.restype = c_uint
    return AIOWDM.OutPortB(address, c_ubyte(data))


def OutPort(address: int, data) -> c_uint:
    """Writes a UInt16 register; deprecated, use RelOutPort instead when viable"""
    AIOWDM.OutPort.argtypes = [c_uint, c_ushort]
    AIOWDM.OutPort.restype = c_uint
    return AIOWDM.OutPort(address, c_ushort(data))


def OutPortL(address: int, data) -> c_uint:
    """Writes a UInt32 register; deprecated, use RelOutPortL instead when viable"""
    AIOWDM.OutPortL.argtypes = [c_uint, c_ubyte, c_uint]
    AIOWDM.OutPortL.restype = c_uint
    return AIOWDM.OutPortL(address, c_ulong(data))


def displayBoardInfo(index: int):
    """Prints basic information from QueryCardInfo()"""
    # we don't generally need the base address in this, the "RelInPort()" era
    status, PID, base, name = QueryCardInfo(index)
    print(index, PID, name)
    print(f"Card found at cardNum {index} has deviceID 0x{PID:04X} = {name} [addr=0x{base:04X}]")


if __name__ == "__main__":  # main code section

    class IRQWorker(Thread):
        def __init__(self):
            Thread.__init__(self)
            self.count = 0
            self.terminated = False

        def run(self):
            while not self.terminated:
                status = WaitForIRQ(0)
                if status != 0:
                    print("IRQ ! ", self.count)
                    self.count = self.count + 1
                else:
                    print("terminated, status = ", status)
                    if status != 995:
                        print("error was ", GetLastError())

    print("AIOWDM Python introductory sample using ctypes\n")

    cardCount = GetNumCards()
    if cardCount == 0:
        print(
            """No valid AIOWDM devices were detected.\nPlease confirm your device is
        installed\nand the drivers are installed correctly (check Device Manager)."""
        )
        exit()

    boardsFound = 0
    for card_num in range(0, cardCount):
        boardsFound = boardsFound + 1
        displayBoardInfo(card_num)
    print("\nAIOWDM Boards found:", boardsFound)

    if False:
        worker = IRQWorker()
        worker.start()
        RelInPortB(0, 2)  # enable IRQs

        input("Waiting for IRQs to occur. Press ENTER to continue.")

        print("Total IRQs detected:", worker.count)
        worker.terminated = True
        CloseCard(0)

    print("\nDone.")
