/*
 * Copyright (C) 2020-2021 Aloysius Indrayanto
 *
 * This file is part of the eCxx library, see LICENSE file for the license details.
 */


#ifndef ECXX_CTL_GFX_COLOR_ANIMATION_2D_PROC_RAIN_H
#define ECXX_CTL_GFX_COLOR_ANIMATION_2D_PROC_RAIN_H


namespace eCxx {


//
// A class to hold Animation2DProc_Rain configuration specifiers
//
class Rain2DConfig : public Uninstantiable {
    public:
        //
        // Direction
        //
        enum class Dir : uint8_t {
            Down,
            Up,
            Right,
            Left
        };
};


//
// A simple 2-dimensional animation processor class - rain with customized color
//
// NOTE : If SkipDrawingClearPixel is set to true, the corresponding ColorFX2D<T> instance
//        will need to be cleared manually, either by calling its clear() method or filling
//        its buffer via another methods.
//
template <
    typename          ColorAnimator2D_T,
    uint8_t           RainPointCountN,
    Rain2DConfig::Dir Dir_,
    bool              SkipDrawingClearPixel = false,
    typename          AnimProcColorT        = typename ColorAnimator2D_T::ColorFX2DType::BufferColorType
>
class Animation2DProc_Rain : public ColorAnimator2D_T::Animation2DProcType {
    static_assert(
        (RainPointCountN >= 1) && (RainPointCountN <= 250),
        "template argument substituting RainPointCountN is not between 1 and 250"
    );

    static_assert(
        RainPointCountN <= ColorAnimator2D_T::ColorFX2DType::BufferElementCount,
        "template argument substituting RainPointCountN is larger than the number of elements contained by the type that substitutes the template argument ColorAnimator2D_T::ColorFX2DType"
    );

    public:
        using AnimProcColorType   = AnimProcColorT;
        using ColorAnimator2DType = ColorAnimator2D_T;
        using ColorFX2DType       = typename ColorAnimator2D_T::Animation2DProcType::ColorFX2DType;
        using BufferColorType     = typename ColorAnimator2D_T::Animation2DProcType::BufferColorType;
        using BufferSizeType      = typename ColorAnimator2D_T::Animation2DProcType::BufferSizeType;

        using Dir                 = Rain2DConfig::Dir;

        static constexpr uint8_t RainPointCount = RainPointCountN;

    public:
        inline Animation2DProc_Rain(const AnimProcColorType& colorOff, const AnimProcColorType& colorDim, const AnimProcColorType& colorMedium, const AnimProcColorType& colorBright);
        inline virtual ~Animation2DProc_Rain();

        virtual void reset(ColorFX2DType& colorFX2D, BufferSizeType elemXBeginIdx, BufferSizeType elemYBeginIdx, BufferSizeType elemXEndIdx, BufferSizeType elemYEndIdx);
        virtual void update(ColorFX2DType& colorFX2D, BufferSizeType elemXBeginIdx, BufferSizeType elemYBeginIdx, BufferSizeType elemXEndIdx, BufferSizeType elemYEndIdx);

#if defined(__x86_64__) || defined(__i386__)
        // NOTE : This animation processor implementation does not require reset() to be
        //        called first for this function to work properly
        virtual CBP::QUInt estimatedTotalAnimationFrameCount() const;
#endif

    private:
        struct RainPoint {
            uint8_t x;
            uint8_t y;
            uint8_t s;
        } __ECXX_ATTR_PACKED;

    private:
        const AnimProcColorType _colors[4];

