/*
 *  puzzlefw.hpp
 *
 *  C++ library for PuzzleFW firmware.
 *
 *  Joris van Rantwijk 2024
 */

#ifndef PUZZLEFW_H_
#define PUZZLEFW_H_

#include <stdint.h>
#include <stdexcept>


namespace puzzlefw {

/* Size of memory mapped register area. */
constexpr uint32_t REGS_SIZE = 0x1000;

/* Register addresses relative to register area base address. */
constexpr uint32_t REG_INFO               = 0x000;
constexpr uint32_t REG_IRQ_ENABLE         = 0x010;
constexpr uint32_t REG_IRQ_PENDING        = 0x014;
constexpr uint32_t REG_DMA_EN             = 0x100;
constexpr uint32_t REG_DMA_STATUS         = 0x104;
constexpr uint32_t REG_DMA_CLEAR          = 0x108;
constexpr uint32_t REG_TIMESTAMP_LO       = 0x180;
constexpr uint32_t REG_TIMESTAMP_HI       = 0x184;
constexpr uint32_t REG_TIMESTAMP_CLEAR    = 0x188;
constexpr uint32_t REG_ACQ_ADDR_START     = 0x200;
constexpr uint32_t REG_ACQ_ADDR_END       = 0x204;
constexpr uint32_t REG_ACQ_ADDR_LIMIT     = 0x208;
constexpr uint32_t REG_ACQ_ADDR_INTR      = 0x20c;
constexpr uint32_t REG_ACQ_ADDR_PTR       = 0x210;
constexpr uint32_t REG_ACQ_DMA_CTRL       = 0x214;
constexpr uint32_t REG_ACQ_INTR_CTRL      = 0x218;
constexpr uint32_t REG_ACQ_DMA_STATUS     = 0x21c;
constexpr uint32_t REG_ACQUISITION_EN     = 0x220;
constexpr uint32_t REG_RECORD_LENGTH      = 0x224;
constexpr uint32_t REG_DECIMATION_FACTOR  = 0x228;
constexpr uint32_t REG_SHIFT_STEPS        = 0x22c;
constexpr uint32_t REG_AVERAGING_EN       = 0x230;
constexpr uint32_t REG_CH4_MODE           = 0x234;
constexpr uint32_t REG_SIMULATE_ADC       = 0x238;
constexpr uint32_t REG_TRIGGER_MODE       = 0x240;
constexpr uint32_t REG_TRIGGER_DELAY      = 0x244;
constexpr uint32_t REG_TRIGGER_STATUS     = 0x248;
constexpr uint32_t REG_ADC_SAMPLE         = 0x280;
constexpr uint32_t REG_ADC23_SAMPLE       = 0x284;
constexpr uint32_t REG_ADC_RANGE_CLEAR    = 0x28c;
constexpr uint32_t REG_ADC0_MINMAX        = 0x290;
constexpr uint32_t REG_ADC1_MINMAX        = 0x294;
constexpr uint32_t REG_ADC2_MINMAX        = 0x298;
constexpr uint32_t REG_ADC3_MINMAX        = 0x29c;
constexpr uint32_t REG_TT_ADDR_START      = 0x300;
constexpr uint32_t REG_TT_ADDR_END        = 0x304;
constexpr uint32_t REG_TT_ADDR_LIMIT      = 0x308;
constexpr uint32_t REG_TT_ADDR_INTR       = 0x30c;
constexpr uint32_t REG_TT_ADDR_PTR        = 0x310;
constexpr uint32_t REG_TT_DMA_CTRL        = 0x314;
constexpr uint32_t REG_TT_INTR_CTRL       = 0x318;
constexpr uint32_t REG_TT_DMA_STATUS      = 0x31c;
constexpr uint32_t REG_TIMETAGGER_EN      = 0x320;
constexpr uint32_t REG_TIMETAGGER_MARK    = 0x324;
constexpr uint32_t REG_DIG_SIMULATE       = 0x330;
constexpr uint32_t REG_DIG_SAMPLE         = 0x338;
constexpr uint32_t REG_LED_STATE          = 0x404;

/* Pending IRQ flags. */
constexpr uint32_t IRQ_PENDING_ACQ        = 0x01;
constexpr uint32_t IRQ_PENDING_TT         = 0x02;

/* DMA status flags. */
constexpr uint32_t DMA_STATUS_BUSY        = 0x01;
constexpr uint32_t DMA_ERROR_READ         = 0x02;
constexpr uint32_t DMA_ERROR_WRITE        = 0x04;
constexpr uint32_t DMA_ERROR_ADDRESS      = 0x08;
constexpr uint32_t DMA_ERROR_ANY          = 0x10;


/** Synchronize between register access and DMA buffer access. */
static inline void sync_dma()
{
    asm volatile ("dmb" : : : "memory");
}


/** Trigger modes. */
enum TriggerMode {
    TRIG_NONE = 0,      // trigger disabled, manual trigger only
    TRIG_AUTO = 1,      // continuous triggering
    TRIG_EXTERNAL = 2   // external triggering
};


/** Firmware version information. */
struct VersionInfo {
    uint8_t api_version;
    uint8_t major_version;
    uint8_t minor_version;
};


/**
 * Device API for PuzzleFW firmware.
 *
 * This class uses the UIO driver framework and the custom PuzzleFW Linux
 * kernel driver to access the PuzzleFW firmware.
 *
 * Only a single instance of this class should exist at any time in any
 * process on the system. Creating multiple instances, even in separate
 * processes, may cause the firmware to function incorrectly.
 *
 * Methods of this class may throw exceptions to report error conditions.
 * "std::invalid_argument" is used to report invalid arguments.
 * "std::runtime_error" is used to report errors during opening of the device.
 *
 * Unless stated otherwise, the methods of this class must not be called
 * concurrently from multiple threads without explicit synchronization.
 */
class PuzzleFwDevice
{
public:
    /** Minimum size of the DMA buffer. */
    static constexpr size_t MIN_DMA_BUF_SIZE = 16384;

