/*
 * 模块名称：System Clock — Wall Clock Time Maintenance
 * 模块功能：实现系统墙钟时间维护，包括 RTC 初始读取、HAL Tick 漂移维持
 * 适用平台：STM32F407ZGTx (Cortex-M4 FPU, 168MHz)
 * 作者：王建锋
 * 创建日期：2026-07-19
 * 修改记录：
 *   v1.0  2026-07-19  王建锋  创建初始版本
 */

#include "sys_clock.h"
#include "main.h"
#include <string.h>
#include <stdio.h>

#define DBG_TAG "[CLK]"
#include "dbg_log.h"

/* ============================================================
 * RTC 校时（开发期一次性）
 * 使能 RTC_DEFAULT_INIT_ENABLE 后：
 *   RTC_DEFAULT_INIT_FORCE=1：每次启动都写入默认时间（首次/覆盖校时用）
 *   RTC_DEFAULT_INIT_FORCE=0：仅当 RTC 时间无效（越界）时写入
 * 校时完成后建议将 FORCE 置 0（或注释 ENABLE），避免每次开机覆盖用户设的时间。
 * 修改日期/时间时同步更新下列宏。
 * ============================================================ */
#define RTC_DEFAULT_INIT_ENABLE   1
#define RTC_DEFAULT_INIT_FORCE    0
#define RTC_DEFAULT_YEAR    2026
#define RTC_DEFAULT_MONTH   8
#define RTC_DEFAULT_DAY     24
#define RTC_DEFAULT_HOUR    0
#define RTC_DEFAULT_MINUTE  0
#define RTC_DEFAULT_SECOND  0

/* ============================================================
 * 内部状态：系统墙钟
 * ============================================================ */

static volatile uint32_t s_base_tick = 0;  /* HAL_GetTick() 基准 */
static volatile uint32_t s_base_unix_secs = UINT32_MAX;

/* ============================================================
 * 内部辅助函数：Unix time ↔ sd2506_time_t 转换
 * ============================================================ */

/*
 * 函数功能：根据公历日期计算星期几
 * 入口参数：y - 年份      uint16_t  1970~2099
 *           m - 月份      uint8_t   1~12
 *           d - 日        uint8_t   1~31
 * 返回值：星期编号        uint8_t   0=Sunday, 1=Monday, ... 6=Saturday
 * 限定条件：输入为公历合法日期
 * 函数说明：1. 采用 Zeller 公式（变种），结果 0 表示周日
 *           2. 与 unix_to_sd2506() 内星期算法保持一致
 */
static uint8_t rtc_calc_week(uint16_t y, uint8_t m, uint8_t d)
{
    uint32_t wy = y;
    uint32_t wm = m;

    if (wm <= 2) {
        wy--;
        wm += 12u;
    }
    /* Zeller 公式：世纪项 + 月项 + 日项，模 7 得星期（0=周日） */
    return (uint8_t)(((uint32_t)(wy % 100u + wy / 100u / 4u - wy / 100u / 15u) +
                      (uint32_t)((26u * (wm + 1u)) / 10u) + (uint32_t)(d % 100u)) % 7u);
}

/*
 * 函数功能：将 Unix 时间戳（自 1970-01-01 00:00:00 起的秒数）转换为 SD2506 RTC 时间结构
 * 入口参数：unix_secs - Unix 时间戳              uint32_t  0~0x7FFFFFFF（~2038）
 *           p_sd_time - 输出时间结构指针          sd2506_time_t*  不得为 NULL
 * 出口参数：p_sd_time - 填充年/月/日/时/分/秒/星期
 * 返回值：无
 * 限定条件：p_sd_time 必须指向有效缓冲区
 * 函数说明：1. 先取模拆分时分秒，再按累计天数推算年月日
 *           2. 星期由 Zeller 公式（与 rtc_calc_week 同算法）得出
 *           3. SD2506 年份字段为相对值（0~99，+2000），故回写时减 2000
 */
