//Zmodyfikowano! v0.9.710  "weather"
#include "options.h"
#include "Arduino.h"
#include "timekeeper.h"
#include "config.h"
#include "network.h"
#include "display.h"
#include "player.h"
#include "netserver.h"
#include "rtcsupport.h"
#include "../displays/tools/l10n.h"
#include "../pluginsManager/pluginsManager.h"
#ifdef USE_NEXTION
#include "../displays/nextion.h"
#endif
#if DSP_MODEL==DSP_DUMMY
#define DUMMYDISPLAY
#endif

#if RTCSUPPORTED
  //#define TIME_SYNC_INTERVAL  24*60*60*1000
  #define TIME_SYNC_INTERVAL  config.store.timeSyncIntervalRTC*60*60*1000
#else
  #define TIME_SYNC_INTERVAL  config.store.timeSyncInterval*60*1000
#endif
#define WEATHER_SYNC_INTERVAL config.store.weatherSyncInterval*60*1000

#define SYNC_STACK_SIZE       1024 * 4
#define SYNC_TASK_CORE        0
#define SYNC_TASK_PRIORITY    3
#define WEATHER_STRING_L      254

#ifdef HEAP_DBG
  void printHeapFragmentationInfo(const char* title){
    size_t freeHeap = heap_caps_get_free_size(MALLOC_CAP_DEFAULT);
    size_t largestBlock = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
    float fragmentation = 100.0 * (1.0 - ((float)largestBlock / (float)freeHeap));
    Serial.printf("\n****** %s ******\n", title);
    Serial.printf("* Free heap: %u bytes\n", freeHeap);
    Serial.printf("* Largest free block: %u bytes\n", largestBlock);
    Serial.printf("* Fragmentation: %.2f%%\n", fragmentation);
    Serial.printf("*************************************\n\n");
  }
  #define HEAP_INFO() printHeapFragmentationInfo(__PRETTY_FUNCTION__)
#else
  #define HEAP_INFO()
#endif

TimeKeeper timekeeper;

void _syncTask(void *pvParameters) {
  if (timekeeper.forceWeather && timekeeper.forceTimeSync) {
    timekeeper.timeTask();
    timekeeper.weatherTask();
  } 
  else if (timekeeper.forceWeather) {
    timekeeper.weatherTask();
  }
  else if (timekeeper.forceTimeSync) {
    timekeeper.timeTask();
  }
  timekeeper.busy = false;
  vTaskDelete(NULL);
}

TimeKeeper::TimeKeeper(){
  busy          = false;
  forceWeather  = true;
  forceTimeSync = true;
  _returnPlayerTime = _doAfterTime = 0;
  weatherBuf=NULL;
  memset(weatherIcon, 0, sizeof(weatherIcon));
  sunriseTime = 0;
  sunsetTime = 0;
  #if (DSP_MODEL!=DSP_DUMMY || defined(USE_NEXTION)) && !defined(HIDE_WEATHER)
    weatherBuf = (char *) malloc(sizeof(char) * WEATHER_STRING_L);
    memset(weatherBuf, 0, WEATHER_STRING_L);
  #endif
}

bool TimeKeeper::loop0(){ // core0 (display)
  if (network.status != CONNECTED) return true;
  uint32_t currentTime = millis();
  static uint32_t _last1s = 0;
  static uint32_t _last2s = 0;
  static uint32_t _last5s = 0;
  if (currentTime - _last1s >= 1000) { // 1sec
    _last1s = currentTime;
//#ifndef DUMMYDISPLAY
#if !defined(DUMMYDISPLAY) || defined(USE_NEXTION)
  #ifndef UPCLOCK_CORE1
    _upClock();
  #endif
#endif
  }
  if (currentTime - _last2s >= 2000) { // 2sec
    _last2s = currentTime;
    _upRSSI();
  }
  if (currentTime - _last5s >= 5000) { // 5sec
    _last5s = currentTime;
    //HEAP_INFO();
  }

  return true; // just in case
}