    /** Maximum number of samples per trigger. */
    static constexpr unsigned int MAX_DECIMATION_FACTOR = 1 << 18;

    /** Maximum number of samples per trigger. */
    static constexpr unsigned int MAX_RECORD_LENGTH = 1 << 16;

    /** Maximum trigger delay. */
    static constexpr unsigned int MAX_TRIGGER_DELAY = (1 << 16) - 1;

    /** Constructor. Opens the device driver. */
    PuzzleFwDevice();

    // Delete copy constructor and assignment operator.
    PuzzleFwDevice(const PuzzleFwDevice&) = delete;
    PuzzleFwDevice& operator=(const PuzzleFwDevice&) = delete;

    /** Destructor. Closes the device driver. */
    ~PuzzleFwDevice();

    /**
     * Return the file descriptor of the UIO device node.
     *
     * It can be used to detect FPGA interrupts.
     */
    int uio_fd() const
    {
        return m_uio_fd;
    }

    /** Return a pointer to the DMA buffer. */
    volatile void * dma_buffer() const
    {
        return m_dma_buf;
    }

    /** Return the DMA buffer size in bytes. */
    size_t dma_buffer_size() const
    {
        return m_dma_buf_size;
    }

    /**
     * Return the DMA address alignment.
     *
     * This alignment restriction applies to start address, end address,
     * and the limit pointer of DMA stream buffers.
     */
    size_t dma_alignment() const
    {
        return 128;
    }

    /** Read from FPGA register. */
    uint32_t read_reg(uint32_t addr)
    {
        return m_regs[addr / 4];
    }

    /** Write to FPGA register. */
    void write_reg(uint32_t addr, uint32_t value)
    {
        m_regs[addr / 4] = value;
    }

    /** Return firmware version information. */
    VersionInfo get_version_info()
    {
        uint32_t v = read_reg(REG_INFO);
        uint8_t api_version = (v >> 16) & 0xff;
        uint8_t major_version = (v >> 8) & 0xff;
        uint8_t minor_version = v & 0xff;
        return VersionInfo{api_version, major_version, minor_version};
    }

    /** Return the number of analog channels (2 or 4). */
    unsigned int get_analog_channel_count()
    {
// TODO -- modify firmware to add ch4-support bit
        uint32_t v = read_reg(REG_CH4_MODE);
        return (v & 0x100) ? 4 : 2;
    }