static void unix_to_sd2506(uint32_t unix_secs, sd2506_time_t *p_sd_time)
{
    uint32_t y, m, d;
    uint8_t h, mi, s;
    int is_leap;

    /* 时分秒 */
    {
        uint64_t total = (uint64_t)unix_secs;
        s = (uint8_t)(total % 60uLL);
        total /= 60uLL;
        mi = (uint8_t)(total % 60uLL);
        total /= 60uLL;
        h = (uint8_t)(total % 24uL);
    }

    /* 年月日 */
    {
        uint64_t day_count = ((uint64_t)unix_secs / 86400uL);

        y = 1970u;
        while (day_count >= 365uLL) {
            uint64_t diy = 365uLL;
            if ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)) {
                diy = 366uLL;
            }
            if (day_count < diy) break;
            day_count -= diy;
            y++;
        }

        is_leap = ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0));
        uint8_t mdays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
        if (is_leap) mdays[1] = 29;

        m = 0;
        while (m < 12u) {
            if (day_count < (uint64_t)mdays[m]) break;
            day_count -= mdays[m];
            m++;
        }
        d = (uint32_t)(day_count + 1uL);

        /* 星期（Zeller's congruence，0=Sunday） */
        uint64_t wy = y;
        uint64_t wm = m;
        if (wm <= 2) {
            wy--;
            wm += 12uL;
        }
        /* Zeller 公式同 rtc_calc_week()：世纪项 + 月项 + 日项，模 7 得星期（0=周日） */
        uint8_t week = ((uint32_t)(wy % 100uLL + wy / 100uLL / 4uLL - wy / 100uLL / 15uLL) +
                       (uint32_t)(((uint64_t)26 * (wm + 1uL)) / 10uLL) +
                       (uint32_t)(d % 100uLL)) % 7uLL;

        /* sd2506_time_t.year 采用完整年份（如 2026），与 sd2506_set_time
         * 中 year-2000、sd2506_get_time 中 +2000 的约定保持一致；
         * 此处绝不能再减 2000，否则写 RTC 时会被二次减 2000 导致年份错乱。 */
        p_sd_time->year   = (uint16_t)y;
        p_sd_time->month  = m + 1uL;    /* 月份从 0 → 1 */
        p_sd_time->day    = d;
        p_sd_time->hour   = h;
        p_sd_time->minute = mi;
        p_sd_time->second = s;
        p_sd_time->week   = week;
    }
}

/* ============================================================
 * 系统墙钟时间维护
 * ============================================================ */

/*
 * 函数功能：初始化系统墙钟时间基准
 * 入口参数：无
 * 返回值：0 - 成功（始终返回 0）
 * 限定条件：CubeMX 外设初始化完成、SD2506 RTC 驱动已初始化
 * 函数说明：1. 从 SD2506 读取初始时间，必要时按编译期默认宏校时（RTC_DEFAULT_INIT_*）
 *           2. 将 RTC 时间换算为 Unix epoch 秒数
 *           3. 记录当前 HAL Tick 与 Unix 秒数为基准，供 sys_clock_get() 后续推算
 *           4. 注意：sd2506_get_time() 返回的 year 已是完整年份（驱动内 +2000），
 *              换算时不得再加 2000，否则年份虚增导致 Unix 秒数超 uint32 回绕
 */
