///////////////////////////////////////////////////////////////////////////////////////////////////
// RainGauge.h
//
// Calculation of hourly (past 60 minutes), daily, weekly and monthly rainfall
// from raw rain gauge data.
//
// Non-volatile data is stored in the ESP32's RTC RAM to allow retention during deep sleep mode.
//
// https://github.com/matthias-bs/BresserWeatherSensorReceiver
//
//
// created: 08/2022
//
//
// MIT License
//
// Copyright (c) 2025 Matthias Prinke
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// History:
//
// 20220830 Created
// 20230330 Added changes for Adafruit Feather 32u4 LoRa Radio
// 20230716 Implemented sensor startup handling
// 20230817 Implemented partial reset
// 20231227 Added prerequisites for storing rain data in preferences
// 20231218 Implemented storing of rain data in preferences, 
//          new algorithm for past 60 minutes rainfall
// 20240118 Changed raingaugeMax to class member set by constructor
// 20240119 Changed preferences to class member
//          Modified update at the same index as before
//          Modified pastHour() algorithm and added features
//          Changed hist[] width
// 20240120 Added set_max()
//          Removed old implementation
// 20240122 Changed scope of nvData -
//          Using RTC RAM: global
//          Using Preferences, Unit Tests: class member
// 20250323 Added configuration of expected update rate at run-time
//          pastHour(): modified parameters
// 20260211 Added past24Hours()
//          Refactored to use RollingCounter base class
// 20260221 Improved RollingCounter generalization, documentation, and code deduplication
//
// ToDo: 
// -
//
///////////////////////////////////////////////////////////////////////////////////////////////////

#ifndef _RAINGAUGE_H
#define _RAINGAUGE_H

#include "time.h"
#if defined(ESP32) || defined(ESP8266)
  #include <sys/time.h>
#endif
#include "RollingCounter.h"
#if defined(RAINGAUGE_USE_PREFS) && !defined(INSIDE_UNITTEST)
    #include <Preferences.h>
#endif

/**
 * \def
 * 
 * Set to the value which leads to a reset of the rain gauge output to zero.
 */
#define RAINGAUGE_MAX_VALUE 1000
//#define RAINGAUGE_MAX_VALUE 20000

/**
 * \def
 * 
 * Lightning sensor update rate [min]
 */
#define RAINGAUGE_UPD_RATE 6

/**
 * \def
 * 
 * Set to 3600 [sec] / min_update_rate_rate [sec]
 */
#define RAIN_HIST_SIZE 10

/**
 * \def RAIN_HIST_SIZE_24H
 * 
 * Size of 24-hour rain history buffer (24 hours / 1 hour per bin)
 */
#define RAIN_HIST_SIZE_24H 24


/**
 * \defgroup Reset rain counters
 */
 #define RESET_RAIN_H 1
 #define RESET_RAIN_D 2
 #define RESET_RAIN_W 4
 #define RESET_RAIN_M 8
 #define RESET_RAIN_24H 16


/**
 * \typedef nvData_t
 *
 * \brief Data structure for rain statistics to be stored in non-volatile memory
 */
typedef struct {
    /* Timestamp of last update */
    time_t    lastUpdate;

    /* Data of past 60 minutes */
    int16_t   hist[RAIN_HIST_SIZE];

    /* Data of past 24 hours */
    int16_t   hist24h[RAIN_HIST_SIZE_24H];

    /* Sensor startup handling */
    bool      startupPrev; // previous state of startup
    float     rainPreStartup; // previous rain gauge reading (before startup)

    /* Rainfall of current day (can start anytime, but will reset on begin of new day) */
    uint8_t   tsDayBegin; // day of week
    float     rainDayBegin; // rain gauge @ begin of day

    /* Rainfall of current week (can start anytime, but will reset on Monday */
    uint8_t   tsWeekBegin; // day of week 
    float     rainWeekBegin; // rain gauge @ begin of week
    uint8_t   wdayPrev; // day of week at previous run - to detect new week

    /* Rainfall of current calendar month (can start anytime, but will reset at begin of month */
    uint8_t   tsMonthBegin; // month
    float     rainMonthBegin; // rain gauge @ begin of month

    float     rainPrev;  // rain gauge at previous run - to detect overflow
    float     rainAcc; // accumulated rain (overflows and startups)

    uint8_t   updateRate; // update rate for pastHour() calculation
} nvData_t;

/**
 * \class RainGauge
 *
 * \brief Calculation of hourly (past 60 minutes), daily, weekly and monthly rainfall
 *
 * Additionally overflow of the rain gauge is handled when reaching RAINGAUGE_MAX_VALUE. 
 */
class RainGauge : public RollingCounter {
private:
    float rainCurr;
    float raingaugeMax;

    #if defined(RAINGAUGE_USE_PREFS) || defined(INSIDE_UNITTEST)
    nvData_t nvData = {
        .lastUpdate = 0,
        .hist = {-1},
        .hist24h = {-1},
        .startupPrev = false,
        .rainPreStartup = 0,
        .tsDayBegin = 0xFF,
        .rainDayBegin = 0,
        .tsWeekBegin = 0xFF,
        .rainWeekBegin = 0,
        .wdayPrev = 0xFF,
        .tsMonthBegin = 0xFF,
        .rainMonthBegin = 0,
        .rainPrev = 0,
        .rainAcc = 0,
        .updateRate = RAINGAUGE_UPD_RATE
    };
    #endif
    #if defined(RAINGAUGE_USE_PREFS) && !defined(INSIDE_UNITTEST)
    Preferences preferences;
    #endif

public:
    /**
     * Constructor
     * 
     * \param raingauge_max     raingauge value which causes a counter overflow
     * \param quality_threshold fraction of valid rain_hist entries required for valid pastHour() result
     */
    RainGauge(const float raingauge_max = RAINGAUGE_MAX_VALUE, const float quality_threshold = DEFAULT_QUALITY_THRESHOLD) :
        RollingCounter(quality_threshold),
        raingaugeMax(raingauge_max)
    {};