bool TimeKeeper::loop1(){ // core1 (player)
  uint32_t currentTime = millis();
  static uint32_t _last1s = 0;
  static uint32_t _last2s = 0;
  static uint32_t _lastStallCheck = 0;
  static uint32_t _stallSince = 0;
  static uint32_t _lastRestart = 0;
  static uint32_t _restartBackoffMs = 5000;
  static uint32_t _lastRecoverAttempt = 0;
  static uint32_t _recoverBackoffMs = 3000;
  static uint8_t _recoverAttempts = 0;
  if (currentTime - _last1s >= 1000) { // 1sec
    pm.on_ticker();
    _last1s = currentTime;
//#ifndef DUMMYDISPLAY
#if !defined(DUMMYDISPLAY) || defined(USE_NEXTION)
  #ifdef UPCLOCK_CORE1
    _upClock();
  #endif
#endif
    _upScreensaver();
    _upSDPos();
    _returnPlayer();
    _doAfterWait();
  }
  if (currentTime - _last2s >= 2000) { // 2sec
    _last2s = currentTime;
  }

  if (network.lostPlaying) {
    if (player.isRunning()) {
      network.lostPlaying = false;
      _recoverAttempts = 0;
      _recoverBackoffMs = 3000;
      _lastRecoverAttempt = 0;
    } else if (config.getMode() == PM_WEB && network.status == CONNECTED && !player.lockOutput) {
      if (currentTime - _lastRecoverAttempt >= _recoverBackoffMs) {
        _lastRecoverAttempt = currentTime;
        if (_recoverAttempts < 8) {
          _recoverAttempts++;
          player.sendCommand({PR_PLAY, config.lastStation()});
          _recoverBackoffMs = min<uint32_t>(_recoverBackoffMs * 2, 30000);
        } else {
          network.lostPlaying = false;
          _recoverAttempts = 0;
          _recoverBackoffMs = 3000;
          _lastRecoverAttempt = 0;
        }
      }
    }
  } else {
    _recoverAttempts = 0;
    _recoverBackoffMs = 3000;
    _lastRecoverAttempt = 0;
  }

  if (currentTime - _lastStallCheck >= 1000) { // 1sec
    _lastStallCheck = currentTime;
    if (config.store.watchdog && player.isRunning() && config.getMode() == PM_WEB && network.status == CONNECTED && !player.lockOutput) {
      uint32_t buf = player.inBufferFilled();
      if (buf == 0) {
        if (_stallSince == 0) {
          _stallSince = currentTime;
        }
      } else {
        _stallSince = 0;
        _restartBackoffMs = 5000;
      }

      if (_stallSince > 0 && (currentTime - _stallSince) >= 12000) {
        if (currentTime - _lastRestart >= _restartBackoffMs) {
          _lastRestart = currentTime;
          if (_restartBackoffMs < 60000) {
            _restartBackoffMs = min<uint32_t>(_restartBackoffMs * 2, 60000);
          }
          player.sendCommand({PR_STOP, 0});
          player.sendCommand({PR_PLAY, config.lastStation()});
          _stallSince = 0;
        }
      }
    } else {
      _stallSince = 0;
      _restartBackoffMs = 5000;
    }
  }

  //#ifdef DUMMYDISPLAY
  #if defined(DUMMYDISPLAY) && !defined(USE_NEXTION)
  return true;
  #endif
  // Sync weather & time
  static uint32_t lastWeatherTime = 0;
  if (currentTime - lastWeatherTime >= WEATHER_SYNC_INTERVAL) {
    lastWeatherTime = currentTime;
    forceWeather = true;
  }
  static uint32_t lastTimeTime = 0;
  if (currentTime - lastTimeTime >= TIME_SYNC_INTERVAL) {
    lastTimeTime = currentTime;
    forceTimeSync = true;
  }
  if (!busy && (forceWeather || forceTimeSync) && network.status == CONNECTED) {
    busy = true;
    //config.setTimeConf();
    xTaskCreatePinnedToCore(
      _syncTask,
      "syncTask",
      SYNC_STACK_SIZE,
      NULL,           // Params
      SYNC_TASK_PRIORITY,
      NULL,           // Descriptor
      SYNC_TASK_CORE
    );
  }
  
  return true; // just in case
}