int sys_clock_init(void)
{
    sd2506_time_t sd_time;
    uint32_t year, month, day, hour, minute, second;
    int64_t unix_secs = 0;
    int i;

    /* ============================================================
     * Step 1：从 SD2506 硬件 RTC 读取初始时间
     * ============================================================
     * 注意：sd2506_get_time() 返回的 year 已是完整年份（如 2026，
     * 驱动内部已 +2000）。此处【禁止】再 +2000，否则年份虚增到 4026，
     * 换算出的 Unix 秒数远超 uint32_t 上限被截断回绕，时间彻底错乱。 */
    sd2506_get_time(&sd_time);

    /* ============================================================
     * Step 1.1：开发期校时（可选，由 RTC_DEFAULT_INIT_* 宏控制）
     * ============================================================
     * 作用：RTC 出厂/掉电异常导致时间非法时，写入一个可用初始值；
     *       FORCE=1 时每次启动都强制写入，便于首次烧录或硬件时钟丢失。
     * 判定"时间无效"：年份越界、月/日越界（仅粗判，不细校 2 月 29 日等）。 */
#if defined(RTC_DEFAULT_INIT_ENABLE)
    {
        int rtc_invalid = (sd_time.year < 2000 || sd_time.year > 2099 ||
                           sd_time.month < 1 || sd_time.month > 12 ||
                           sd_time.day   < 1 || sd_time.day   > 31);
#if (RTC_DEFAULT_INIT_FORCE == 1)
        rtc_invalid = 1;   /* 强制每次启动校时：会覆盖用户/网络已设的正确时间，仅限开发期 */
#endif
        if (rtc_invalid) {
            sd2506_time_t init_t;
            /* 用编译期默认宏构造初始时间；week 由 Zeller 公式（rtc_calc_week）推算 */
            init_t.year   = RTC_DEFAULT_YEAR;
            init_t.month  = RTC_DEFAULT_MONTH;
            init_t.day    = RTC_DEFAULT_DAY;
            init_t.hour   = RTC_DEFAULT_HOUR;
            init_t.minute = RTC_DEFAULT_MINUTE;
            init_t.second = RTC_DEFAULT_SECOND;
            init_t.week   = rtc_calc_week(init_t.year, init_t.month, init_t.day);

            /* 写回 SD2506：断电后该时间被保留，下次上电即为有效值 */
            if (sd2506_set_time(&init_t) == SD2506_OK) {
                DBG_INFO("sys_clock_init: RTC set %04d-%02d-%02d %02d:%02d:%02d",
                         init_t.year, init_t.month, init_t.day,
                         init_t.hour, init_t.minute, init_t.second);
                sd2506_get_time(&sd_time);   /* 重新读取校准后的值，确保后续换算用真实 RTC 数据 */
            } else {
                DBG_ERROR("sys_clock_init: RTC default write FAIL");
            }
        }
    }
#endif

    /* 取出各时间字段（year 已是完整年份，直接用于下文 Unix 换算） */
    year   = (uint32_t)sd_time.year;
    month  = (uint32_t)sd_time.month;
    day    = (uint32_t)sd_time.day;
    hour   = (uint32_t)sd_time.hour;
    minute = (uint32_t)sd_time.minute;
    second = (uint32_t)sd_time.second;

    /* ============================================================
     * Step 2：将 RTC 日历时间换算为 Unix epoch 秒数
     * ============================================================
     * Unix 时间定义：自 1970-01-01 00:00:00 起经过的秒数。
     * 算法：总天数（1970~去年整年 + 今年到上月 + 本月已过天）× 86400
     *       + 当天时分秒。
     * 用 int64_t / uint64_t 累加避免中间溢出；最终结果（受 uint32_t 限制）
     * 有效范围约 1970-01-01 ~ 2038-01-19，超出将回绕（见文档第 12 节）。 */
    {
        uint64_t total_days = 0;

        /* 2.1 累加 1970 至（今年-1）的整年天数：闰年 366、平年 365
         * 闰年规则：能被 4 整除但不能被 100 整除，或能被 400 整除 */
        for (i = 1970; i < year; i++) {
            if ((i % 4 == 0 && i % 100 != 0) || (i % 400 == 0)) {
                total_days += 366uLL;
            } else {
                total_days += 365uLL;
            }
        }

        /* 2.2 判断今年是否闰年，构造各月天数表（2 月特判） */
        int is_leap = ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
        uint8_t mdays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
        if (is_leap) mdays[1] = 29;   /* 闰年 2 月 29 天 */

        /* 2.3 累加今年 1 月至（本月-1）月的天数 */
        for (i = 0; i < month - 1; i++) {
            total_days += mdays[i];
        }
        /* 2.4 加上本月已过天数（day-1：从今天 0 点起算） */
        total_days += day - 1uL;

        /* 2.5 总天数×86400（一天秒数）+ 当日时分秒 = 绝对 Unix 秒数 */
        unix_secs = (int64_t)(total_days * 86400LL) + hour * 3600LL + minute * 60LL + second;
    }

    /* ============================================================
     * Step 3：建立"墙钟基准"（基准法核心）
     * ============================================================
     * 记录当前 HAL Tick 与对应的 Unix 秒数。此后 sys_clock_get() 仅用
     *   now_unix = s_base_unix_secs + (HAL_GetTick() - s_base_tick)/1000
     * 推算当前时间，运行期不再访问 I2C RTC，零阻塞、无累积误差。 */
    s_base_tick = HAL_GetTick();
    s_base_unix_secs = (uint32_t)unix_secs;

    DBG_INFO("sys_clock_init: RTC initial time %04d-%02d-%02d %02d:%02d:%02d",
             year, month, day, hour, minute, second);

    DBG_INFO("sys_clock_init: wall clock base = unix %lu, base_tick=%lu",
             (unsigned long)s_base_unix_secs, (unsigned long)s_base_tick);

    return 0;
}