    /** Return the current value of the timestamp counter. */
    uint64_t get_timestamp()
    {
        uint32_t vhi, vlo;
        vhi = read_reg(REG_TIMESTAMP_HI);
        while (1) {
            uint32_t vhi_prev = vhi;
            vlo = read_reg(REG_TIMESTAMP_LO);
            vhi = read_reg(REG_TIMESTAMP_HI);
            if (vhi == vhi_prev) {
                break;
            }
        }
        return (((uint64_t)vhi) << 32) | vlo;
    }

    /** Clear the timestamp counter. */
    void clear_timestamp()
    {
        write_reg(REG_TIMESTAMP_CLEAR, 1);
    }

    /** Return True if the selected LED is on. */
    bool get_led_state(unsigned int led_index)
    {
        if (led_index > 7) {
            throw std::invalid_argument("Invalid led_index");
        }
        uint32_t v = read_reg(REG_LED_STATE);
        return ((v >> led_index) & 1) != 0;
    }

    /**
     * Turn the specified LED on or off.
     *
     * Valid led_index values are 4 to 7.
     */
    void set_led_state(unsigned int led_index, bool led_on)
    {
        if (led_index > 7) {
            throw std::invalid_argument("Invalid led_index");
        }
        uint32_t v = read_reg(REG_LED_STATE);
        uint32_t m = (1 << led_index);
        if (led_on) {
            v |= m;
        } else {
            v &= ~m;
        }
        write_reg(REG_LED_STATE, v);
    }

    /** Return the current trigger mode. */
    TriggerMode get_trigger_mode()
    {
        uint32_t v = read_reg(REG_TRIGGER_MODE);
        if ((v & 0x01) != 0) {
            return TRIG_AUTO;
        }
        if ((v & 0x02) != 0) {
            return TRIG_EXTERNAL;
        }
        return TRIG_NONE;
    }

    /** Set trigger mode. */
    void set_trigger_mode(TriggerMode mode)
    {
        uint32_t v = read_reg(REG_TRIGGER_MODE);
        v &= 0xfc;
        if (mode == TRIG_AUTO) {
            v |= 0x01;
        }
        if (mode == TRIG_EXTERNAL) {
            v |= 0x02;
        }
        write_reg(REG_TRIGGER_MODE, v);
    }

    /** Return the selected external trigger channel. */
    unsigned int get_trigger_ext_channel()
    {
        uint32_t v = read_reg(REG_TRIGGER_MODE);
        return ((v >> 4) & 7);
    }

    /** Select external trigger channel. */
    void set_trigger_ext_channel(unsigned int channel)
    {
        if (channel > 7) {
            throw std::invalid_argument("Invalid trigger channel");
        }
        uint32_t v = read_reg(REG_TRIGGER_MODE);
        v &= 0x8f;
        v |= (channel << 4);
        write_reg(REG_TRIGGER_MODE, v);
    }

    /** Return true if triggering on falling edge. */
    bool get_trigger_ext_falling()
    {
        uint32_t v = read_reg(REG_TRIGGER_MODE);
        return ((v & 0x80) != 0);
    }

    /** Select active edge for external trigger signal. */
    void set_trigger_ext_falling(bool falling)
    {
        uint32_t v = read_reg(REG_TRIGGER_MODE);
        v &= 0x7f;
        if (falling) {
            v |= 0x80;
        }
        write_reg(REG_TRIGGER_MODE, v);
    }

    /** Force a single trigger event. */
    void trigger_force()
    {
        uint32_t v = read_reg(REG_TRIGGER_MODE);
        write_reg(REG_TRIGGER_MODE, v | 0x100);
    }

    /** Return configured trigger delay (in ADC samples). */
    unsigned int get_trigger_delay()
    {
        return read_reg(REG_TRIGGER_DELAY);
    }

    /** Set trigger delay (in ADC samples). */
    void set_trigger_delay(unsigned int delay)
    {
        if (delay > MAX_TRIGGER_DELAY) {
            throw std::invalid_argument("Invalid trigger delay");
        }
        write_reg(REG_TRIGGER_DELAY, delay);
    }

    /** Return true if analog acquisition is waiting for a trigger. */
    bool is_waiting_for_trigger()
    {
        uint32_t v = read_reg(REG_TRIGGER_STATUS);
        return ((v & 1) != 0);
    }

    /** Return true if analog acquisition is enabled. */
    bool is_acquisition_enabled()
    {
        uint32_t v = read_reg(REG_ACQUISITION_EN);
        return ((v & 1) != 0);
    }