        RainPoint               _rainPoint[RainPointCount];
};


template <typename ColorAnimator2D_T, uint8_t RainPointCountN, Rain2DConfig::Dir Dir_, bool SkipDrawingClearPixel, typename AnimProcColorT>
inline Animation2DProc_Rain<ColorAnimator2D_T, RainPointCountN, Dir_, SkipDrawingClearPixel, AnimProcColorT>
::Animation2DProc_Rain(const AnimProcColorType& colorOff, const AnimProcColorType& colorDim, const AnimProcColorType& colorMedium, const AnimProcColorType& colorBright)
: _colors{ colorOff, colorBright, colorDim, colorMedium }
{}


template <typename ColorAnimator2D_T, uint8_t RainPointCountN, Rain2DConfig::Dir Dir_, bool SkipDrawingClearPixel, typename AnimProcColorT>
inline Animation2DProc_Rain<ColorAnimator2D_T, RainPointCountN, Dir_, SkipDrawingClearPixel, AnimProcColorT>
::~Animation2DProc_Rain()
{}


template <typename ColorAnimator2D_T, uint8_t RainPointCountN, Rain2DConfig::Dir Dir_, bool SkipDrawingClearPixel, typename AnimProcColorT>
void Animation2DProc_Rain<ColorAnimator2D_T, RainPointCountN, Dir_, SkipDrawingClearPixel, AnimProcColorT>
::reset(ColorFX2DType&, BufferSizeType, BufferSizeType, BufferSizeType, BufferSizeType)
{ memset( _rainPoint, 0, sizeof(_rainPoint) ); }


template <typename ColorAnimator2D_T, uint8_t RainPointCountN, Rain2DConfig::Dir Dir_, bool SkipDrawingClearPixel, typename AnimProcColorT>
void Animation2DProc_Rain<ColorAnimator2D_T, RainPointCountN, Dir_, SkipDrawingClearPixel, AnimProcColorT>
::update(ColorFX2DType& colorFX2D, BufferSizeType elemXBeginIdx, BufferSizeType elemYBeginIdx, BufferSizeType elemXEndIdx, BufferSizeType elemYEndIdx)
{
    // Helper function to draw vertical line
    auto drawV = [](ColorFX2DType& colorFX2D, const AnimProcColorType& col, int16_t minY, int16_t maxY, uint8_t x, int16_t yFrom, int16_t yTo) {
        for(int16_t y = yFrom; y <= yTo; ++y) {
            if(y < minY) continue;
            if(y > maxY) break;
            colorFX2D.setXY(x, y, col);
        }
    };

    // Helper function to draw horizontal line
    auto drawH = [](ColorFX2DType& colorFX2D, const AnimProcColorType& col, int16_t minX, int16_t maxX, int16_t xFrom, int16_t xTo, uint8_t y) {
        for(int16_t x = xFrom; x <= xTo; ++x) {
            if(x < minX) continue;
            if(x > maxX) break;
            colorFX2D.setXY(x, y, col);
        }
    };

    // Draw the rain lines
    for(uint8_t p = 0; p < RainPointCount; ++p) {

        // Get the current rain point
        RainPoint& rp = _rainPoint[p];

        // Process based on the direction
        __if_constexpr(Dir_ == Dir::Down) {
            // Create a new rain point
            if(!rp.s) {
                // Calculate a new coordinate and speed
                rp.x = Application::random8(elemXBeginIdx, elemXEndIdx);
                rp.y = elemYBeginIdx;
                rp.s = Application::random8(1, 3);
                // Draw the new rain line
                drawV(colorFX2D, _colors[rp.s], elemYBeginIdx, elemYEndIdx, rp.x, rp.y, static_cast<int16_t>(rp.y) + rp.s - 1);
            }
            // Update the rain point
            else {
                // Calculate the new position
                const int16_t n = static_cast<int16_t>(rp.y) + rp.s;
                // Check if the new position is out bound
                if( n > static_cast<int16_t>(elemYEndIdx) ) {
                    // Erase the old rain line (unless it is configured to skip drawing clear pixel)
                    __if_constexpr(!SkipDrawingClearPixel) {
                        drawV(colorFX2D, _colors[0], elemYBeginIdx, elemYEndIdx, rp.x, rp.y, static_cast<int16_t>(rp.y) + rp.s - 1);
                    }
                    // Remove the rain point
                    rp.s = 0;
                }
                // The new position is in bound
                else {
                    // Erase the old rain line (unless it is configured to skip drawing clear pixel)
                    __if_constexpr(!SkipDrawingClearPixel) {
                        drawV(colorFX2D, _colors[0], elemYBeginIdx, elemYEndIdx, rp.x, rp.y, static_cast<int16_t>(rp.y) + rp.s - 1);
                    }
                    // Set the rain point's new position
                    rp.y = n;
                    // Draw the new rain line
                    drawV(colorFX2D, _colors[rp.s], elemYBeginIdx, elemYEndIdx, rp.x, rp.y, static_cast<int16_t>(rp.y) + rp.s - 1);
                }
            }
        }
        else __if_constexpr(Dir_ == Dir::Up) {
            // Create a new rain point
            if(!rp.s) {
                // Calculate a new coordinate and speed
                rp.x = Application::random8(elemXBeginIdx, elemXEndIdx);
                rp.y = elemYEndIdx;
                rp.s = Application::random8(1, 3);
                // Draw the new rain line
                drawV(colorFX2D, _colors[rp.s], elemYBeginIdx, elemYEndIdx, rp.x, static_cast<int16_t>(rp.y) - rp.s + 1, rp.y);
            }
            // Update the rain point
            else {
                // Calculate the new position
                const int16_t n = static_cast<int16_t>(rp.y) - rp.s;
                // Check if the new position is out bound
                if( n < static_cast<int16_t>(elemYBeginIdx) ) {
                    // Erase the old rain line (unless it is configured to skip drawing clear pixel)
                    __if_constexpr(!SkipDrawingClearPixel) {
                        drawV(colorFX2D, _colors[0], elemYBeginIdx, elemYEndIdx, rp.x, static_cast<int16_t>(rp.y) - rp.s + 1, rp.y);
                    }
                    // Remove the rain point
                    rp.s = 0;
                }
                // The new position is in bound
                else {
                    // Erase the old rain line (unless it is configured to skip drawing clear pixel)
                    __if_constexpr(!SkipDrawingClearPixel) {
                        drawV(colorFX2D, _colors[0], elemYBeginIdx, elemYEndIdx, rp.x, static_cast<int16_t>(rp.y) - rp.s + 1, rp.y);
                    }
                    // Set the rain point's new position
                    rp.y = n;
                    // Draw the new rain line
                    drawV(colorFX2D, _colors[rp.s], elemYBeginIdx, elemYEndIdx, rp.x, static_cast<int16_t>(rp.y) - rp.s + 1, rp.y);
                }
            }
        }
        else __if_constexpr(Dir_ == Dir::Right) {
            // Create a new rain point
            if(!rp.s) {
                // Calculate a new coordinate and speed
                rp.x = elemXBeginIdx;
                rp.y = Application::random8(elemYBeginIdx, elemYEndIdx);
                rp.s = Application::random8(1, 3);
                // Draw the new rain line
                drawH(colorFX2D, _colors[rp.s], elemXBeginIdx, elemXEndIdx, rp.x, static_cast<int16_t>(rp.x) + rp.s - 1, rp.y);
            }
            // Update the rain point
            else {
                // Calculate the new position
                const int16_t n = static_cast<int16_t>(rp.x) + rp.s;
                // Check if the new position is out bound
                if( n > static_cast<int16_t>(elemXEndIdx) ) {
                    // Erase the old rain line (unless it is configured to skip drawing clear pixel)
                    __if_constexpr(!SkipDrawingClearPixel) {
                        drawH(colorFX2D, _colors[0], elemXBeginIdx, elemXEndIdx, rp.x, static_cast<int16_t>(rp.x) + rp.s - 1, rp.y);
                    }
                    // Remove the rain point
                    rp.s = 0;
                }
                // The new position is in bound
                else {
                    // Erase the old rain line (unless it is configured to skip drawing clear pixel)
                    __if_constexpr(!SkipDrawingClearPixel) {
                        drawH(colorFX2D, _colors[0], elemXBeginIdx, elemXEndIdx, rp.x, static_cast<int16_t>(rp.x) + rp.s - 1, rp.y);
                    }
                    // Set the rain point's new position
                    rp.x = n;
                    // Draw the new rain line
                    drawH(colorFX2D, _colors[rp.s], elemXBeginIdx, elemXEndIdx, rp.x, static_cast<int16_t>(rp.x) + rp.s - 1, rp.y);
                }
            }
        }
        else __if_constexpr(Dir_ == Dir::Left) {
            // Create a new rain point
            if(!rp.s) {
                // Calculate a new coordinate and speed
                rp.x = elemXEndIdx;
                rp.y = Application::random8(elemYBeginIdx, elemYEndIdx);
                rp.s = Application::random8(1, 3);
                // Draw the new rain line
                drawH(colorFX2D, _colors[rp.s], elemXBeginIdx, elemXEndIdx, static_cast<int16_t>(rp.x) - rp.s + 1, rp.x, rp.y);
            }
            // Update the rain point
            else {
                // Calculate the new position
                const int16_t n = static_cast<int16_t>(rp.x) - rp.s;
                // Check if the new position is out bound
                if( n < static_cast<int16_t>(elemXBeginIdx) ) {
                    // Erase the old rain line (unless it is configured to skip drawing clear pixel)
                    __if_constexpr(!SkipDrawingClearPixel) {
                        drawH(colorFX2D, _colors[0], elemXBeginIdx, elemXEndIdx, static_cast<int16_t>(rp.x) - rp.s + 1, rp.x, rp.y);
                    }
                    // Remove the rain point
                    rp.s = 0;
                }
                // The new position is in bound
                else {
                    // Erase the old rain line (unless it is configured to skip drawing clear pixel)
                    __if_constexpr(!SkipDrawingClearPixel) {
                        drawH(colorFX2D, _colors[0], elemXBeginIdx, elemXEndIdx, static_cast<int16_t>(rp.x) - rp.s + 1, rp.x, rp.y);
                    }
                    // Set the rain point's new position
                    rp.x = n;
                    // Draw the new rain line
                    drawH(colorFX2D, _colors[rp.s], elemXBeginIdx, elemXEndIdx, static_cast<int16_t>(rp.x) - rp.s + 1, rp.x, rp.y);
                }
            }
        }

    } // for p
}


#if defined(__x86_64__) || defined(__i386__)

template <typename ColorAnimator2D_T, uint8_t RainPointCountN, Rain2DConfig::Dir Dir_, bool SkipDrawingClearPixel, typename AnimProcColorT>
CBP::QUInt Animation2DProc_Rain<ColorAnimator2D_T, RainPointCountN, Dir_, SkipDrawingClearPixel, AnimProcColorT>
::estimatedTotalAnimationFrameCount() const
{
    return static_cast<CBP::QUInt>( RainPointCount )
           *
           static_cast<CBP::QUInt>( (Dir_ == Dir::Down || Dir_ == Dir::Up) ? ColorFX2DType::YElementCount
                                                                           : ColorFX2DType::XElementCount
                                  );
}

#endif


} // namespace eCxx


#endif