    /**
     * Set maximum rain counter value
     * 
     * \param raingauge_max     raingauge value which causes a counter overflow
     */
    void set_max(float raingauge_max)
    {
        raingaugeMax = raingauge_max;
    }
    
    /**
     * \brief Set expected update rate for pastHour() calculation
     * 
     * RAIN_HIST_SIZE: number of entries in rain_hist[]
     * updateRate: update rate in minutes
     * 
     * 60 minutes / updateRate = no_of_hist_bins
     * The resulting number of history bins must be an integer value which
     * does not exceed RAIN_HIST_SIZE.
     * 
     * Examples: 
     * 
     * 1. updateRate =  6 -> 60 / 6 = 10 entries
     * 2. updateRate = 12 -> 60 / 12 = 5 entries
     * 
     * Changing the update rate will reset the history buffer, therefore
     * the caller should avoid frequent changes.
     * 
     * Actual update intervals shorter than updateRate will lead to a reduced
     * resolution of the pastHour() result and a higher risk of an invalid
     * result if a bin in the history buffer was missed.
     * 
     * Actual update intervals longer than updateRate will lead to an invalid
     * result, because bins in the history buffer will be missed.
     * 
     * \param rate    update rate in minutes (default: 6)
     * \return true if rate is valid and was set, false otherwise
     */
    bool setUpdateRate(uint8_t rate = RAINGAUGE_UPD_RATE) {
        // Validate rate: must be > 0, must evenly divide 60, and result must fit in buffer
        if (rate == 0) {
            log_w("setUpdateRate: rate cannot be 0");
            return false;
        }
        if (60 % rate != 0) {
            log_w("setUpdateRate: rate=%u must evenly divide 60 minutes", rate);
            return false;
        }
        if (60 / rate > RAIN_HIST_SIZE) {
            log_w("setUpdateRate: rate=%u would require %u bins, but only %u available",
                  rate, 60 / rate, RAIN_HIST_SIZE);
            return false;
        }
        
        #if !defined(INSIDE_UNITTEST)
        preferences.begin("BWS-RAIN", false);
        uint8_t updateRatePrev = preferences.getUChar("updateRate", RAINGAUGE_UPD_RATE);
        preferences.putUChar("updateRate", rate);
        preferences.end();
        #else
        static uint8_t updateRatePrev = RAINGAUGE_UPD_RATE;
        updateRatePrev = nvData.updateRate;
        #endif
        nvData.updateRate = rate;
        if (nvData.updateRate != updateRatePrev) {
            hist_init();
        }
        return true;
    }

    /**
     * Reset non-volatile data and current rain counter value
     * 
     * \param flags Flags defining what to reset:
     */
    void reset(uint8_t flags=0x1F);
    
    /**
     * Initialize history buffer for hourly (past 60 minutes) rainfall
     * 
     * \param rain  initial value for all entries (default: -1 for invalid)
     */
    void hist_init(int16_t rain = -1) override;
    
    /**
     * Initialize 24-hour history buffer
     * 
     * \param rain  initial value for all entries (default: -1 for invalid)
     */
    void hist24h_init(int16_t rain = -1);

    #if defined(RAINGAUGE_USE_PREFS) && !defined(INSIDE_UNITTEST)
    void prefs_load(void);
    void prefs_save(void);
    #endif

    /**
     * \fn update
     * 
     * \brief Update rain gauge statistics
     * 
     * \param ts           timestamp
     * 
     * \param rain         rain gauge raw value (in mm/m²)
     * 
     * \param startup      sensor startup flag
     */
    void  update(time_t ts, float rain, bool startup = false);
    
    /**
     * Rainfall during past 60 minutes
     * 
     * \param valid     number of valid entries in rain_hist >= qualityThreshold * 60 / updateRate
     * \param nbins     number of valid entries in rain_hist
     * \param quality   fraction of valid entries in rain_hist (0..1); quality = nbins / (60 / updateRate)
     * 
     * \returns amount of rain during past 60 minutes
     */
    float pastHour(bool *valid = nullptr, int *nbins = nullptr, float *quality = nullptr);

    /**
     * Rainfall during past 24 hours
     * 
     * \param valid     number of valid entries in rain_hist24h >= qualityThreshold * 24
     * \param nbins     number of valid entries in rain_hist24h
     * \param quality   fraction of valid entries in rain_hist24h (0..1); quality = nbins / 24
     * 
     * \returns amount of rain during past 24 hours
     */
    float past24Hours(bool *valid = nullptr, int *nbins = nullptr, float *quality = nullptr);

    /**
     * Rainfall of current calendar day
     * 
     * \returns amount of rain
     */
    float currentDay(void);
    
    /**
     * Rainfall of current calendar week
     * 
     * \returns amount of rain
     */
    float currentWeek(void);
    
    /**
     * Rainfall of current calendar month
     * 
     * \returns amount of rain
     */
    float currentMonth(void);
};
#endif // _RAINGAUGE_H
