/***************************************************************************
 *   Copyright (C) 2023 by Federico Amedeo Izzo IU2NUO,                    *
 *                         Niccolò Izzo IU2KIN                             *
 *                         Frederik Saraci IU2NRO                          *
 *                         Silvano Seva IU2KWO                             *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 3 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, see <http://www.gnu.org/licenses/>   *
 ***************************************************************************/

#ifndef DMA_STREAM_H
#define DMA_STREAM_H

#include <kernel/scheduler/scheduler.h>
#include <stm32f4xx.h>
#include <miosix.h>

/**
 * Enumerating type describing the memory and peripheral data sizes allowed
 * for a DMA transfer.
 */
enum class DataSize
{
    _8BIT  = 0,     ///< 8-bit data size
    _16BIT = 1,     ///< 16-bit data size
    _32BIT = 2,     ///< 32-bit data size
};

/**
 * Stream handler class for STM32F4 DMA Stream peripheral.
 */
class StreamHandler
{
public:

    /**
     * Constructor.
     *
     * @param s: pointer to the DMA stream peripheral.
     * @param IRQn: DMA stream IRQ number.
     */
    StreamHandler(DMA_Stream_TypeDef *s, IRQn_Type IRQn) : IRQn(IRQn), stream(s)
    { }

    /**
     * Destructor.
     */
    ~StreamHandler()
    {
        stream->CR = 0;
        NVIC_DisableIRQ(IRQn);
    }

    /**
     * Start a DMA stream.
     *
     * @param periph: pointer to source/target peripheral.
     * @param memory: pointer to source/target memory.
     * @param size: transfer size, in elements.
     * @param circ: if true the stream runs in circular double buffered mode.
     */
    void start(volatile void *periph, void *memory, const size_t size,
               const bool circ = false)
    {
        uint32_t circFlags = DMA_SxCR_CIRC      // Circular buffer mode
                           | DMA_SxCR_HTIE;     // Half transfer interrupt

        if(circ)
            stream->CR |= circFlags;
        else
            stream->CR &= ~circFlags;

        transferSize = size;
        stream->NDTR = size;
        stream->PAR  = reinterpret_cast< uint32_t >(periph);
        stream->M0AR = reinterpret_cast< uint32_t >(memory);
        stream->CR  |= DMA_SxCR_EN;
    }

    /**
     * Get a pointer to the currently "idle" section of a DMA stream that is,
     * the section not being read by the DMA.
     * A call to this function is meaningful only if the stream is running in
     * circular double buffered mode.
     *
     * @return address of the idle section or NULL if the stream is not in
     * circular mode.
     */
    void *idleBuf()
    {
        if((stream->CR & DMA_SxCR_CIRC) == 0)
            return NULL;

        miosix::FastInterruptDisableLock dLock;
        uint32_t curPos = stream->NDTR;
        uint32_t idle   = stream->M0AR;
        uint32_t size   = (stream->CR >> DMA_SxCR_MSIZE_Pos) & 0x03;

        if(curPos > (transferSize / 2))
            idle += (transferSize / 2) * size * 2;

        return reinterpret_cast< void * >(idle);
    }

    /**
     * Syncronize the execution flow with the stream transfer, blocking function.
     * For cirular buffer mode the calling thread is put to sleep until the DMA
     * reaches either the half or the end of the transfer, whichever comes first.
     * For linear buffer mode the calling thread is put to sleep until the DMA
     * reaches the end of the transfer.
     *
     * @return true if thread was effectively put to sleep, false otherwise.
     */
    bool sync()
    {
        using namespace miosix;

        // Enter in critical section until the end of the function
        FastInterruptDisableLock dLock;

        Thread *curThread = Thread::IRQgetCurrentThread();
        if((waiting != 0) && (waiting != curThread))
            return false;

        waiting = curThread;

        do
        {
            Thread::IRQwait();
            {
                // Re-enable interrupts while waiting for IRQ
                FastInterruptEnableLock eLock(dLock);
                Thread::yield();
            }
        }
        while(waiting != 0);

        waiting = 0;
        return true;
    }

    /**
     * Stop an ongoing DMA stream.
     * The stream does not stop immediately but only when it reaches the half
     * or the end of the transfer.
     */
    inline void stop()
    {
        stopTransfer = true;
    }

    /**
     * Forcefully stop an ongoing DMA stream.
     * Calling this function causes the immediate stop of an ongoing stream.
     */
    inline void halt()
    {
        stopTransfer = true;
        NVIC_SetPendingIRQ(IRQn);
    }

    /**
     * Query che the current status of the stream.
     *
     * @return true if the stream is active, false otherwise.
     */
    inline bool running()
    {
        return (stream->CR & DMA_SxCR_EN) ? true : false;
    }

    /**
     * Register a function to be called on stream end.
     *
     * @param callback: std::function for the stream end callback.
     */
    inline void setEndTransferCallback(std::function<void()>&& callback)
    {
        streamEndCallback = callback;
    }

    /**
     * Stream interrupt handler function.
     *
     * @param irqFlags: IRQ status flags.
     */
    void IRQhandler(const uint32_t irqFlags)
    {
        (void) irqFlags;

        using namespace miosix;

        if(((stream->CR & DMA_SxCR_CIRC) == 0) || (stopTransfer == true))
        {
            stream->CR &= ~DMA_SxCR_EN;
            stopTransfer = false;

            if(streamEndCallback)
                streamEndCallback();
        }

        // Wake up eventual pending threads
        if(waiting == 0) return;
        waiting->IRQwakeup();
        if(waiting->IRQgetPriority()>Thread::IRQgetCurrentThread()->IRQgetPriority())
            Scheduler::IRQfindNextThread();
        waiting = 0;
    }

private:

