#include "../../core/options.h"
#if DSP_MODEL != DSP_DUMMY
#ifdef NAMEDAYS_FILE
#include "../../core/namedays.h"
#endif

#include "../../core/config.h"
#include "../../core/network.h"  //  for Clock widget
#include "../../core/player.h"   //  for VU widget
#include "../dspcore.h"
#include "../tools/l10n.h"
#include "../tools/psframebuffer.h"
#include "../fonts/Arimo_Regular_36.h"  //  for clock seconds
#include "../fonts/Arimo_Regular_72.h"  //  for clock main
#include "Arduino.h"
#include "widgets.h"
// clang-format off
/************************
      FILL WIDGET
 ************************/
void FillWidget::init(FillConfig conf, uint16_t bgcolor){
  Widget::init(conf.widget, bgcolor, bgcolor);
  _width = conf.width;
  _height = conf.height;
  
}

void FillWidget::_draw(){
  if(!_active) return;
  dsp.fillRect(_config.left, _config.top, _width, _height, _bgcolor);
}

void FillWidget::setHeight(uint16_t newHeight){
  _height = newHeight;
  //_draw();
}
/************************
      TEXT WIDGET
 ************************/
TextWidget::~TextWidget() {
  free(_text);
  free(_oldtext);
}

void TextWidget::_charSize(uint8_t textsize, uint8_t& width, uint16_t& height){
#ifndef DSP_LCD
  width = textsize * CHARWIDTH;
  height = textsize * CHARHEIGHT;
#else
  width = 1;
  height = 1;
#endif
}

void TextWidget::init(WidgetConfig wconf, uint16_t buffsize, bool uppercase, uint16_t fgcolor, uint16_t bgcolor) {
  Widget::init(wconf, fgcolor, bgcolor);
  _buffsize = buffsize;
  _text = (char *) malloc(sizeof(char) * _buffsize);
  memset(_text, 0, _buffsize);
  _oldtext = (char *) malloc(sizeof(char) * _buffsize);
  memset(_oldtext, 0, _buffsize);
  _charSize(_config.textsize, _charWidth, _textheight);
  _textwidth = _oldtextwidth = _oldleft = 0;
  _uppercase = uppercase;
}

void TextWidget::setText(const char* txt) {
  strlcpy(_text, utf8To(txt, _uppercase), _buffsize);
  _textwidth = strlen(_text) * _charWidth;
  if (strcmp(_oldtext, _text) == 0) return;
  if (_active) {
    uint16_t clearLeft = (_oldleft == 0) ? _realLeft() : min(_oldleft, _realLeft());
    uint16_t oldRight = _oldleft + _oldtextwidth;
    uint16_t newRight = _realLeft() + _textwidth;
    uint16_t clearWidth = max(oldRight, newRight) - clearLeft;
    dsp.fillRect(clearLeft, _config.top, clearWidth, _textheight, _bgcolor);
  }
  _oldtextwidth = _textwidth;
  _oldleft = _realLeft();
  if (_active) _draw();
}

void TextWidget::setText(int val, const char *format){
  char buf[_buffsize];
  snprintf(buf, _buffsize, format, val);
  setText(buf);
}

void TextWidget::setText(const char* txt, const char *format){
  char buf[_buffsize];
  snprintf(buf, _buffsize, format, txt);
  setText(buf);
}

uint16_t TextWidget::_realLeft(bool w_fb) {
  uint16_t realwidth = (_width>0 && w_fb)?_width:dsp.width();
  switch (_config.align) {
    case WA_CENTER: return (realwidth - _textwidth) / 2; break;
    case WA_RIGHT: return (realwidth - _textwidth - (!w_fb?_config.left:0)); break;
    default: return !w_fb?_config.left:0; break;
  }
}

void TextWidget::_draw() {
  if(!_active) return;
  dsp.setTextColor(_fgcolor, _bgcolor);
  dsp.setCursor(_realLeft(), _config.top);
  dsp.setFont();
  dsp.setTextSize(_config.textsize);
  dsp.print(_text);
  strlcpy(_oldtext, _text, _buffsize);
}

/************************
      SCROLL WIDGET
 ************************/
ScrollWidget::ScrollWidget(const char* separator, ScrollConfig conf, uint16_t fgcolor, uint16_t bgcolor) {
  init(separator, conf, fgcolor, bgcolor);
}

ScrollWidget::~ScrollWidget() {
  free(_fb);
  free(_sep);
  free(_window);
}

void ScrollWidget::init(const char* separator, ScrollConfig conf, uint16_t fgcolor, uint16_t bgcolor) {
  TextWidget::init(conf.widget, conf.buffsize, conf.uppercase, fgcolor, bgcolor);
  _sep = (char *) malloc(sizeof(char) * 4);
  memset(_sep, 0, 4);
  snprintf(_sep, 4, " %.*s ", 1, separator);
  _x = conf.widget.left;
  _startscrolldelay = conf.startscrolldelay;
  _scrolldelta = conf.scrolldelta;
  _scrolltime = conf.scrolltime;
  _charSize(_config.textsize, _charWidth, _textheight);
  _sepwidth = strlen(_sep) * _charWidth;
  _width = conf.width;
  _backMove.width = _width;
  _window = (char *) malloc(sizeof(char) * (MAX_WIDTH / _charWidth + 1));
  memset(_window, 0, (MAX_WIDTH / _charWidth + 1));  // +1?
  _doscroll = false;
  #ifdef PSFBUFFER
  _fb = new psFrameBuffer(dsp.width(), dsp.height());
  uint16_t _rl = (_config.align==WA_CENTER)?(dsp.width()-_width)/2:_config.left;
  _fb->begin(&dsp, _rl, _config.top, _width, _textheight, _bgcolor);
  #endif
}

void ScrollWidget::_setTextParams() {
  if (_config.textsize == 0) return;
  if(_fb->ready()){
  #ifdef PSFBUFFER
    _fb->setTextSize(_config.textsize);
    _fb->setTextColor(_fgcolor, _bgcolor);
  #endif
  }else{
    dsp.setTextSize(_config.textsize);
    dsp.setTextColor(_fgcolor, _bgcolor);
  }
}

bool ScrollWidget::_checkIsScrollNeeded() {
  return _textwidth > _width;
}

void ScrollWidget::setText(const char* txt) {
  strlcpy(_text, utf8To(txt, _uppercase), _buffsize - 1);
  if (strcmp(_oldtext, _text) == 0) return;
  _textwidth = strlen(_text) * _charWidth;
  _x = _fb->ready()?0:_config.left;
  _doscroll = _checkIsScrollNeeded();
  if (dsp.getScrollId() == this) dsp.setScrollId(NULL);
  _scrolldelay = millis();
  if (_active) {
    _setTextParams();
    if (_doscroll) {
      if(_fb->ready()){
      #ifdef PSFBUFFER
        _fb->fillRect(0, 0, _width, _textheight, _bgcolor);
        _fb->setCursor(0, 0);
        snprintf(_window, _width / _charWidth + 1, "%s", _text); //TODO
        _fb->print(_window);
        _fb->display();
      #endif
      } else {
        dsp.fillRect(_config.left,  _config.top, _width, _textheight, _bgcolor);
        dsp.setCursor(_config.left, _config.top);
        snprintf(_window, _width / _charWidth + 1, "%s", _text); //TODO
        dsp.setClipping({_config.left, _config.top, _width, _textheight});
        dsp.print(_window);
        dsp.clearClipping();
      }
    } else {
      if(_fb->ready()){
      #ifdef PSFBUFFER
        _fb->fillRect(0, 0, _width, _textheight, _bgcolor);
        _fb->setCursor(_realLeft(true), 0);
        _fb->print(_text);
        _fb->display();
      #endif
      } else {
        dsp.fillRect(_config.left, _config.top, _width, _textheight, _bgcolor);
        dsp.setCursor(_realLeft(), _config.top);
        //dsp.setClipping({_config.left, _config.top, _width, _textheight});
        dsp.print(_text);
        //dsp.clearClipping();
      }
    }
    strlcpy(_oldtext, _text, _buffsize);
  }
}