/*
 * 函数功能：获取当前系统墙钟时间（Unix 时间戳）
 * 入口参数：无
 * 返回值：当前 Unix 秒数      uint32_t  未初始化时返回 UINT32_MAX
 * 限定条件：sys_clock_init() 已成功调用
 * 函数说明：1. 以基准 Tick 与基准 Unix 秒数为起点，按当前 HAL Tick 与 1000ms
 *              的整除差推算流逝秒数（避免每毫秒累加，减少漂移与溢出风险）
 *           2. 若推算结果为负（基准未建立），返回 UINT32_MAX 表示无效
 */
uint32_t sys_clock_get(void)
{
    uint32_t now_tick = HAL_GetTick();
    int64_t unix_secs = (int64_t)s_base_unix_secs + (int64_t)((uint32_t)(now_tick - s_base_tick) / 1000uL);

    if (unix_secs < 0) {
        return UINT32_MAX; /* 未初始化 */
    }

    return (uint32_t)unix_secs;
}

/*
 * 函数功能：获取当前墙钟时间的可读字符串（YYYY-MM-DD HH:MM:SS）
 * 入口参数：buf - 输出字符串缓冲区   char*    不得为 NULL
 *           buf_size - 缓冲区大小    size_t   须 ≥ 20（含结尾 '\0'）
 * 出口参数：buf - 填充格式化时间字符串
 * 返回值：buf 指针（便于链式调用）
 * 限定条件：buf 指向至少 buf_size 字节的有效缓冲区；sys_clock_init() 已调用
 * 函数说明：1. 内部先取 Unix 秒数，未初始化时输出占位串 "----/--/-- --:--:--"
 *           2. 通过 unix_to_sd2506() 转换为 SD2506 结构后格式化
 *           3. 使用 snprintf 防止缓冲区溢出
 */
char *sys_clock_get_str(char *buf, size_t buf_size)
{
    uint32_t unix_secs = sys_clock_get();
    if (unix_secs == UINT32_MAX) {
        snprintf(buf, buf_size, "----/--/-- --:--:--");
        return buf;
    }

    sd2506_time_t sd;
    unix_to_sd2506(unix_secs, &sd);

    snprintf(buf, buf_size, "%04d-%02d-%02d %02d:%02d:%02d",
              sd.year, sd.month, sd.day,
             sd.hour, sd.minute, sd.second);
    return buf;
}

/*
 * 函数功能：设置（更新）系统墙钟时间基准，并可写回 SD2506 RTC
 * 入口参数：unix_secs - 新的 Unix 时间戳          uint32_t  0~0x7FFFFFFF
 *           sd_time - 对应的 RTC 时间结构指针      sd2506_time_t*  可为 NULL（仅更新基准）
 * 出口参数：无
 * 返回值：0 - 成功（始终返回 0）
 * 限定条件：unix_secs 为合法 Unix 时间戳
 * 函数说明：1. 重置基准 Tick 与基准 Unix 秒数，使后续 sys_clock_get() 以此为准
 *           2. 若 sd_time 非 NULL，将其写回 SD2506 RTC，实现断电保持
 */
int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time)
{
    /* Step 1: 更新墙钟基准 */
    s_base_tick = HAL_GetTick();
    s_base_unix_secs = unix_secs;

    DBG_INFO("sys_clock_set: wall clock updated to %lu", (unsigned long)unix_secs);

    /* Step 2: 写回 SD2506 RTC（若提供了 sd_time） */
    if (sd_time != NULL) {
        if (sd2506_set_time(sd_time) == SD2506_OK) {
            DBG_INFO("sys_clock_set: SD2506 RTC written");
        } else {
            DBG_ERROR("sys_clock_set: SD2506 RTC write FAIL");
        }
    }

    return 0;
}

int sys_clock_set_unix(uint32_t unix_secs)
{
    sd2506_time_t sd;

    /* 内部静态函数：将 Unix 秒数拆分为 SD2506 时间结构 */
    unix_to_sd2506(unix_secs, &sd);

    /* 更新运行期基准并写回 RTC，实现掉电保持 */
    return sys_clock_set(unix_secs, &sd);
}