    bool                  stopTransfer;
    size_t                transferSize;
    const IRQn_Type       IRQn;
    std::function<void()> streamEndCallback;
    DMA_Stream_TypeDef   *stream;
    miosix::Thread       *waiting;
};

/**
 * Stream class for STM32F4 DMA Stream peripheral.
 *
 * @tparam DMA: base address of the DMA peripheral.
 * @tparam STN: DMA stream number.
 * @tparam CH: data channel of the DMA stream.
 * @tparam PL: priority level of the DMA stream.
 */
template < uint32_t DMA, uint8_t STN, uint8_t CH, uint8_t PL >
class DmaStream
{
public:

    /**
     * Initialize a DMA stream without starting it.
     *
     * @param irqPrio: priority of the DMA stream interrupts.
     * @param ds: data size for both source and destination locations.
     * @param memToPer: if set to true, transfer goes from memory to peripheral.
     * @return a StreamHandler object to manage the DMA stream.
     */
    static StreamHandler init(const uint8_t irqPrio, const DataSize ds,
                              const bool memToPer = false)
    {
        enableClock();

        uint32_t dir = memToPer ? DMA_SxCR_DIR_0 : 0;
        uint32_t TS  = static_cast < uint8_t >(ds);
        auto stream  = getStream();
        stream->CR   = (CH << 25)       // Channel
                     | (PL << 16)       // Priority
                     | (TS << 13)       // Source transfer size
                     | (TS << 11)       // Destination transfer size
                     | DMA_SxCR_MINC    // Increment source pointer
                     | dir              // Direction
                     | DMA_SxCR_TCIE    // Transfer complete interrupt
                     | DMA_SxCR_TEIE;   // Transfer error interrupt

        // Enable DMA interrupts
        IRQn_Type irq = IRQn();
        NVIC_ClearPendingIRQ(irq);
        NVIC_SetPriority(irq, irqPrio);
        NVIC_EnableIRQ(irq);

        return StreamHandler(stream, irq);
    }

    /**
     * Low-level shutdown of a DMA stream.
     */
    static inline void terminate()
    {
        NVIC_DisableIRQ(IRQn());
        getStream()->CR = 0;
    }

    /**
     * Stream IRQ handler, forwards the call to the handler inside a StreamHandler
     * class.
     *
     * @param hdl: pointer to the StreamHandler class managing the stream.
     */
    static inline void IRQhandleInterrupt(StreamHandler *hdl)
    {
        uint32_t flags = readIrqFlags();
        hdl->IRQhandler(flags);
        clearIrqFlags(flags);
    }

private:

    /**
     * Enable DMA AHB clock.
     */
    static inline constexpr void enableClock()
    {
        if(reinterpret_cast< DMA_TypeDef *>(DMA) == DMA1)
            RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
        else
            RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;

        RCC_SYNC();
    }

    /**
     * Obtain pointer to the DMA stream peripheral given the DMA and the stream
     * number.
     */
    static inline constexpr DMA_Stream_TypeDef *getStream()
    {
        DMA_Stream_TypeDef *base;
        if(reinterpret_cast< DMA_TypeDef *>(DMA) == DMA1)
            base = DMA1_Stream0;
        else
            base = DMA2_Stream0;

        return base + STN;
    }

    /**
     * Clear the IRQ flags of a DMA stream according to a given mask.
     */
    static inline constexpr void clearIrqFlags(const uint32_t mask)
    {
        uint32_t shift  = (STN % 4) * 8;

        if(STN < 4)
            reinterpret_cast< DMA_TypeDef *>(DMA)->LIFCR = (mask << shift);
        else
            reinterpret_cast< DMA_TypeDef *>(DMA)->HIFCR = (mask << shift);
    }

    /**
     * Get the current IRQ flags of a DMA stream.
     */
    static inline constexpr uint32_t readIrqFlags()
    {
        uint32_t shift  = (STN % 4) * 8;
        uint32_t flags  = 0;

        if(STN < 4)
            flags = reinterpret_cast< DMA_TypeDef *>(DMA)->LISR;
        else
            flags = reinterpret_cast< DMA_TypeDef *>(DMA)->HISR;

        return (flags >> shift) & 0x7D;
    }

    /**
     * Get the IRQ number of a DMA stream.
     */
    static inline constexpr IRQn_Type IRQn()
    {
        //
        // NOTE: there is no better implementation than the one below because
        // DMA stream IRQ numbers are not contiguous...
        //
        if(reinterpret_cast< DMA_TypeDef *>(DMA) == DMA1)
        {
            switch(STN)
            {
                case 0: return DMA1_Stream0_IRQn; break;
                case 1: return DMA1_Stream1_IRQn; break;
                case 2: return DMA1_Stream2_IRQn; break;
                case 3: return DMA1_Stream3_IRQn; break;
                case 4: return DMA1_Stream4_IRQn; break;
                case 5: return DMA1_Stream5_IRQn; break;
                case 6: return DMA1_Stream6_IRQn; break;
                case 7: return DMA1_Stream7_IRQn; break;
            }
        }
        else
        {
            switch(STN)
            {
                case 0: return DMA2_Stream0_IRQn; break;
                case 1: return DMA2_Stream1_IRQn; break;
                case 2: return DMA2_Stream2_IRQn; break;
                case 3: return DMA2_Stream3_IRQn; break;
                case 4: return DMA2_Stream4_IRQn; break;
                case 5: return DMA2_Stream5_IRQn; break;
                case 6: return DMA2_Stream6_IRQn; break;
                case 7: return DMA2_Stream7_IRQn; break;
            }
        }

        return DMA1_Stream0_IRQn;
    }
};

#endif /* DMA_STREAM_H */