void ScrollWidget::setText(const char* txt, const char *format){
  char buf[_buffsize];
  snprintf(buf, _buffsize, format, txt);
  setText(buf);
}

void ScrollWidget::loop() {
  if(_locked) return;
  if (!_doscroll || _config.textsize == 0 || (dsp.getScrollId() != NULL && dsp.getScrollId() != this)) return;
  uint16_t fbl = _fb->ready()?0:_config.left;
  if (_checkDelay(_x == fbl ? _startscrolldelay : _scrolltime, _scrolldelay)) {
    _calcX();
    if (_active) _draw();
  }
}

void ScrollWidget::_clear(){
  if(_fb->ready()){
    #ifdef PSFBUFFER
    _fb->fillRect(0, 0, _width, _textheight, _bgcolor);
    _fb->display();
    #endif
  } else {
    dsp.fillRect(_config.left, _config.top, _width, _textheight, _bgcolor);
  }
}

void ScrollWidget::_draw() {
  if(!_active || _locked) return;
  _setTextParams();
  if (_doscroll) {
    uint16_t fbl = _fb->ready()?0:_config.left;
    uint16_t _newx = fbl - _x;
    const char* _cursor = _text + _newx / _charWidth;
    uint16_t hiddenChars = _cursor - _text;
    uint8_t addChars = _fb->ready()?2:1;
    if (hiddenChars < strlen(_text)) {
    //TODO
    #pragma GCC diagnostic push
    #pragma GCC diagnostic ignored "-Wformat-truncation="
      snprintf(_window, _width / _charWidth + addChars, "%s%s%s", _cursor, _sep, _text);
    #pragma GCC diagnostic pop
    } else {
      const char* _scursor = _sep + (_cursor - (_text + strlen(_text)));
      snprintf(_window, _width / _charWidth + addChars, "%s%s", _scursor, _text);
    }
    if(_fb->ready()){
    #ifdef PSFBUFFER
      _fb->fillRect(0, 0, _width, _textheight, _bgcolor);
      _fb->setCursor(_x + hiddenChars * _charWidth, 0);
      _fb->print(_window);
      _fb->display();
    #endif
    } else {
      dsp.setCursor(_x + hiddenChars * _charWidth, _config.top);
      dsp.setClipping({_config.left, _config.top, _width, _textheight});
      dsp.print(_window);
      #ifndef DSP_LCD
        dsp.print(" ");
      #endif
      dsp.clearClipping();
    }
  } else {
    if(_fb->ready()){
    #ifdef PSFBUFFER
      _fb->fillRect(0, 0, _width, _textheight, _bgcolor);
      _fb->setCursor(_realLeft(true), 0);
      _fb->print(_text);
      _fb->display();
    #endif
    } else {
      dsp.fillRect(_config.left, _config.top, _width, _textheight, _bgcolor);
      dsp.setCursor(_realLeft(), _config.top);
      dsp.setClipping({_realLeft(), _config.top, _width, _textheight});
      dsp.print(_text);
      dsp.clearClipping();
    }
  }
}

void ScrollWidget::_calcX() {
  if (!_doscroll || _config.textsize == 0) return;
  _x -= _scrolldelta;
  uint16_t fbl = _fb->ready()?0:_config.left;
  if (-_x > _textwidth + _sepwidth - fbl) {
    _x = fbl;
    dsp.setScrollId(NULL);
  } else {
    dsp.setScrollId(this);
  }
}

bool ScrollWidget::_checkDelay(int m, uint32_t &tstamp) {
  if (millis() - tstamp > m) {
    tstamp = millis();
    return true;
  } else {
    return false;
  }
}

void ScrollWidget::_reset(){
  dsp.setScrollId(NULL);
  _x = _fb->ready()?0:_config.left;
  _scrolldelay = millis();
  _doscroll = _checkIsScrollNeeded();
  #ifdef PSFBUFFER
  _fb->freeBuffer();
  uint16_t _rl = (_config.align==WA_CENTER)?(dsp.width()-_width)/2:_config.left;
  _fb->begin(&dsp, _rl, _config.top, _width, _textheight, _bgcolor);
  #endif
}

/************************
      SLIDER WIDGET
 ************************/
void SliderWidget::init(FillConfig conf, uint16_t fgcolor, uint16_t bgcolor, uint32_t maxval, uint16_t oucolor) {
  Widget::init(conf.widget, fgcolor, bgcolor);
  _width = conf.width; _height = conf.height; _outlined = conf.outlined; _oucolor = oucolor, _max = maxval;
  _oldvalwidth = _value = 0;
}

void SliderWidget::setValue(uint32_t val) {
  _value = val;
  if (_active && !_locked) _drawslider();

}

void SliderWidget::_drawslider() {
  uint16_t valwidth = map(_value, 0, _max, 0, _width - _outlined * 2);
  if (_oldvalwidth == valwidth) return;
  dsp.fillRect(_config.left + _outlined + min(valwidth, _oldvalwidth), _config.top + _outlined, abs(_oldvalwidth - valwidth), _height - _outlined * 2, _oldvalwidth > valwidth ? _bgcolor : _fgcolor);
  _oldvalwidth = valwidth;
}

void SliderWidget::_draw() {
  if(_locked) return;
  _clear();
  if(!_active) return;
  if (_outlined) dsp.drawRect(_config.left, _config.top, _width, _height, _oucolor);
  uint16_t valwidth = map(_value, 0, _max, 0, _width - _outlined * 2);
  dsp.fillRect(_config.left + _outlined, _config.top + _outlined, valwidth, _height - _outlined * 2, _fgcolor);
}

void SliderWidget::_clear() {
//  _oldvalwidth = 0;
  dsp.fillRect(_config.left, _config.top, _width, _height, _bgcolor);
}
void SliderWidget::_reset() {
  _oldvalwidth = 0;
}

// clang-format on
/************************
      VU WIDGET
 ************************/
#if !defined(DSP_LCD) && !defined(DSP_OLED)
VuWidget::~VuWidget() {
  if (_canvas) {
    free(_canvas);
  }
}

void VuWidget::init(WidgetConfig wconf, VUBandsConfig bands, uint16_t vumaxcolor, uint16_t vumidcolor, uint16_t vumincolor, uint16_t bgcolor) {
  Widget::init(wconf, bgcolor, bgcolor);
  _vumaxcolor = vumaxcolor;
  _vumidcolor = vumidcolor;  // Modyfikacja: nowa linia.
  _vumincolor = vumincolor;
  _bands = bands;
  //  _canvas = new Canvas(_bands.width * 2 + _bands.space, _bands.height);
  // _canvas = new Canvas(_bands.width, _bands.height * 2 + _bands.space);
#ifndef BOOMBOX_STYLE
  // dwa VU jeden pod drugim
  _canvas = new Canvas(_bands.width, _bands.height * 2 + _bands.space);
#else
  // dwa VU obok siebie
  _canvas = new Canvas(_bands.width * 2 + _bands.space, _bands.height);
#endif
}

/* Zmienna _labelsDrawn gdy ma wartość true, rysuje etykiety L R przed miernikiem VU.
W BitrateWidget::_clear() otrzymuje wartość false.*/
bool VuWidget::_labelsDrawn = false;  // Modyfikacja

void VuWidget::setLabelsDrawn(bool value) {  // Własne
  _labelsDrawn = value;
}

bool VuWidget::isLabelsDrawn() {  // Własne
  return _labelsDrawn;
}