    /** Enable or disable analog acquisition. */
    void set_acquisition_enabled(bool enable)
    {
        write_reg(REG_ACQUISITION_EN, enable ? 1 : 0);
    }

    /** Return true if 4-channel mode is enabled. */
    bool is_4channel_mode()
    {
        uint32_t v = read_reg(REG_CH4_MODE);
        return ((v & 1) != 0);
    }

    /** Enable or disable 4-channel mode. */
    void set_4channel_mode(bool enable)
    {
        write_reg(REG_CH4_MODE, enable ? 1 : 0);
    }

    /** Return the decimation factor. */
    unsigned int get_decimation_factor()
    {
        uint32_t v = read_reg(REG_DECIMATION_FACTOR);
        return v + 1;
    }

    /**
     * Set the decimation factor (number of ADC samples per decimated sample).
     *
     * Valid values are 1 to MAX_DECIMATION_FACTOR.
     * This method writes the specified value minus 1 to the FPGA register.
     */
    void set_decimation_factor(unsigned int decimation_factor)
    {
        if (decimation_factor == 0
                || decimation_factor > MAX_DECIMATION_FACTOR) {
            throw std::invalid_argument("Invalid decimation factor");
        }
        write_reg(REG_DECIMATION_FACTOR, decimation_factor - 1);
    }

    /** Return true if sample averaging is enabled. */
    bool is_averaging_enabled()
    {
        uint32_t v = read_reg(REG_AVERAGING_EN);
        return ((v & 1) != 0);
    }

    /** Enable or disable sample averaging. */
    void set_averaging_enabled(bool enable)
    {
        write_reg(REG_AVERAGING_EN, enable ? 1 : 0);
    }

    /** Return the number of shift-right positions for decimated samples. */
    int get_shift_steps()
    {
        return read_reg(REG_SHIFT_STEPS);
    }

    /**
     * Set the number of shift-right positions for decimated samples.
     *
     * Valid values are 0 to 8 steps.
     */
    void set_shift_steps(int shift_steps)
    {
        if (shift_steps < 0 || shift_steps > 8) {
            throw std::invalid_argument("Invalid shift_steps");
        }
        write_reg(REG_SHIFT_STEPS, shift_steps);
    }

    /** Return the record length (number of samples per trigger). */
    unsigned int get_record_length()
    {
        return read_reg(REG_RECORD_LENGTH) + 1;
    }

    /**
     * Set record length (number of samples per trigger).
     *
     * Valid values are 1 to MAX_RECORD_LENGTH.
     * This method writes the specified value minus 1 to the FPGA register.
     */
    void set_record_length(unsigned int samples)
    {
        if (samples == 0 || samples > MAX_RECORD_LENGTH) {
            throw std::invalid_argument("Invalid record length");
        }
        write_reg(REG_RECORD_LENGTH, samples - 1);
    }

    /** Return the most recent ADC sample for the specified channel. */
    unsigned int get_adc_sample(unsigned int channel)
    {
        if (channel >= 4) {
            throw std::invalid_argument("Invalid ADC channel");
        }
        uint32_t addr = (channel < 2) ? REG_ADC_SAMPLE : REG_ADC23_SAMPLE;
        uint32_t v = read_reg(addr);
        return (v >> (16 * (channel & 1))) & 0x3fff;
    }

    /** Return minimum and maximum sample value sinc last cleared. */
    void get_adc_range(unsigned int channel,
                       unsigned int& min_sample,
                       unsigned int& max_sample)
    {
        if (channel >= 4) {
            throw std::invalid_argument("Invalid ADC channel");
        }
        uint32_t addr = (channel == 0) ? REG_ADC0_MINMAX :
                        (channel == 1) ? REG_ADC1_MINMAX :
                        (channel == 2) ? REG_ADC2_MINMAX :
                        REG_ADC3_MINMAX;
        uint32_t v = read_reg(addr);
        min_sample = v & 0x3fff;
        max_sample = (v >> 16) & 0x3fff;
    }

    /** Clear minimum/maximum sample ranges. */
    void clear_adc_range()
    {
        write_reg(REG_ADC_RANGE_CLEAR, 1);
    }