void TimeKeeper::waitAndReturnPlayer(uint8_t time_s){
  _returnPlayerTime = millis()+time_s*1000;
}
void TimeKeeper::_returnPlayer(){
  if(_returnPlayerTime>0 && millis()>=_returnPlayerTime){
    _returnPlayerTime = 0;
    display.putRequest(NEWMODE, PLAYER);
  }
}

void TimeKeeper::waitAndDo(uint8_t time_s, void (*callback)()){
  _doAfterTime = millis()+time_s*1000;
  _aftercallback = callback;
}
void TimeKeeper::_doAfterWait(){
  if(_doAfterTime>0 && millis()>=_doAfterTime){
    _doAfterTime = 0;
    _aftercallback();
  }
}

void TimeKeeper::_upClock(){
#if RTCSUPPORTED
  if(config.isRTCFound()) rtc.getTime(&network.timeinfo);
#else
  if(network.timeinfo.tm_year>100 || network.status == SDREADY) {
    network.timeinfo.tm_sec++;
    mktime(&network.timeinfo);
  }
#endif
  if(display.ready()) display.putRequest(CLOCK);
}

void TimeKeeper::_upScreensaver(){
#ifndef DSP_LCD
  if(!display.ready()) return;
  if(config.store.screensaverEnabled && display.mode()==PLAYER && !player.isRunning()){
    config.screensaverTicks++;
    if(config.screensaverTicks > config.store.screensaverTimeout+SCREENSAVERSTARTUPDELAY){
      if(config.store.screensaverBlank){
        display.putRequest(NEWMODE, SCREENBLANK);
      }else{
        display.putRequest(NEWMODE, SCREENSAVER);
      }
      config.screensaverTicks=SCREENSAVERSTARTUPDELAY;
    }
  }
  if(config.store.screensaverPlayingEnabled && display.mode()==PLAYER && player.isRunning()){
    config.screensaverPlayingTicks++;
    if(config.screensaverPlayingTicks > config.store.screensaverPlayingTimeout*60+SCREENSAVERSTARTUPDELAY){
      if(config.store.screensaverPlayingBlank){
        display.putRequest(NEWMODE, SCREENBLANK);
      }else{
        display.putRequest(NEWMODE, SCREENSAVER);
      }
      config.screensaverPlayingTicks=SCREENSAVERSTARTUPDELAY;
    }
  }
#endif
}

void TimeKeeper::_upRSSI(){
  if(network.status == CONNECTED){
    netserver.setRSSI(WiFi.RSSI());
    netserver.requestOnChange(NRSSI, 0);
    if(display.ready()) display.putRequest(DSPRSSI, netserver.getRSSI());
  }
#ifdef USE_SD
  if(display.mode()!=SDCHANGE) player.sendCommand({PR_CHECKSD, 0});
#endif
  player.sendCommand({PR_VUTONUS, 0});
}

void TimeKeeper::_upSDPos(){
  if(player.isRunning() && config.getMode()==PM_SDCARD) netserver.requestOnChange(SDPOS, 0);
}

void TimeKeeper::timeTask(){
  static uint8_t tsFailCnt = 0;
  config.waitConnection();
  if(getLocalTime(&network.timeinfo)){
    tsFailCnt = 0;
    forceTimeSync = false;
    mktime(&network.timeinfo);
    
    #if RTCSUPPORTED
    // Automatic DST only when RTC is enabled
    // Without RTC, user can manually set timezone via web panel
    if (config.isRTCFound()) {
      // Update DST and check if timezone changed
      int8_t oldTzHour = config.store.tzHour;
      config.updateDST(); // Auto-update DST status
      
      // If timezone was changed by updateDST(), refresh time from NTP
      if (oldTzHour != config.store.tzHour) {
        delay(100); // Brief delay to allow configTime to take effect
        getLocalTime(&network.timeinfo); // Get time again with correct timezone
        mktime(&network.timeinfo);
      }
    }
    #endif
    
    display.putRequest(CLOCK, 1);
    network.requestTimeSync(true);
    #if RTCSUPPORTED
      if (config.isRTCFound()) rtc.setTime(&network.timeinfo);
    #endif
  }else{
    if(tsFailCnt<4){
      forceTimeSync = true;
      tsFailCnt++;
    }else{
      forceTimeSync = false;
      tsFailCnt=0;
    }
  }
}
void TimeKeeper::weatherTask(){
  forceWeather = false;
  if(!weatherBuf || strlen(config.store.weatherkey)==0 || !config.store.showweather) return;
  _getWeather();
}