void VuWidget::_draw() {
  if (!_active || _locked) {
    return;
  }

  static uint16_t measL, measR;
  uint16_t bandColor;
  uint16_t dimension = _config.align ? _bands.width : _bands.height;
  uint16_t vulevel = player.get_VUlevel(dimension);
  uint8_t L = (vulevel >> 8) & 0xFF;
  uint8_t R = vulevel & 0xFF;
  uint8_t refresh_time = 30;
  static uint32_t last_draw_time;
#ifdef VU_PEAK
  static uint16_t peakL = 0, peakR = 0;            // Wartości szczytowe
  static uint32_t peakL_time = 0, peakR_time = 0;  // Znacznik czasu szczytu
  const uint8_t peak_decay_step = 3;               // Zanikanie szczytu w pikselach
  const uint16_t peak_hold_ms = 200;               // Czas utrzymania szczytu
#endif
  uint32_t now = millis();

  if (last_draw_time + refresh_time > now) {
    return;
  } else {
    last_draw_time = now;
  }

  bool played = player.isRunning();
  if (played) {
    // Przechowywanie poziomów do wycofania.
    measL = (L >= measL) ? measL + _bands.fadespeed : L;  // Tyle cofa z całego paska L
    measR = (R >= measR) ? measR + _bands.fadespeed : R;  // Tyle cofa z całego paska R

    // --- Logika szczytu ---
#ifdef VU_PEAK
#ifndef BOOMBOX_STYLE
    if (dimension - measL > peakL) {
      peakL = dimension - measL;
      peakL_time = now;
    } else if (now - peakL_time > peak_hold_ms && peakL > 0) {
      peakL = (peakL > peak_decay_step) ? peakL - peak_decay_step : 0;
    }
#else
    // Serial.printf("peakL : %d, measL : %d, peak_decay_step : %d , dimension : %d \n", peakL, measL, peak_decay_step, dimension);
    if (measL < peakL) {
      peakL = measL;
      peakL_time = now;
    } else if (now - peakL_time > peak_hold_ms && peakL >= 0) {
      peakL = (peakL < dimension) ? peakL + peak_decay_step : dimension;
      // Serial.printf("ki peakL : %d, measL : %d, peak_decay_step : %d \n", peakL, measL, peak_decay_step);
    }
#endif

    if (dimension - measR > peakR) {
      peakR = dimension - measR;
      peakR_time = now;
    } else if (now - peakR_time > peak_hold_ms && peakR > 0) {
      peakR = (peakR > peak_decay_step) ? peakR - peak_decay_step : 0;
    }
#endif  // VU_PEAK
  } else {
    if (measL < dimension) {
      measL += _bands.fadespeed;
    }
    if (measR < dimension) {
      measR += _bands.fadespeed;
    }
  }

  // Zabezpieczenie Clamp
  if (measL > dimension) {
    measL = dimension;
  }
  if (measR > dimension) {
    measR = dimension;
  }

#ifndef BOOMBOX_STYLE
  // dwa VU jeden pod drugim
  _canvas->fillRect(0, 0, _bands.width, _bands.height * 2 + _bands.space, _bgcolor);  // usuwanie paska
#else
  // dwa VU obok siebie
  _canvas->fillRect(0, 0, _bands.width * 2 + _bands.space, _bands.height, _bgcolor);  // usuwanie paska
#endif

  // --- Rysowanie paska LED z gradientem kolorów ---
  int green_end = (_bands.width * 65) / 100;   // przy 70% koniec zielonego
  int yellow_end = (_bands.width * 85) / 100;  // przy 85% koniec żółtego, od tego czerwony

  uint8_t h = (dimension / _bands.perheight) - _bands.vspace;
  for (int i = 0; i < dimension; i++) {
    if (i % (dimension / _bands.perheight) == 0) {
      if (i < green_end) {
        bandColor = _vumincolor;
      } else if (i < yellow_end) {
        bandColor = _vumidcolor;
      } else {
        bandColor = _vumaxcolor;
      }
#ifndef BOOMBOX_STYLE
      // bandColor = (i > _bands.width - (_bands.width / _bands.perheight) * 4) ? _vumaxcolor : _vumincolor;
      _canvas->fillRect(i, 0, h, _bands.height, bandColor);  //
      _canvas->fillRect(i, _bands.height + _bands.space, h, _bands.height, bandColor);
#else  // Jeśli jest BOMBOX_STYLE.
      /* Kolorowanie lewego paska czerwony - żółty - zielony */
      if (i < _bands.width - yellow_end) {
        bandColor = _vumaxcolor;
      } else if (i < _bands.width - green_end) {
        bandColor = _vumidcolor;
      } else {
        bandColor = _vumincolor;
      }
      _canvas->fillRect(i, 0, h, _bands.height, bandColor);  // lewy kanał
      /* Kolorowanie prawego paska zielony - żółty - czerwony */
      if (i < green_end) {
        bandColor = _vumincolor;
      } else if (i < yellow_end) {
        bandColor = _vumidcolor;
      } else {
        bandColor = _vumaxcolor;
      }
      _canvas->fillRect(i + _bands.width + _bands.space, 0, h, _bands.height, bandColor);  // prawy kanał
#endif
    }
  }
#ifndef BOOMBOX_STYLE
  // --- Wycofywanie na podstawie bieżącego poziomu ---
  _canvas->fillRect(_bands.width - measL, 0, measL, _bands.height, _bgcolor);
  _canvas->fillRect(_bands.width - measR, _bands.height + _bands.space, measR, _bands.height, _bgcolor);

#ifdef VU_PEAK
  // --- Rysowanie szczytów ---
  const uint16_t peak_color = 0xFFFF;
  const uint16_t peak_bright = 0xF7FF;
  int peak_width = 1;

  // Bal csatorna
  if (peakL >= 2 && peakL < (int)_bands.width - peak_width) {
    _canvas->fillRect(peakL - 1, 0, peak_width + 2, _bands.height, peak_bright);
    _canvas->fillRect(peakL, 0, peak_width, _bands.height, peak_color);
  }

  // Jobb csatorna
  if (peakR >= 2 && peakR < (int)_bands.width - peak_width) {
    _canvas->fillRect(peakR - 1, _bands.height + _bands.space, peak_width + 2, _bands.height, peak_bright);
    _canvas->fillRect(peakR, _bands.height + _bands.space, peak_width, _bands.height, peak_color);
  }
#endif  // VU_PEAK

  // --- Końcowe rysowanie ---
  dsp.drawRGBBitmap(_config.left, _config.top, _canvas->getBuffer(), _bands.width, _bands.height * 2 + _bands.space);

#else
  // --- Wycofywanie na podstawie bieżącego poziomu ---
  _canvas->fillRect(0, 0, measL, _bands.height, _bgcolor);
  _canvas->fillRect(_bands.width * 2 + _bands.space - measR, 0, measR, _bands.height, _bgcolor);
#ifdef VU_PEAK
  // --- Rysowanie szczytów ---
  const uint16_t peak_color = 0xFFFF;
  const uint16_t peak_bright = 0xF7FF;
  int peak_width = 1;
  // Bal csatorna

  if (peakL >= 2 && peakL < (int)_bands.width - peak_width) {
    //Serial.printf("peakL : %d, measL : %d \n", peakL, measL);
    _canvas->fillRect(peakL - 1, 0, peak_width + 2, _bands.height, peak_bright);
    _canvas->fillRect(peakL, 0, peak_width, _bands.height, peak_color);
  }
  // Jobb csatorna
  if (peakR >= 2 && peakR < (int)_bands.width - peak_width) {
    _canvas->fillRect(_bands.width + _bands.space + peakR - 1, 0, peak_width + 2, _bands.height, peak_bright);
    _canvas->fillRect(_bands.width + _bands.space + peakR, 0, peak_width, _bands.height, peak_color);
  }
#endif  // VU_PEAK
  dsp.startWrite();
  dsp.setAddrWindow(_config.left + 4, _config.top + 10, _bands.width * 2 + _bands.space, _bands.height);
  dsp.writePixels((uint16_t *)_canvas->getBuffer(), (_bands.width * 2 + _bands.space) * _bands.height);
  dsp.endWrite();
#endif

  // --- Rysowanie etykiet L/R ---
#ifndef BOOMBOX_STYLE
  if (played && !_labelsDrawn) {
    // Serial.println("Rysowanie L/R");
    int label_width = _bands.height + 15;
    int label_height = _bands.height + 4;
    int label_offset = label_width + 4;
    int label_left = _config.left - label_offset;
    if (label_left >= 0) {
      dsp.fillRect(label_left, _config.top - 4, label_width, label_height, 0x7BEF);
      dsp.fillRect(label_left, _config.top + _bands.height + _bands.space, label_width, label_height - 1, 0x7BEF);
      dsp.setTextSize(1);
      dsp.setFont();
      dsp.setTextColor(0xFFFF);
      int text_x = label_left + (label_width - 6) / 2;
      int text_y_L = (_config.top - 2) + (label_height - 10) / 2;
      int text_y_R = _config.top + _bands.height + _bands.space + (label_height - 8) / 2;
      dsp.setCursor(text_x, text_y_L);
      dsp.print("L");
      dsp.setCursor(text_x, text_y_R);
      dsp.print("R");
      _labelsDrawn = true;
    }
  }
#else
  if (played && !_labelsDrawn) {
    // Serial.println("Rysowanie L/R");
    int label_width = _bands.height + 15;
    int label_height = _bands.height + 4;
    int total_width = 2 * label_width + 6;
    int center_left = (dsp.width() - total_width) / 2;
    int label_left_L = center_left;
    int label_left_R = center_left + label_width + 6;

    // Lewy (L) prostokąt
    dsp.fillRect(label_left_L, _config.top - 4, label_width, label_height, 0x7BEF);
    dsp.setTextSize(1);
    dsp.setFont();
    dsp.setTextColor(0xFFFF);
    int text_x_L = label_left_L + (label_width - 6) / 2;
#if DSP_MODEL == DSP_ILI9341
    int text_y = ((_config.top - 2) + (label_height - 10) / 2) - 1;
#else
    int text_y = (_config.top - 2) + (label_height - 10) / 2;
#endif
    dsp.setCursor(text_x_L, text_y);
    dsp.print("L");

    // Prawy (R) prostokąt
    dsp.fillRect(label_left_R, _config.top - 4, label_width, label_height, 0x7BEF);
    int text_x_R = label_left_R + (label_width - 6) / 2;
    dsp.setCursor(text_x_R, text_y);
    dsp.print("R");

    _labelsDrawn = true;
  }
#endif
}