    /** Return true if ADC sample simulation is enabled. */
    bool is_adc_simulation_enabled()
    {
        uint32_t v = read_reg(REG_SIMULATE_ADC);
        return ((v & 1) != 0);
    }

    /** Enable or disable ADC sample simulation. */
    void set_adc_simulation_enabled(bool enable)
    {
        write_reg(REG_SIMULATE_ADC, enable ? 1 : 0);
    }

    /** Return the mask of enabled timetagger event types. */
    uint32_t get_timetagger_event_mask()
    {
        return read_reg(REG_TIMETAGGER_EN);
    }

    /**
     * Set mask of enabled timetagger event types.
     *
     * Bit 0 enables rising edges on digital input channel 0.
     * Bit 1 enables falling edges on digital input channel 0.
     * Bit 2 enables rising edges on digital input channel 1.
     * And so on.
     */
    void set_timetagger_event_mask(uint32_t event_mask)
    {
        write_reg(REG_TIMETAGGER_EN, event_mask);
    }

    /** Request a marker record in the timetagger event stream. */
    void timetagger_mark()
    {
        write_reg(REG_TIMETAGGER_MARK, 1);
    }

    /** Return a bitmask representing the current digital input state. */
    uint32_t get_digital_input_state()
    {
        return read_reg(REG_DIG_SAMPLE);
    }

    /** Return true if digital signal simulation is enabled. */
    bool is_digital_simulation_enabled()
    {
        uint32_t v = read_reg(REG_DIG_SIMULATE);
        return ((v & 0x100) != 0);
    }

    /** Enable or disable digital signal simulation. */
    void set_digital_simulation_enabled(bool enable)
    {
        uint32_t v = read_reg(REG_DIG_SIMULATE);
        if (enable) {
            v |= 0x100;
        } else {
            v &= 0xff;
        }
        write_reg(REG_DIG_SIMULATE, v);
    }

    /** Return simulated digital input state. */
    uint32_t get_digital_simulation_state()
    {
        uint32_t v = read_reg(REG_DIG_SIMULATE);
        return v & 0xff;
    }

    /** Set simulated digital input state. */
    void set_digital_simulation_state(uint32_t mask)
    {
        uint32_t v = read_reg(REG_DIG_SIMULATE);
        v &= 0x100;
        v |= mask;
        write_reg(REG_DIG_SIMULATE, v);
    }

    /** Return true if FPGA interrupts are enabled. */
    bool is_irq_enabled()
    {
        uint32_t v = read_reg(REG_IRQ_ENABLE);
        return ((v & 1) != 0);
    }

    /**
     * Enable FPGA interrupts.
     *
     * The Linux kernel driver will automatically disable interrupts
     * whenever an interrupt occurs.
     *
     * It is safe to call this function concurrently from multiple threads.
     */
    void enable_irq()
    {
        write_reg(REG_IRQ_ENABLE, 1);
    }

    /**
     * Return bit mask of pending interrupt conditions.
     *
     * It is safe to call this function concurrently from multiple threads.
     */
    uint32_t get_irq_pending()
    {
        return read_reg(REG_IRQ_PENDING);
    }

    /** Return true if the FPGA DMA engine is enabled. */
    bool is_dma_enabled()
    {
        uint32_t v = read_reg(REG_DMA_EN);
        return ((v & 1) != 0);
    }

    /** Enable or disable the FPGA DMA engine. */
    void set_dma_enabled(bool enable)
    {
        write_reg(REG_DMA_EN, enable ? 1 : 0);
    }

    /** Return bit mask of DMA status and error flags. */
    uint32_t get_dma_status()
    {
        return read_reg(REG_DMA_STATUS);
    }

    /**
     * Clear pending DMA error flags.
     *
     * Before doing this, any active DMA data streams should be stopped.
     */
    void clear_dma_errors()
    {
        write_reg(REG_DMA_CLEAR, 1);
    }

private:
    /** File descriptor of the UIO device. */
    int                 m_uio_fd;

    /** Pointer to device registers mapped in process address space. */
    volatile uint32_t * m_regs;

    /** Pointer to DMA buffer mapped in process address space. */
    volatile void *     m_dma_buf;

    /** Size of DMA buffer (in bytes). */
    size_t              m_dma_buf_size;

    /** Full name of the PuzzleFW platform device. */
    std::string         m_device_name;

