/*
 * 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_FIRE_H
#define ECXX_CTL_GFX_COLOR_ANIMATION_2D_PROC_FIRE_H


#include "Fire2DConfig.h"


namespace eCxx {


//
// A simple 2-dimensional animation processor class - fire with customized color
//
// NOTE : If StopDrawingOnZeroPower 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,
    Fire2DConfig::Flow Flow_,
    Fire2DConfig::Dist Dist_,
    bool               StopDrawingOnZeroPower = false,
    typename           AnimProcColorT         = typename ColorAnimator2D_T::ColorFX2DType::BufferColorType
>
class Animation2DProc_Fire : public ColorAnimator2D_T::Animation2DProcType {
    public:
        using AnimProcColorType   = AnimProcColorT;
        using ColorAnimator2DType = ColorAnimator2D_T;
        using ColorFX2DType       = typename ColorAnimator2D_T::Animation2DProcType::ColorFX2DType;
        using ColorPGMPointerType = typename ColorAnimator2D_T::Animation2DProcType::ColorFX2DType::ColorPGMPointerType;
        using BufferColorType     = typename ColorAnimator2D_T::Animation2DProcType::BufferColorType;
        using BufferSizeType      = typename ColorAnimator2D_T::Animation2DProcType::BufferSizeType;

        using Flow                = Fire2DConfig::Flow;
        using Dist                = Fire2DConfig::Dist;

    public:

        static constexpr CBP::SUInt FirePointCount = (Flow_ == Flow::BottomToTop || Flow_ == Flow::TopToBottom)
                                                   ? ColorAnimator2D_T::Animation2DProcType::ColorFX2DType::XElementCount
                                                   : ColorAnimator2D_T::Animation2DProcType::ColorFX2DType::YElementCount;

        static constexpr CBP::SUInt FireLineLength = (Flow_ == Flow::BottomToTop || Flow_ == Flow::TopToBottom)
                                                   ? ColorAnimator2D_T::Animation2DProcType::ColorFX2DType::YElementCount
                                                   : ColorAnimator2D_T::Animation2DProcType::ColorFX2DType::XElementCount;

        static constexpr uint8_t    MinimumPower   =  4;
        static constexpr uint8_t    MaximumPower   = 32;

    public:
        // # The maxPower must be between MinimumPower and MaximumPower.
        // # The program memory pointer must have (maxPower * AnimProcColorT::RawByteSize) bytes
        //   and have the correct data bytes as expected by AnimProcColorT.
        inline Animation2DProc_Fire(uint8_t maxPower, ColorPGMPointerType colorLUT);

        inline virtual ~Animation2DProc_Fire();

        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 FirePoint {
            uint16_t curPower : 6; // Current power
            uint16_t minPower : 6; // Minimum power
            uint16_t cdFactor : 4; // Cool-down factor
        } __ECXX_ATTR_PACKED;

    private:
        const uint8_t             _maxPower;
        const ColorPGMPointerType _colorLUT;

        FirePoint                 _firePoint[FirePointCount];
};


template <typename ColorAnimator2D_T, Fire2DConfig::Flow Flow_, Fire2DConfig::Dist Dist_, bool StopDrawingOnZeroPower, typename AnimProcColorT>
inline Animation2DProc_Fire<ColorAnimator2D_T, Flow_, Dist_, StopDrawingOnZeroPower, AnimProcColorT>
::Animation2DProc_Fire(uint8_t maxPower, ColorPGMPointerType colorLUT)
: _maxPower( eCxx::max<uint8_t>( MinimumPower, eCxx::min<uint8_t>(MaximumPower, maxPower) ) - 1 )
, _colorLUT( colorLUT                                                                           )
{}


template <typename ColorAnimator2D_T, Fire2DConfig::Flow Flow_, Fire2DConfig::Dist Dist_, bool StopDrawingOnZeroPower, typename AnimProcColorT>
inline Animation2DProc_Fire<ColorAnimator2D_T, Flow_, Dist_, StopDrawingOnZeroPower, AnimProcColorT>
::~Animation2DProc_Fire()
{}


template <typename ColorAnimator2D_T, Fire2DConfig::Flow Flow_, Fire2DConfig::Dist Dist_, bool StopDrawingOnZeroPower, typename AnimProcColorT>
void Animation2DProc_Fire<ColorAnimator2D_T, Flow_, Dist_, StopDrawingOnZeroPower, AnimProcColorT>
::reset(ColorFX2DType&, BufferSizeType, BufferSizeType, BufferSizeType, BufferSizeType)
{ memset( _firePoint, 0, sizeof(_firePoint) ); }


template <typename ColorAnimator2D_T, Fire2DConfig::Flow Flow_, Fire2DConfig::Dist Dist_, bool StopDrawingOnZeroPower, typename AnimProcColorT>
void Animation2DProc_Fire<ColorAnimator2D_T, Flow_, Dist_, StopDrawingOnZeroPower, AnimProcColorT>
::update(ColorFX2DType& colorFX2D, BufferSizeType elemXBeginIdx, BufferSizeType elemYBeginIdx, BufferSizeType elemXEndIdx, BufferSizeType elemYEndIdx)
{
#if defined(__x86_64__) || defined(__i386__)
//    #define DEBUG_F2DGEN
#endif

#ifdef DEBUG_F2DGEN
    static bool firstCall = true;
#endif

    // Determine the begin and end indexes
    const bool           vertical      = (Flow_ == Flow::BottomToTop || Flow_ == Flow::TopToBottom);
    const BufferSizeType elemPBeginIdx = vertical ? elemXBeginIdx : elemYBeginIdx;
    const BufferSizeType elemPEndIdx   = vertical ? elemXEndIdx   : elemYEndIdx;
    const BufferSizeType elemLBeginIdx = vertical ? elemYBeginIdx : elemXBeginIdx;
    const BufferSizeType elemLEndIdx   = vertical ? elemYEndIdx   : elemXEndIdx;

    // Calculate the actual number of fire points
    const BufferSizeType actualPointCount = elemPEndIdx - elemPBeginIdx + 1;

    // Calculate the actual length of the fire lines
    const BufferSizeType actualLineLength = elemLEndIdx - elemLBeginIdx + 1;

    // Fire pixel color
    AnimProcColorType fpColor;

    // Draw the fire line(s)
    for(uint8_t p = elemPBeginIdx; p <= elemPEndIdx; ++p) {

        // Get the current fire point
        FirePoint& fp = _firePoint[p];

        // Create a new fire point as needed
        if(!fp.curPower) {
            __if_constexpr(Dist_ == Dist::Uniform) {
                fp.curPower = Application::random8(_maxPower / 2, _maxPower);
                fp.minPower = Application::random8(_maxPower / 2, _maxPower);
                fp.cdFactor = Application::random8(1,             3        );
            }
            else __if_constexpr(Dist_ == Dist::TowardsMiddle) {
                const uint8_t xcd2     = actualPointCount / 2;
                const uint8_t xrel     = p - elemPBeginIdx;
                const uint8_t delta    = xcd2 - ( ( xcd2 >= (xrel + 1) ) ? (xcd2 - xrel - 1) : (xrel - xcd2) );
                const uint8_t maxPower = static_cast<uint16_t>(_maxPower) * delta / xcd2;
                const uint8_t maxCDF   = 3 - 2 * delta / xcd2;
                fp.curPower = Application::random8(maxPower / 2, maxPower);
                fp.minPower = Application::random8(maxPower / 2, maxPower);
                fp.cdFactor = Application::random8(1,            maxCDF  );
#ifdef DEBUG_F2DGEN
                if(firstCall) printf("TowardsMiddle: p=%02d delta=%02d maxPower=%02d maxCDF=%d\n", (int) p, (int) delta, (int) maxPower, (int) maxCDF);
#endif
            }
            else __if_constexpr(Dist_ == Dist::TowardsEdge) {
                const uint8_t xcd2     = actualPointCount / 2;
                const uint8_t xrel     = p - elemPBeginIdx;
                const uint8_t delta    = ( xcd2 >= (xrel + 1) ) ? (xcd2 - xrel - 1) : (xrel - xcd2);
                const uint8_t maxPower = static_cast<uint16_t>(_maxPower) * delta / xcd2;
                const uint8_t maxCDF   = 1 + 3 * delta / xcd2;
#ifdef DEBUG_F2DGEN
                if(firstCall) printf("TowardsEdge: p=%02d delta=%02d maxPower=%02d maxCDF=%d\n", (int) p, (int) delta, (int) maxPower, (int) maxCDF);
#endif
                fp.curPower = Application::random8(maxPower / 2, maxPower);
                fp.minPower = Application::random8(maxPower / 2, maxPower);
                fp.cdFactor = Application::random8(1,            maxCDF  );
            }
            else __if_constexpr(Dist_ == Dist::TowardsFront) {
                const uint8_t xrel     = p - elemPBeginIdx;
                const uint8_t maxPower = static_cast<uint16_t>(_maxPower) * (actualPointCount - xrel - 1) / actualPointCount;
                const uint8_t maxCDF   = 3 - 2 * xrel / actualPointCount;
#ifdef DEBUG_F2DGEN
                if(firstCall) printf("TowardsFront: p=%02d maxPower=%02d maxCDF=%d\n", (int) p, (int) maxPower, (int) maxCDF);
#endif
                fp.curPower = Application::random8(maxPower / 2, maxPower);
                fp.minPower = Application::random8(maxPower / 2, maxPower);
                fp.cdFactor = Application::random8(1,            maxCDF  );
            }
            else __if_constexpr(Dist_ == Dist::TowardsBack) {
                const uint8_t xrel     = p - elemPBeginIdx;
                const uint8_t maxPower = static_cast<uint16_t>(_maxPower) * xrel / actualPointCount;
                const uint8_t maxCDF   = 1 + 3 * xrel / actualPointCount;
#ifdef DEBUG_F2DGEN
                if(firstCall) printf("TowardsBack: p=%02d maxPower=%02d maxCDF=%d\n", (int) p, (int) maxPower, (int) maxCDF);
#endif
                fp.curPower = Application::random8(maxPower / 2, maxPower);
                fp.minPower = Application::random8(maxPower / 2, maxPower);
                fp.cdFactor = Application::random8(1,            maxCDF  );
            }
        }

        // Draw the fire line from the starting point
        uint8_t curPower  = fp.curPower;
        uint8_t subFactor = static_cast<uint16_t>(fp.cdFactor   )
                          * static_cast<uint16_t>(FireLineLength)
                          / actualLineLength;
        uint8_t lIdx      = actualLineLength;
        for(;;) {
            // Get the color
            fpColor.pgmGet( _colorLUT.offset(curPower * AnimProcColorType::RawByteSize) );
            // Draw the pixel
                 __if_constexpr(Flow_ == Flow::BottomToTop) colorFX2D.setXY(p, lIdx             - 1    + elemLBeginIdx, fpColor);
            else __if_constexpr(Flow_ == Flow::TopToBottom) colorFX2D.setXY(p, actualLineLength - lIdx + elemLBeginIdx, fpColor);
            else __if_constexpr(Flow_ == Flow::LeftToRight) colorFX2D.setXY(actualLineLength - lIdx + elemLBeginIdx, p, fpColor);
            else __if_constexpr(Flow_ == Flow::RightToLeft) colorFX2D.setXY(lIdx             - 1    + elemLBeginIdx, p, fpColor);
            // Decrement the index and break it has reached the boundary
            --lIdx;
            if(!lIdx) break;
            // Decrement the power if it is not zero yet
            curPower = (curPower > subFactor) ? (curPower - subFactor) : 0;
            // If configured, stop drawing if the power has reached zero
            __if_constexpr(StopDrawingOnZeroPower) {
                if(!curPower) break;
            }
        }

        // Update the power
        if(fp.curPower >= fp.minPower) {
            fp.curPower = (fp.curPower > fp.cdFactor) ? (fp.curPower - fp.cdFactor) : 0;
        }
        if(fp.curPower < fp.minPower) fp.curPower = 0;

    } // for p

#ifdef DEBUG_F2DGEN
    firstCall = false;
#endif
}


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

template <typename ColorAnimator2D_T, Fire2DConfig::Flow Flow_, Fire2DConfig::Dist Dist_, bool StopDrawingOnZeroPower, typename AnimProcColorT>
CBP::QUInt Animation2DProc_Fire<ColorAnimator2D_T, Flow_, Dist_, StopDrawingOnZeroPower, AnimProcColorT>
::estimatedTotalAnimationFrameCount() const
{ return static_cast<CBP::QUInt>(FirePointCount) * static_cast<CBP::QUInt>(_maxPower) * 2; }

#endif


} // namespace eCxx


#endif