void VuWidget::loop() {
  if (_active || !_locked) {
    _draw();
  }
}

void VuWidget::_clear() {
  // dsp.fillRect(_config.left, _config.top, _bands.width * 2 + _bands.space, _bands.height, _bgcolor);
  dsp.fillRect(0, _config.top - 4, 479, 24, _bgcolor);
  _labelsDrawn = false;  // L i R muszą być ponownie narysowane. Modyfikacja.
  // Serial.println("widget.cpp -> VuWidget::_clear()");
}
#else  // DSP_LCD
VuWidget::~VuWidget() {}
void VuWidget::init(WidgetConfig wconf, VUBandsConfig bands, uint16_t vumaxcolor, uint16_t vumincolor, uint16_t bgcolor) {
  Widget::init(wconf, bgcolor, bgcolor);
}
void VuWidget::_draw() {}
void VuWidget::loop() {}
void VuWidget::_clear() {}
#endif

// clang-format off
/************************
      NUM & CLOCK
 ************************/
#if !defined(DSP_LCD)
  #if TIME_SIZE<19 //19->NOKIA
  const GFXfont* Clock_GFXfontPtr = nullptr;
  #define CLOCKFONT5x7
  #else
  const GFXfont* Clock_GFXfontPtr = &Arimo_Regular_72;  // Zmiana na Arimo 72
  const GFXfont* Clock_GFXfontPtr_Sec = &Arimo_Regular_36;  // Zmiana na Arimo 36 dla sekund
  #endif
#endif //!defined(DSP_LCD)

#if !defined(CLOCKFONT5x7) && !defined(DSP_LCD)
  inline GFXglyph *pgm_read_glyph_ptr(const GFXfont *gfxFont, uint8_t c) {
    return gfxFont->glyph + c;
  }
  uint8_t _charWidth(unsigned char c){
    // Obsługa Arimo - sprawdź czy znak jest w zakresie 0x30-0x3A
    if(c >= 0x30 && c <= 0x3A) {
      GFXglyph *glyph = pgm_read_glyph_ptr(&Arimo_Regular_72, c - 0x30);
      return pgm_read_byte(&glyph->xAdvance);
    } else {
      // Dla spacji lub innych znaków zwróć domyślną szerokość
      return 21;  // Szerokość spacji dla Arimo 72pt
    }
  }
  uint16_t _textHeight(){
    GFXglyph *glyph = pgm_read_glyph_ptr(&Arimo_Regular_72, '8' - 0x30);
    return pgm_read_byte(&glyph->height);
  }
#else //!defined(CLOCKFONT5x7) && !defined(DSP_LCD)
  uint8_t _charWidth(unsigned char c){
  #ifndef DSP_LCD
    return CHARWIDTH * TIME_SIZE;
  #else
    return 1;
  #endif
  }
  uint16_t _textHeight(){
    return CHARHEIGHT * TIME_SIZE;
  }
#endif
uint16_t _textWidth(const char *txt){
  uint16_t w = 0, l=strlen(txt);
  for(uint16_t c=0;c<l;c++) w+=_charWidth(txt[c]);
//  #if DSP_MODEL==DSP_ILI9225
//  return w+l;
//  #else
  return w;
//  #endif
}

/************************
      NUM WIDGET
 ************************/
void NumWidget::init(WidgetConfig wconf, uint16_t buffsize, bool uppercase, uint16_t fgcolor, uint16_t bgcolor) {
  Widget::init(wconf, fgcolor, bgcolor);
  _buffsize = buffsize;
  _text = (char *) malloc(sizeof(char) * _buffsize);
  memset(_text, 0, _buffsize);
  _oldtext = (char *) malloc(sizeof(char) * _buffsize);
  memset(_oldtext, 0, _buffsize);
  _textwidth = _oldtextwidth = _oldleft = 0;
  _uppercase = uppercase;
  _textheight = TIME_SIZE/*wconf.textsize*/;
}

void NumWidget::setText(const char* txt) {
  strlcpy(_text, txt, _buffsize);
  _getBounds();
  if (strcmp(_oldtext, _text) == 0) return;
  uint16_t realth = _textheight;
#if defined(DSP_OLED) && DSP_MODEL!=DSP_SSD1322
  if(Clock_GFXfontPtr==nullptr) realth = _textheight * 8; //CHARHEIGHT
#endif
  if (_active)
  #ifndef CLOCKFONT5x7
    dsp.fillRect(_oldleft == 0 ? _realLeft() : min(_oldleft, _realLeft()),  _config.top-_textheight+1, max(_oldtextwidth, _textwidth), realth, _bgcolor);
  #else
    dsp.fillRect(_oldleft == 0 ? _realLeft() : min(_oldleft, _realLeft()),  _config.top, max(_oldtextwidth, _textwidth), realth, _bgcolor);
  #endif

  _oldtextwidth = _textwidth;
  _oldleft = _realLeft();
  if (_active) _draw();
}