    /** Path name of UIO device node. */
    std::string         m_uio_path;
};


/**
 * Manage a DMA data write stream.
 *
 * The PuzzleFW firmware provides two DMA write streams: one stream for
 * analog sample data and one stream for timetagger data.
 *
 * Only a single instance of this class should exist for a specific
 * DMA stream type at any time. I.e. at most one instance should exist
 * for analog sample data, and at most one instance for timetagger data.
 *
 * Each DMA stream uses a segment within the global DMA buffer.
 * Users of this class must make sure that the stream buffer segment
 * is located within the global DMA buffer, and that it does not overlap
 * with any buffer segment for another DMA stream.
 *
 * An instance of this class must not be accessed concurrently from
 * multiple threads without explicit synchronization. However, multiple
 * threads may safely access different instances of this class.
 */
class DmaWriteStream
{
public:
    enum StreamType { DMA_ACQ = 1, DMA_TT };

    /** Keep this number of bytes unused in the DMA stream buffer segment. */
    static constexpr size_t POINTER_MARGIN = 4096;

    /**
     * Initialize DMA stream.
     *
     * The DMA stream is initially disabled.
     *
     * Parameters "buf_start" and "buf_end" specify the start and end offset
     * of the buffer segment, relative to the start of the global DMA buffer.
     * These must be aligned to the DMA address alignment as returned by
     * "PuzzleFwDevice::dma_alignment()".
     *
     * The DMA stream instance keeps an internal pointer to the device
     * instance. The device instance must remain valid for the life time
     * of the DMA stream instance.
     */
    DmaWriteStream(PuzzleFwDevice& device,
                   StreamType stream_type,
                   size_t buf_start,
                   size_t buf_end)
      : m_device(device)
      , m_reg((stream_type == DMA_ACQ) ? regs_acq : regs_tt)
      , m_irq_pending_mask((stream_type == DMA_ACQ) ?
                           IRQ_PENDING_ACQ : IRQ_PENDING_TT)
      , m_buf_start(buf_start)
      , m_buf_end(buf_end)
      , m_read_pointer(buf_start)
    {
        if (buf_start >= buf_end
                || buf_end - buf_start < 4 * POINTER_MARGIN
                || buf_end > m_device.dma_buffer_size()
                || buf_start % m_device.dma_alignment() != 0
                || buf_end % m_device.dma_alignment() != 0) {
            throw std::invalid_argument("Invalid buffer segment");
        }

        // Disable DMA stream.
        m_device.write_reg(m_reg.dma_ctrl, 0);

        // Disable DMA stream interrupts and clear pending interrupts.
        disable_interrupt();

        // Set up DMA buffer segment.
        m_device.write_reg(m_reg.addr_start, m_buf_start);
        m_device.write_reg(m_reg.addr_end, m_buf_end);
        m_device.write_reg(m_reg.addr_limit, m_buf_end - POINTER_MARGIN);

        // Initialize DMA stream (reset write pointer to start of segment).
        m_device.write_reg(m_reg.dma_ctrl, 2);
    }

    // Delete copy constructor and assignment operator.
    DmaWriteStream(const DmaWriteStream&) = delete;
    DmaWriteStream& operator=(const DmaWriteStream&) = delete;

    /** Disable DMA stream. Remaining data in buffer is discarded. */
    ~DmaWriteStream()
    {
        // Disable DMA stream and re-initialize.
        m_device.write_reg(m_reg.dma_ctrl, 0);
        m_device.write_reg(m_reg.dma_ctrl, 2);

        // Disable DMA stream interrupt and clear pending interrupt.
        disable_interrupt();
    }

    /** Return the size of the DMA buffer segment. */
    size_t buffer_segment_size() const
    {
        return m_buf_end - m_buf_start;
    }

    /** Return the DMA address alignment. */
    size_t dma_alignment() const
    {
        return m_device.dma_alignment();
    }

    /** Enable or disable the DMA stream. */
    void set_enabled(bool enable)
    {
        m_device.write_reg(m_reg.dma_ctrl, enable ? 1 : 0);
    }

    /** Return the number of bytes available in the DMA buffer. */
    size_t get_data_available()
    {
        uint32_t write_pointer = m_device.read_reg(m_reg.addr_ptr);
        if (write_pointer >= m_read_pointer) {
            return write_pointer - m_read_pointer;
        } else {
            return m_buf_end - m_buf_start + write_pointer - m_read_pointer;
        }
    }