bool _getWeather() {
#if (DSP_MODEL!=DSP_DUMMY || defined(USE_NEXTION)) && !defined(HIDE_WEATHER)
  static AsyncClient * weatherClient = NULL;
  static const char* host = "api.openweathermap.org";
  if(weatherClient) return false;
  weatherClient = new AsyncClient();
  if(!weatherClient) return false;

  weatherClient->onError([](void * arg, AsyncClient * client, int error){
    Serial.println("##WEATHER###: connection error");
    weatherClient = NULL;
    delete client;
  }, NULL);

  weatherClient->onConnect([](void * arg, AsyncClient * client){
    weatherClient->onError(NULL, NULL);
    weatherClient->onDisconnect([](void * arg, AsyncClient * c){ weatherClient = NULL; delete c; }, NULL);
    
    char httpget[250] = {0};
    sprintf(httpget, "GET /data/2.5/weather?lat=%s&lon=%s&units=%s&lang=%s&appid=%s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", config.store.weatherlat, config.store.weatherlon, LANG::weatherUnits, LANG::weatherLang, config.store.weatherkey, host);
    client->write(httpget);
    
    client->onData([](void * arg, AsyncClient * c, void * data, size_t len){
      uint8_t * d = (uint8_t*)data;
      const char *bodyStart = strstr((const char*)d, "\r\n\r\n");
      if (bodyStart != NULL) {
        bodyStart += 4;
        size_t bodyLen = len - (bodyStart - (const char*)d);
        char line[bodyLen+1];
        memcpy(line, bodyStart, bodyLen);
        line[bodyLen] = '\0';
        /* parse it */
        char *cursor;
        char desc[120];
        float tempf, tempfl, wind_speed;
        int hum, press, wind_deg;
        uint32_t sunrise_ts, sunset_ts;
        bool result = true;

        cursor = strstr(line, "\"description\":\"");
        if (cursor) { sscanf(cursor, "\"description\":\"%119[^\"]", desc); }else{ Serial.println("##WEATHER###: description not found !"); result=false; }
        cursor = strstr(line, "\"icon\":\"");
        if (cursor) { sscanf(cursor, "\"icon\":\"%4[^\"]", timekeeper.weatherIcon); }else{ Serial.println("##WEATHER###: icon not found !"); result=false; }
        cursor = strstr(line, "\"temp\":");
        if (cursor) { sscanf(cursor, "\"temp\":%f", &tempf); }else{ Serial.println("##WEATHER###: temp not found !"); result=false; }
        cursor = strstr(line, "\"pressure\":");
        if (cursor) { sscanf(cursor, "\"pressure\":%d", &press); }else{ Serial.println("##WEATHER###: pressure not found !"); result=false; }
        cursor = strstr(line, "\"humidity\":");
        if (cursor) { sscanf(cursor, "\"humidity\":%d", &hum); }else{ Serial.println("##WEATHER###: humidity not found !"); result=false; }
        cursor = strstr(line, "\"feels_like\":");
        if (cursor) { sscanf(cursor, "\"feels_like\":%f", &tempfl); }else{ Serial.println("##WEATHER###: feels_like not found !"); result=false; }
       // cursor = strstr(line, "\"grnd_level\":");
       // if (cursor) { sscanf(cursor, "\"grnd_level\":%d", &press); }
        cursor = strstr(line, "\"speed\":");
        if (cursor) { sscanf(cursor, "\"speed\":%f", &wind_speed); }else{ Serial.println("##WEATHER###: wind speed not found !"); result=false; }
        cursor = strstr(line, "\"deg\":");
        if (cursor) { sscanf(cursor, "\"deg\":%d", &wind_deg); }else{ Serial.println("##WEATHER###: wind deg not found !"); result=false; }
        cursor = strstr(line, "\"sunrise\":");
        if (cursor) { sscanf(cursor, "\"sunrise\":%u", &sunrise_ts); timekeeper.sunriseTime = sunrise_ts; }else{ Serial.println("##WEATHER###: sunrise not found !"); }
        cursor = strstr(line, "\"sunset\":");
        if (cursor) { sscanf(cursor, "\"sunset\":%u", &sunset_ts); timekeeper.sunsetTime = sunset_ts; }else{ Serial.println("##WEATHER###: sunset not found !"); }
      // press = press / 1.333;
      // press = press / 0.973; //Zmodyfikowano wyświetlanie hPa. "weather"
      #ifdef WIND_SPEED_IN_KMH
      wind_speed *= 3.6f;
      #endif

        if(!result) return;

        #ifdef USE_NEXTION
          nextion.putcmdf("press_txt.txt=\"%dmm\"", press);
          nextion.putcmdf("hum_txt.txt=\"%d%%\"", hum);
          char cmd[30];
          snprintf(cmd, sizeof(cmd)-1,"temp_txt.txt=\"%.1f\"", tempf);
          nextion.putcmd(cmd);
          int iconofset;
          if(strstr(timekeeper.weatherIcon,"01")!=NULL)      iconofset = 0;
          else if(strstr(timekeeper.weatherIcon,"02")!=NULL) iconofset = 1;
          else if(strstr(timekeeper.weatherIcon,"03")!=NULL) iconofset = 2;
          else if(strstr(timekeeper.weatherIcon,"04")!=NULL) iconofset = 3;
          else if(strstr(timekeeper.weatherIcon,"09")!=NULL) iconofset = 4;
          else if(strstr(timekeeper.weatherIcon,"10")!=NULL) iconofset = 5;
          else if(strstr(timekeeper.weatherIcon,"11")!=NULL) iconofset = 6;
          else if(strstr(timekeeper.weatherIcon,"13")!=NULL) iconofset = 7;
          else if(strstr(timekeeper.weatherIcon,"50")!=NULL) iconofset = 8;
          else                             iconofset = 9;
          nextion.putcmd("cond_img.pic", 50+iconofset);
          nextion.weatherVisible(1);
        #endif
        
        Serial.printf("##WEATHER###: icon code: %s\n", timekeeper.weatherIcon);
        Serial.printf("##WEATHER###: description: %s, temp:%.1f C, pressure:%d hPa, humidity:%d%%, wind: %d\n", desc, tempf, press, hum, (int)(wind_deg/22.5));
        #ifdef WEATHER_FMT_SHORT
        sprintf(timekeeper.weatherBuf, LANG::weatherFmt, tempf, press, hum); //Módisítás LANG:: hozzáírva. "weather"
        #else
          #if EXT_WEATHER
            sprintf(timekeeper.weatherBuf, LANG::weatherFmt, desc, tempf, tempfl, press, hum, wind_speed);
          #else
            sprintf(timekeeper.weatherBuf, LANG::weatherFmt, desc, tempf, press, hum);
          #endif
        #endif
        display.putRequest(NEWWEATHER);
      } else {
        Serial.println("##WEATHER###: weather not found !");
      }
    }, NULL); // <-- client->onData
  }, NULL); // <-- weatherClient->onConnect
  config.waitConnection();
  if(!weatherClient->connect(host, 80)){
    Serial.println("##WEATHER###: connection failed");
    AsyncClient * client = weatherClient;
    weatherClient = NULL;
    delete client;
  }

  return true;
#endif // if (DSP_MODEL!=DSP_DUMMY || defined(USE_NEXTION)) && !defined(HIDE_WEATHER)
  return false;
}

//******************