void NumWidget::setText(int val, const char *format){
  char buf[_buffsize];
  snprintf(buf, _buffsize, format, val);
  setText(buf);
}

void NumWidget::_getBounds() {
  _textwidth= _textWidth(_text);
}

void NumWidget::_draw() {
#ifndef DSP_LCD
  if(!_active || TIME_SIZE<2) return;
  dsp.setTextSize(Clock_GFXfontPtr==nullptr?TIME_SIZE:1);
  dsp.setFont(Clock_GFXfontPtr);
  dsp.setTextColor(_fgcolor, _bgcolor);
#endif
  if(!_active) return;
  dsp.setCursor(_realLeft(), _config.top);
  dsp.print(_text);
  strlcpy(_oldtext, _text, _buffsize);
  dsp.setFont();
}

/**************************
      PROGRESS WIDGET
 **************************/
void ProgressWidget::_progress() {
  char buf[_width + 1];
  snprintf(buf, _width, "%*s%.*s%*s", _pg <= _barwidth ? 0 : _pg - _barwidth, "", _pg <= _barwidth ? _pg : 5, ".....", _width - _pg, "");
  _pg++; if (_pg >= _width + _barwidth) _pg = 0;
  setText(buf);
}

bool ProgressWidget::_checkDelay(int m, uint32_t &tstamp) {
  if (millis() - tstamp > m) {
    tstamp = millis();
    return true;
  } else {
    return false;
  }
}

void ProgressWidget::loop() {
  if (_checkDelay(_speed, _scrolldelay)) {
    _progress();
  }
}

/**************************
      CLOCK WIDGET
 **************************/
void ClockWidget::init(WidgetConfig wconf, uint16_t fgcolor, uint16_t bgcolor){
  Widget::init(wconf, fgcolor, bgcolor);
  _timeheight = _textHeight();
  _fullclock = TIME_SIZE>35 || DSP_MODEL==DSP_ILI9225;
  if(_fullclock) _superfont = TIME_SIZE / 17; //magick
  else if(TIME_SIZE==19 || TIME_SIZE==2) _superfont=1;
  else _superfont=0;
  _space = (5*_superfont)/2; //magick
  #ifndef HIDE_DATE
  if(_fullclock){
    _dateheight = _superfont<4?1:2;
    // _clockheight = _timeheight + _space + CHARHEIGHT * _dateheight; //Original
    _clockheight = _timeheight;
  } else {
    _clockheight = _timeheight;
  }
  #else
    _clockheight = _timeheight;
  #endif
  _getTimeBounds();
#ifdef PSFBUFFER
  _fb = new psFrameBuffer(dsp.width(), dsp.height());
  _begin();
#endif
}

void ClockWidget::_begin(){
#ifdef PSFBUFFER
  _fb->begin(&dsp, _clockleft, _config.top-_timeheight, _clockwidth, _clockheight+1, config.theme.background);
#endif
}

bool ClockWidget::_getTime(){
  strftime(_timebuffer, sizeof(_timebuffer), "%H:%M", &network.timeinfo);
  bool ret = network.timeinfo.tm_sec==0 || _forceflag!=network.timeinfo.tm_year;
  _forceflag = network.timeinfo.tm_year;
  return ret;
}

uint16_t ClockWidget::_left(){
  if(_fb->ready()) return 0; else return _clockleft;
}
uint16_t ClockWidget::_top(){
  if(_fb->ready()) return _timeheight; else return _config.top;
}

void ClockWidget::_getTimeBounds() {
  _timewidth = _textWidth(_timebuffer);
  uint8_t fs = _superfont>0?_superfont:TIME_SIZE;
  uint16_t rightside = CHARWIDTH * fs * 2; // seconds
  if(_fullclock){
    rightside += _space*2+1; //2space+vline
    _clockwidth = _timewidth+rightside;
  } else {
    if(_superfont==0)
      _clockwidth = _timewidth;
    else
      _clockwidth = _timewidth + rightside;
  }
  switch(_config.align){
    case WA_LEFT: _clockleft = _config.left; break;
    case WA_RIGHT: _clockleft = dsp.width()-_clockwidth-_config.left; break;
    default:
      _clockleft = (dsp.width()/2 - _clockwidth/2)+_config.left;
      break;
  }
  char buf[4];
  strftime(buf, 4, "%H", &network.timeinfo);
  _dotsleft=_textWidth(buf);
}

#ifndef DSP_LCD

Adafruit_GFX& ClockWidget::getRealDsp(){
#ifdef PSFBUFFER
  if (_fb && _fb->ready()) return *_fb;
#endif
  return dsp;
}