    /**
     * Return a contiguous block of available data in the DMA buffer.
     *
     * This function may return a subset of the available data.
     */
    void get_data_block(void *& ptr, size_t& n)
    {
        uint32_t write_pointer = m_device.read_reg(m_reg.addr_ptr);

        // Ensure that upcoming memory access to the buffer occurs AFTER
        // reading the pointer register.
        sync_dma();

        ptr = (char *)(m_device.dma_buffer()) + m_read_pointer;
        if (write_pointer >= m_read_pointer) {
            n = write_pointer - m_read_pointer;
        } else {
            n = m_buf_end - m_read_pointer;
        }
    }

    /**
     * Release the specified number of bytes from the buffer.
     *
     * Update the read pointer and allow the DMA engine to reuse
     * the released range for new data.
     *
     * The specified size must be a multiple of 8 bytes.
     */
    void consume_data(size_t n)
    {
        if ((n & 7) != 0) {
            throw std::invalid_argument("Invalid size");
        }

        // Update read pointer.
        if (n < m_buf_end - m_read_pointer) {
            m_read_pointer += n;
        } else {
            m_read_pointer = m_read_pointer + n - m_buf_end + m_buf_start;
        }

        // Update limit address.
        // Round down to aligned address.
        uint32_t limit;
        if (m_read_pointer - m_buf_start < POINTER_MARGIN) {
            limit = m_read_pointer - m_buf_start + m_buf_end - POINTER_MARGIN;
        } else {
            limit = m_read_pointer - POINTER_MARGIN;
        }
        limit &= ~((uint32_t)127);

        sync_dma();

        m_device.write_reg(m_reg.addr_limit, limit);
    }

    /** Return true if the interrupt for this DMA stream is pending. */
    bool is_interrupt_pending()
    {
        uint32_t irq_pending = m_device.get_irq_pending();
        return (irq_pending & m_irq_pending_mask) != 0;
    }

    /** Enable interrupt when specified amount of data is available. */
    void enable_interrupt_on_data_available(size_t n)
    {
        // Update interrupt pointer.
        uint32_t threshold;
        if (m_buf_end - m_read_pointer > n) {
            threshold = m_read_pointer + n;
        } else {
            threshold = m_read_pointer + n - m_buf_end + m_buf_start;
        }
        m_device.write_reg(m_reg.addr_intr, threshold);

        // Enable interrupt.
        m_device.write_reg(m_reg.intr_ctrl, 1);
    }

    /** Disable interrupt and clear pending interrupt for this DMA stream. */
    void disable_interrupt()
    {
        m_device.write_reg(m_reg.intr_ctrl, 2);
    }

private:
    struct RegisterAddress {
        uint32_t addr_start;
        uint32_t addr_end;
        uint32_t addr_limit;
        uint32_t addr_intr;
        uint32_t addr_ptr;
        uint32_t dma_ctrl;
        uint32_t intr_ctrl;
        uint32_t dma_status;
    };

    static constexpr RegisterAddress regs_acq {
        REG_ACQ_ADDR_START,
        REG_ACQ_ADDR_END,
        REG_ACQ_ADDR_LIMIT,
        REG_ACQ_ADDR_INTR,
        REG_ACQ_ADDR_PTR,
        REG_ACQ_DMA_CTRL,
        REG_ACQ_INTR_CTRL,
        REG_ACQ_DMA_STATUS
    };

    static constexpr RegisterAddress regs_tt {
        REG_TT_ADDR_START,
        REG_TT_ADDR_END,
        REG_TT_ADDR_LIMIT,
        REG_TT_ADDR_INTR,
        REG_TT_ADDR_PTR,
        REG_TT_DMA_CTRL,
        REG_TT_INTR_CTRL,
        REG_TT_DMA_STATUS
    };

    PuzzleFwDevice&     m_device;
    const RegisterAddress m_reg;
    const uint32_t      m_irq_pending_mask;
    const uint32_t      m_buf_start;
    const uint32_t      m_buf_end;
    uint32_t            m_read_pointer;
};


};  // namespace puzzlefw

#endif // PUZZLEFW_H_