void ClockWidget::_printClock(bool force){
  auto& gfx = getRealDsp();
  gfx.setTextSize(Clock_GFXfontPtr==nullptr?TIME_SIZE:1);
  gfx.setFont(Clock_GFXfontPtr);
  bool clockInTitle=!config.isScreensaver && _config.top<_timeheight; //DSP_SSD1306x32
  if(force){
    _clearClock();
    _getTimeBounds();
    #ifndef DSP_OLED
    if(CLOCKFONT_MONO) {
      gfx.setTextColor(config.theme.clockbg, config.theme.background);
      gfx.setCursor(_left(), _top());
      gfx.print("88:88");
    }
    #endif
    if(clockInTitle)
      gfx.setTextColor(config.theme.meta, config.theme.metabg);
    else
      gfx.setTextColor(config.theme.clock, config.theme.background);
    gfx.setCursor(_left(), _top());
    gfx.print(_timebuffer);
    if(_fullclock){
      // Pozioma i pionowa linia sekundy.
      //Serial.printf("config.screensaver: %d,  _fb->ready(): %d \n", config.isScreensaver, _fb->ready());
      bool fullClockOnScreensaver = (!config.isScreensaver || (_fb->ready() && FULL_SCR_CLOCK));
      _linesleft = _left()+_timewidth+_space;
      if(fullClockOnScreensaver){
      // gfx. zapisuje do obszaru bufora, dsp. bezpośrednio na ekran.
        // Usunieto linie między minutami a sekundami - REQUEST
        // gfx.drawFastVLine(_linesleft, _top()-_timeheight, _timeheight, config.theme.div);
      #if DSP_MODEL == DSP_ILI9341  // Modyfikacja 
        // gfx.drawFastHLine(_linesleft, _top()-(_timeheight)/2 + 17, CHARWIDTH * _superfont * 2 + _space, config.theme.div);
      #else                                    // DSP_MODEL DSP_ILI9341
        // gfx.drawFastHLine(_linesleft, _top()-(_timeheight)/2 + 25, CHARWIDTH * _superfont * 2 + _space, config.theme.div);
      #endif
       
      if (!config.isScreensaver) {
        // dsp.setTextSize(_superfont);
        // dsp.setCursor(_linesleft + _space + 1, _top() - CHARHEIGHT * _superfont);
        // dsp.setTextColor(config.theme.dow, config.theme.background);
        // gfx.print(utf8To(LANG::dow[network.timeinfo.tm_wday], false)); 
        // sprintf(_tmp, "%2d %s %d", network.timeinfo.tm_mday,LANG::mnths[network.timeinfo.tm_mon], network.timeinfo.tm_year+1900);
        // "wielojęzyczność"
        #if L10N_LANGUAGE == RU
                  sprintf(_tmp, "%2d %s %d", network.timeinfo.tm_mday, LANG::mnths[network.timeinfo.tm_mon], network.timeinfo.tm_year + 1900);
        #elif L10N_LANGUAGE == EN
                  sprintf(_tmp, "%2d %s %d", network.timeinfo.tm_mday, LANG::mnths[network.timeinfo.tm_mon], network.timeinfo.tm_year + 1900);
        #elif L10N_LANGUAGE == PL
                  // Sprawdź czy czas jest zsynchronizowany (tm_year > 100 = po roku 2000)
                  if(network.timeinfo.tm_year > 100) {
                    // Czas zsynchronizowany - wyświetl dzień tygodnia
                    int wday = network.timeinfo.tm_wday;
                    if(wday < 0 || wday > 6) wday = 0; // Zabezpieczenie
                    sprintf(_tmp, "%s - %02d.%s.%04d", LANG::dowf[wday], network.timeinfo.tm_mday, LANG::mnths[network.timeinfo.tm_mon], network.timeinfo.tm_year + 1900);
                  } else {
                    // Czas nie zsynchronizowany - wyświetl tylko datę bez dnia tygodnia
                    sprintf(_tmp, "%02d.%s.%04d", network.timeinfo.tm_mday, LANG::mnths[network.timeinfo.tm_mon], network.timeinfo.tm_year + 1900);
                  }
        #endif
        #ifndef HIDE_DATE
            // Oblicz szerokość i pozycję daty
            strlcpy(_datebuf, utf8To(_tmp, true), sizeof(_datebuf));
            uint16_t _datewidth = strlen(_datebuf) * CHARWIDTH*_dateheight;
            uint16_t _dateleft = dsp.width() - _datewidth - dateConf.left;
            // Usunięcie linii tylko w miejscu daty (nie cały ekran)
            int lineHeight = _dateheight * 8;   // kb. 8 pixel per TextSize
            dsp.fillRect(_dateleft, dateConf.top, _datewidth, lineHeight, config.theme.background);
            dsp.setFont();
            dsp.setTextSize(_dateheight);
            #if DSP_MODEL==DSP_GC9A01A
              dsp.setCursor((dsp.width()-_datewidth)/2, _top() + _space);
            #else
              dsp.setCursor(_left()+_clockwidth-_datewidth, _top() + _space);
            #endif
            dsp.setCursor(_dateleft, dateConf.top);       // Modyfikacja własna zmienna ustawienia "dateConf"
            
            // Kolorowanie TYLKO nazwy dnia tygodnia - sobota: niebieski, niedziela: czerwony
            #if L10N_LANGUAGE == PL
            if(network.timeinfo.tm_year > 100) {
              // Jeśli mamy dzień tygodnia w dacie, wyświetl go osobno w kolorze
              int wday = network.timeinfo.tm_wday;
              uint16_t dowColor;
              
              if(wday == 6) {
                // Sobota - jasnoniebieski
                dowColor = 0x06BF; // RGB565: jasnoniebieski (cyan)
              } else if(wday == 0) {
                // Niedziela - jasnoczerwony
                dowColor = 0xF9C7; // RGB565: jasnoczerwony (255,57,57)
              } else {
                // Pozostałe dni - domyślny kolor
                dowColor = config.theme.date;
              }
              
              // Wyświetl nazwę dnia tygodnia w kolorze
              char dowName[32];
              strlcpy(dowName, utf8To(LANG::dowf[wday], true), sizeof(dowName));
              dsp.setTextColor(dowColor, config.theme.background);
              dsp.print(dowName);
              
              // Wyświetl resztę daty w domyślnym kolorze
              char dateOnly[32];
              sprintf(dateOnly, " - %02d.%s.%04d", network.timeinfo.tm_mday, LANG::mnths[network.timeinfo.tm_mon], network.timeinfo.tm_year + 1900);
              char dateOnlyBuf[32];
              strlcpy(dateOnlyBuf, utf8To(dateOnly, true), sizeof(dateOnlyBuf));
              dsp.setTextColor(config.theme.date, config.theme.background);
              dsp.print(dateOnlyBuf);
            } else {
              // Brak zsynchronizowanego czasu - wyświetl całość w domyślnym kolorze
              dsp.setTextColor(config.theme.date, config.theme.background);
              dsp.print(_datebuf);
            }
            #else
            // Dla innych języków - bez kolorowania
            dsp.setTextColor(config.theme.date, config.theme.background);
            dsp.print(_datebuf);
            #endif
           // Serial.printf("widget.cpp -> _left() %d \n", _left());
           // Serial.printf("widget.cpp -> _datebuf %s \n", _datebuf);
        #endif // HIDE_DATE
      }
    }
    }
  }
  if(_fullclock || _superfont>0){
    gfx.setTextSize(0);          // *** Wyświetl sekundy ***
    gfx.setFont(Clock_GFXfontPtr_Sec);
    if (CLOCKFONT_MONO) {
      gfx.setTextColor(config.theme.clockbg, config.theme.background);
    } else {
      gfx.setTextColor(config.theme.background, config.theme.background);
    }
    uint16_t topSec ;
    #if DSP_MODEL == DSP_ILI9341  // Modyfikacja 
     topSec = _top() - _timeheight + 38;
    #else                                    // DSP_MODEL DSP_ILI9341
     topSec = _top() - _timeheight + 50;
    #endif
    gfx.setCursor(_left() + _timewidth + _space + 3, topSec);
    // Lepsze czyszczenie dla czcionek proporcjonalnych
    gfx.fillRect(_left() + _timewidth + _space + 3, topSec - 30, 60, 35, config.theme.background);
    gfx.setTextColor(config.theme.seconds, config.theme.background);
    gfx.setCursor(_left() + _timewidth + _space + 3, topSec);
    sprintf(_tmp, "%02d", network.timeinfo.tm_sec);
    gfx.print(_tmp);
    
    // Rysuj ikonę TTS jeśli jest włączona (po sekundach, żeby nie była kasowana)
    gfx.setFont();
    _drawTTSIcon(gfx);
  }
  gfx.setTextSize(Clock_GFXfontPtr==nullptr?TIME_SIZE:1);
  gfx.setFont(Clock_GFXfontPtr);
  #ifndef DSP_OLED
  gfx.setTextColor(dots ? config.theme.clock : (CLOCKFONT_MONO?config.theme.clockbg:config.theme.background), config.theme.background);
  #else
  if(clockInTitle)
    gfx.setTextColor(dots ? config.theme.meta:config.theme.metabg, config.theme.metabg);
  else
    gfx.setTextColor(dots ? config.theme.clock:config.theme.background, config.theme.background);
  #endif
  dots=!dots;
  gfx.setCursor(_left()+_dotsleft, _top());
  gfx.print(":");
  gfx.setFont();
  
  if(_fb->ready()) _fb->display();
  // Pobierz dzisiejszą datę imienin - tylko jeśli włączone.
  #ifdef NAMEDAYS_FILE
  if(config.store.nameday){
    static uint32_t lastRotation = 0;
    if (millis() - lastRotation >= 4000) {
        getNamedayUpper(_namedayBuf, sizeof(_namedayBuf));
        if (!config.isScreensaver && strcmp(_oldNamedayBuf, _namedayBuf) != 0) {
          strlcpy(_oldNamedayBuf, _namedayBuf, sizeof(_oldNamedayBuf));
          _namedaywidth = strlen(_namedayBuf) * CHARWIDTH * namedayConf.textsize; // przeliczamy tylko przy zmianie
          _printNameday();
        }
        lastRotation = millis();
    }
  }else{
     
  }
#endif // NAMEDAYS_FILE

}

#ifdef NAMEDAYS_FILE
void ClockWidget::getNamedayUpper(char *dest, size_t len) { // zadeklarowane w commongfx.h
  const char *nameday = getNameDay(network.timeinfo.tm_mon + 1, network.timeinfo.tm_mday);
  char        tmp[32];
  strlcpy(tmp, nameday, sizeof(tmp));
  for (int i = 0; tmp[i]; i++) {
    tmp[i] = toupper((unsigned char)tmp[i]);
  }
  strlcpy(dest, utf8To(tmp, true), len);
}

void ClockWidget::_printNameday() {
  uint16_t nameday_top;
  #if DSP_MODEL==DSP_ILI9341
    nameday_top = namedayConf.top + 14;
  #else
    nameday_top = namedayConf.top + 25;  // Dodano 3px (1mm) między "Imieniny:" a imieniem
  #endif
  if(config.store.nameday){
    dsp.setTextColor(dsp.color565(100, 180, 255), config.theme.background);  // Jasny niebieski dla "Imieniny:"
    // Napis "Imieniny:" - obniżony o 6px (2mm)
    dsp.setCursor(namedayConf.left, namedayConf.top + 6); 
    // Napis "Imieniny:" - mniejsza czcionka (rozmiar 1)
    dsp.setTextSize(1);
    if (!config.isScreensaver){
      dsp.print(utf8To(nameday_label, false)); // <<< Tutaj już pochodzi z nagłówka "nameday"
      // Rysuje tylko nazwy kolorem złotym
      dsp.setTextColor(config.theme.nameday, config.theme.background); // szary 0x8410
      // Obszar nazwy imienin do wyczyszczenia z ekranu.
      int clearWidth = max(_oldnamedaywidth, _namedaywidth); // Szerokość szerszej ze starej i nowej nazwy.
      dsp.fillRect(namedayConf.left, nameday_top, clearWidth, CHARHEIGHT * namedayConf.textsize, config.theme.background);
      dsp.setCursor(namedayConf.left, nameday_top);
      dsp.setTextSize(namedayConf.textsize);
      dsp.print(_namedayBuf);
      strlcpy(_oldNamedayBuf, _namedayBuf, sizeof(_namedayBuf));
      _oldnamedaywidth = _namedaywidth;
    }
  }
}
#endif //NAMEDAYS_FILE

void ClockWidget::_clearClock(){
#ifdef PSFBUFFER
  if(_fb->ready()) _fb->clear();
  else
#endif
#ifndef CLOCKFONT5x7
    // dsp.fillRect(_left(), _top()-_timeheight, _clockwidth, _clockheight+1, 0x8410);
  dsp.fillRect(_left(), _top()-(_timeheight + 2), _clockwidth, _clockheight+3, config.theme.background);
// Serial.println("Czyszczenie");
#else
  dsp.fillRect(_left(), _top(), _clockwidth+1, _clockheight+1, config.theme.background);
#endif
}

void ClockWidget::_drawTTSIcon(Adafruit_GFX& gfx) {
  // Pozycja ikony - nad sekundami (używamy współrzędnych dla gfx)
  uint16_t iconX = _left() + _timewidth + _space;
  uint16_t iconY;
  
  #if DSP_MODEL == DSP_ILI9341
    iconY = _top() - _timeheight - 5; // Nad sekundami (przesunięto o 5mm w górę)
  #else
    iconY = _top() - _timeheight; // Nad sekundami (przesunięto o 5mm w górę)
  #endif

  // Sprawdź czy TTS jest włączony
  if (!config.store.clock_tts_enabled) {
    // Jeśli wyłączony, wyczyść obszar ikony
    gfx.fillRect(iconX, iconY, 60, 20, config.theme.background);
    return;
  }

  // Kolor ikony - pomarańczowy
  uint16_t ttsColor = 0xFD20; // Pomarańczowy (RGB: 255, 165, 0)
  
  // Najpierw wyczyść obszar
  gfx.fillRect(iconX, iconY, 60, 20, config.theme.background);
  
  // Rysuj tekst "Clock TTS" mniejszymi literkami (rozmiar 1)
  gfx.setTextSize(1);
  gfx.setFont();
  gfx.setTextColor(ttsColor, config.theme.background);
  gfx.setCursor(iconX, iconY + 4); // bez przesunięcia w prawo, +4 w dół dla centrowania
  gfx.print("Clock TTS");
}

void ClockWidget::draw(bool force){
  if(!_active) return;
  _printClock(_getTime() || force);
}

void ClockWidget::_draw(){
  if(!_active) return;
  _printClock(true);
}

void ClockWidget::_reset(){
#ifdef PSFBUFFER
  if(_fb->ready()) {
    _fb->freeBuffer();
    _getTimeBounds();
    _begin();
  }
#endif
}

void ClockWidget::_clear(){
  _clearClock();
}
#else //#ifndef DSP_LCD

void ClockWidget::_printClock(bool force){
  strftime(_timebuffer, sizeof(_timebuffer), "%H:%M", &network.timeinfo);
  if(force){
    dsp.setCursor(dsp.width()-5, 0);
    dsp.print(_timebuffer);
  }
  dsp.setCursor(dsp.width()-5+2, 0);
  dsp.print((network.timeinfo.tm_sec % 2 == 0)?":":" ");
}

void ClockWidget::_clearClock(){}

void ClockWidget::draw(){
  if(!_active) return;
  _printClock(true);
}
void ClockWidget::_draw(){
  if(!_active) return;
  _printClock(true);
}
void ClockWidget::_reset(){}
void ClockWidget::_clear(){}
#endif //#ifndef DSP_LCD

// clang-format on
/**************************
      BITRATE WIDGET
 **************************/
void BitrateWidget::init(BitrateConfig bconf, uint16_t fgcolor, uint16_t bgcolor) {
  Widget::init(bconf.widget, fgcolor, bgcolor);
  _dimension = bconf.dimension;
  _bitrate = 0;
  _format = BF_UNKNOWN;
  _charSize(bconf.widget.textsize, _charWidth, _textheight);
  memset(_buf, 0, 6);
}

void BitrateWidget::setBitrate(uint16_t bitrate) {
  _bitrate = bitrate;  // Modyfikacja
                       //  if(_bitrate>999) _bitrate = 999;
  if (_bitrate > 20000) {
    _bitrate = _bitrate / 1000;
  }
  _draw();
}

void BitrateWidget::setFormat(BitrateFormat format) {
  _format = format;
  _draw();
}

//TODO move to parent
void BitrateWidget::_charSize(uint8_t textsize, uint8_t &width, uint16_t &height) {
#ifndef DSP_LCD
  width = textsize * CHARWIDTH;
  height = textsize * CHARHEIGHT;
#else
  width = 1;
  height = 1;
#endif
}

void BitrateWidget::_draw(){
  _clear();
  if(!_active || _format == BF_UNKNOWN || _bitrate==0) return;
  // Margines 3px wokół ikonki (0.5mm na każdym boku)
  dsp.drawRect(_config.left - 3, _config.top - 3, _dimension + 6, _dimension + 6, _fgcolor);
  dsp.fillRect(_config.left, _config.top + _dimension/2, _dimension, _dimension/2, _fgcolor);
  dsp.setFont();
  dsp.setTextSize(_config.textsize);
  dsp.setTextColor(_fgcolor, _bgcolor);
  if (_bitrate < 999) {
    snprintf(_buf, 6, "%d", _bitrate);
  } else {
    float _br = (float)_bitrate / 1000;
    snprintf(_buf, 6, "%.1f", _br);
  }
  dsp.setCursor(_config.left + _dimension/2 - _charWidth*strlen(_buf)/2 + 1, _config.top + _dimension/4 - _textheight/2+1);
  dsp.print(_buf);
  dsp.setTextColor(_bgcolor, _fgcolor);
  dsp.setCursor(_config.left + _dimension/2 - _charWidth*3/2 + 1, _config.top + _dimension - _dimension/4 - _textheight/2);
  switch(_format){
    case BF_MP3:  dsp.print("MP3"); break;
    case BF_AAC:  dsp.print("AAC"); break;
    case BF_FLAC: dsp.print("FLC"); break;
    case BF_OGG:  dsp.print("OGG"); break;
    case BF_WAV:  dsp.print("WAV"); break;
    case BF_VOR:  dsp.print("VOR"); break;
    case BF_OPU:  dsp.print("OPU"); break;
    default:      break;
  }
}

void BitrateWidget::_clear() {
  // Czyszczenie z marginesem 3px
  dsp.fillRect(_config.left - 3, _config.top - 3, _dimension + 6, _dimension + 6, _bgcolor);
}

/* Törli mindkét bitratewidget területét és a "nameday" területet is. */
void BitrateWidget::clearAll() {
    // Wyczyść BitrateWidget - normalny obszar
    dsp.fillRect(_config.left, _config.top, _dimension * 2, _dimension, _bgcolor ); 
    // Wyczyść obszar namedays - TYLKO LEWA STRONA ekranu (nie zahaczaj o zegar i datę)
    // Szerokość: maksymalnie 200px aby nie zahaczać o zegar który zaczyna się około x=200
    // Część 1: Etykieta "Imieniny:" - pozycja zgodna z namedayConf.top + 6
    dsp.fillRect(0, namedayConf.top + 6, 200, 12, _bgcolor);
    // Część 2: Nazwa imienia - pozycja zgodna z nameday_top (namedayConf.top + 25)
    dsp.fillRect(0, namedayConf.top + 25, 200, CHARHEIGHT * namedayConf.textsize + 4, _bgcolor);
   // Serial.printf("widgets.cpp->BitrateWidget clearAll() \n") ;
}

// clang-format off

/**************************
      PLAYLIST WIDGET
 **************************/
// =================================
// PLAYLIST WIDGET - FINALNA WERSJA 
// =================================

#define MAX_PL_PAGE_ITEMS 15 
static String _plCache[MAX_PL_PAGE_ITEMS];
static int16_t _plLoadedPage = -1;
static int16_t _plLastGlobalPos = -1;
static uint32_t _plLastDrawTime = 0; 

void PlayListWidget::init(ScrollWidget *current) {
  Widget::init({0, 0, 0, WA_LEFT}, 0, 0);
  _current = current;

  #ifndef DSP_LCD
    _plItemHeight = playlistConf.widget.textsize * (CHARHEIGHT - 1) + playlistConf.widget.textsize * 4;
    
    _plTtemsCount = (dsp.height() - 2) / _plItemHeight;
    if (_plTtemsCount < 1) _plTtemsCount = 1;
    if (_plTtemsCount > MAX_PL_PAGE_ITEMS) _plTtemsCount = MAX_PL_PAGE_ITEMS;

    uint16_t contentHeight = _plTtemsCount * _plItemHeight;
    _plYStart = (dsp.height() - contentHeight) / 2;

  #else
    _plTtemsCount = PLMITEMS;
    _plCurrentPos = 0;
  #endif

  _plLoadedPage = -1;
  _plLastGlobalPos = -1;
  _plCurrentPos = 0;
  _plLastDrawTime = 0;
}

void _loadPlaylistPage(int pageIndex, int itemsPerPage, int totalItems) {
  for (int i = 0; i < MAX_PL_PAGE_ITEMS; i++) _plCache[i] = "";

  if (config.playlistLength() == 0) return;

  File playlist = config.SDPLFS()->open(REAL_PLAYL, "r");
  File index = config.SDPLFS()->open(REAL_INDEX, "r");
  
  if (!playlist || !index) return;

  int startIdx = pageIndex * itemsPerPage;

  for (int i = 0; i < itemsPerPage; i++) {
    int currentGlobalIdx = startIdx + i;
    if (currentGlobalIdx >= config.playlistLength()) break;

    index.seek(currentGlobalIdx * 4, SeekSet);
    uint32_t posAddr;
    if (index.readBytes((char *)&posAddr, 4) != 4) break;

    playlist.seek(posAddr, SeekSet);
    String line = playlist.readStringUntil('\n');
    
    int tabIdx = line.indexOf('\t');
    if (tabIdx > 0) line = line.substring(0, tabIdx);
    line.trim();
    
    if (config.store.numplaylist && line.length() > 0) {
      _plCache[i] = String(currentGlobalIdx + 1) + " " + line;
    } else {
      _plCache[i] = line;
    }
  }
  playlist.close();
  index.close();
}

#ifndef DSP_LCD
void PlayListWidget::drawPlaylist(uint16_t currentItem) {
  
  // Jeśli od ostatniego wywołania minęło > 2000ms to uznajemy, że wracamy z innego
  // ekranu i przerysowujemy cały ekran
    
  bool isLongPause = (millis() - _plLastDrawTime > 2000);
  _plLastDrawTime = millis();

  int activeIdx = (currentItem > 0) ? (currentItem - 1) : 0;
  int itemsPerPage = _plTtemsCount;
  int newPage = activeIdx / itemsPerPage;
  int newLocalPos = activeIdx % itemsPerPage;

  _plCurrentPos = newLocalPos; 

  bool pageChanged = (newPage != _plLoadedPage);
  
  // WARUNKI PEŁNEGO RYSOWANIA:
  // 1. Zmiana strony
  // 2. Powrót do playera (V-Tom 2 sekundy / potwierdzenie automatyczne wyboru streamu)
  // 3. Pusty cache (start)
  if (pageChanged || isLongPause || _plCache[0].length() == 0) {
    
    // Jeśli to tylko powrót do tej samej strony, nie musimy czytać z pamięci (oszczędność czasu)
    // Czytamy z pamięci tylko gdy faktycznie zmieniła się strona lub cache jest pusty.
    if (pageChanged || _plCache[0].length() == 0) {
        _loadPlaylistPage(newPage, itemsPerPage, config.playlistLength());
        _plLoadedPage = newPage;
    }
    
    // Rysujemy całe tło i listę (to usuwa śmieci po playerze)
    dsp.fillRect(0, _plYStart, dsp.width(), itemsPerPage * _plItemHeight, config.theme.background);
    
    for (int i = 0; i < itemsPerPage; i++) {
      _printPLitem(i, _plCache[i].c_str());
    }

  } else {
    // SMART REDRAW - Szybka ścieżka dla płynnego przewijania
    int oldLocalPos = (_plLastGlobalPos > 0 ? _plLastGlobalPos - 1 : 0) % itemsPerPage;
    
    if (oldLocalPos != newLocalPos && oldLocalPos >= 0 && oldLocalPos < itemsPerPage) {
       _printPLitem(oldLocalPos, _plCache[oldLocalPos].c_str());
    }

    _printPLitem(newLocalPos, _plCache[newLocalPos].c_str());
  }

  _plLastGlobalPos = currentItem;
}

void PlayListWidget::_printPLitem(uint8_t pos, const char *item) {
  if (pos >= _plTtemsCount) return;

    int16_t yPos = _plYStart + pos * _plItemHeight;
    
    bool isSelected = (pos == _plCurrentPos);
    uint16_t fgColor = isSelected ? config.theme.plcurrent : config.theme.playlist[0];
    uint16_t bgColor = config.theme.background;

    if (item && item[0] != '\0') {
        dsp.setTextColor(fgColor, bgColor);
        dsp.setTextSize(playlistConf.widget.textsize);
        dsp.setCursor(TFT_FRAMEWDT, yPos + 4); 
        dsp.print(utf8To(item, true));
    }
}
#else
void PlayListWidget::drawPlaylist(uint16_t currentItem) {
  dsp.setCursor(0, 0);
  dsp.print(F("CH: "));
  dsp.print(currentItem);
  dsp.print(F("        "));
}

void PlayListWidget::_printPLitem(uint8_t pos, const char *item) {
  dsp.setCursor(1, pos);
  dsp.print(F("        "));
}
#endif  // DSP_LCD

#endif // #if DSP_MODEL!=DSP_DUMMY