/*
 * Audio.h
 *
 */

#pragma once
#pragma GCC optimize("Ofast")
#include "audiolib_structs.hpp"
#include "esp_arduino_version.h"
#include "esp_dsp.h"
#include "psram_unique_ptr.hpp"
#include <Arduino.h>
#include <FFat.h>
#include <FS.h>
#include <NetworkClient.h>
#include <NetworkClientSecure.h>
#include <SD.h>
#include <SD_MMC.h>
#include <WiFi.h>
#include <atomic>
#include <charconv>
#include <codecvt>
#include <deque>
#include <driver/i2s_std.h>
#include <esp32-hal-log.h>
#include <functional>
#include <libb64/cencode.h>
#include <locale>
#include <memory>
#include <optional>
#include <span>
#include <vector>

#ifndef I2S_GPIO_UNUSED
    #define I2S_GPIO_UNUSED -1 // = I2S_PIN_NO_CHANGE in IDF < 5
#endif

extern __attribute__((weak)) void audio_process_raw_samples(int32_t* outBuff, int16_t validSamples);            // before volume, gain and equalizer, record audiodata
extern __attribute__((weak)) void audio_process_i2s(int32_t* outBuff, int16_t validSamples, bool* continueI2S); // after volume, gain and equalizer, send via BT
extern char                       audioI2SVers[];
class Decoder; // prototype

//----------------------------------------------------------------------------------------------------------------------
class AudioBuffer {

  public:
    AudioBuffer();   // constructor
    ~AudioBuffer();  // frees the buffer
    size_t   init(); // set default values
    bool     isInitialized() { return m_init; };
    size_t   getBufsize();
    size_t   getMaxBlockSize(); // returns maxBlockSize
    void     setMaxBlocksize(uint32_t mbs);
    size_t   freeSpace();             // number of free bytes to overwrite
    size_t   writeSpace();            // space fom writepointer to bufferend
    size_t   bufferFilled();          // returns the number of filled bytes
    size_t   readSpace();             // max readable bytes in one block
    void     bytesWritten(size_t bw); // update writepointer
    void     bytesWasRead(size_t br); // update readpointer
    uint8_t* getWritePtr();           // returns the current writepointer
    uint8_t* getReadPtr();            // returns the current readpointer
    void     reset();                 // restore defaults
    void     showStatus();

  protected:
    size_t          m_mainBuffSize = 0; // most webstreams limit the advance to 100...300Kbytes
    size_t          m_freeSpace = 0;
    size_t          m_writeSpace = 0;
    size_t          m_resBuffSize = 0;
    size_t          m_maxBlockSize = 0;
    size_t          m_readSpace = 0;
    const size_t    m_maxRet = UINT16_MAX;
    ps_ptr<uint8_t> m_buffer;
    uint8_t*        m_buffEnd = nullptr;
    uint8_t*        m_writePtr = nullptr;
    uint8_t*        m_readPtr = nullptr;
    uint8_t*        m_endPtr = nullptr;
    uint8_t*        m_startPtr = nullptr;
    ps_ptr<char>    m_log;
    bool            m_init = false;
    bool            m_isEmpty = true;
    bool            m_isFull = false;

  private:
    SemaphoreHandle_t m_mutex = nullptr;
#define ANSI_ESC_RED "\033[31m"
};
//----------------------------------------------------------------------------------------------------------------------

class Audio {
  private:
    AudioBuffer InBuff; // instance of input buffer

  public:
    Audio(uint8_t i2sPort = I2S_NUM_0);
    ~Audio();
    std::mutex mutex_info; // mutex_info as member

    // callbacks ---------------------------------------------------------
    typedef enum {
        evt_info = 0,
        evt_id3data,
        evt_eof,
        evt_name,
        evt_icydescription,
        evt_streamtitle,
        evt_bitrate,
        evt_icyurl,
        evt_icylogo,
        evt_genre,
        evt_lasthost,
        evt_image,
        evt_lyrics,
        evt_log,
    } event_t;

    // Audio event type descriptions
    static constexpr std::array<const char*, 14> eventStr = {
        "info",            // evt_info
        "id3data",         // evt_id3data
        "eof",             // evt_eof
        "station_name",    // evt_name
        "icy_description", // evt_icydescription
        "streamtitle",     // evt_streamtitle
        "bitrate (b/s)",   // evt_bitrate
        "icy_url",         // evt_icyurl
        "icy_logo",        // evt_icylogo
        "genre",           // evt_genre
        "lasthost",        // evt_lasthost
        "cover_image",     // evt_image
        "lyrics",          // evt_lyrics
        "log",             // evt_log
    };

    typedef struct _msg { // used in info(audio_info_callback());
        const char*           msg = nullptr;
        const char*           s = nullptr;
        event_t               e = (event_t)0; // event type
        int32_t               i2s_num = 0;
        int32_t               arg1 = 0;
        int32_t               arg2 = 0;
        std::vector<uint32_t> vec = {}; // apic [pos, len, pos, len, pos, len, ....]
    } msg_t;
    inline static std::function<void(msg_t i)> audio_info_callback;
    using VolumeCurveFn = std::function<float(float t)>;
    // -------------------------------------------------------------------
    typedef enum : uint32_t { SR_ORIGIN = 0, SR_44100 = 44100, SR_48000 = 48000 } OutputSR_t;

    bool openai_speech(const String& api_key, const String& model, const String& input, const String& instructions, const String& voice, const String& response_format, const String& speed);
    audiolib::hwoe_t dismantle_host(const char* host);
    bool             connecttohost(const char* host, const char* user = nullptr, const char* pwd = nullptr);
    bool             connecttospeech(const char* speech, const char* lang);
    bool             connecttoFS(fs::FS& fs, const char* path, int32_t fileStartTime = -1);
    void             setConnectionTimeout(uint16_t timeout_ms, uint16_t timeout_ms_ssl);
    bool             setAudioPlayTime(uint16_t sec);
    bool             setTimeOffset(int sec);
    bool             setPinout(uint8_t BCLK, uint8_t LRC, uint8_t DOUT, int8_t MCLK = I2S_GPIO_UNUSED);
    bool             pauseResume();
    bool             isRunning() { return m_f_running; }
    void             loop();
    uint32_t         stopSong();
    void             forceMono(bool m);
    void             setOutputSampleRate(OutputSR_t sr);
    void             setBalance(float balance = 0.0f);
    void             setVolumeSteps(uint8_t steps);
    uint8_t          getVolumeSteps();
    void             setVolume(uint8_t vol, uint8_t curve = 0);
    void             setVolumeCurve(VolumeCurveFn curve);
    uint8_t          getVolume();
    void             setMute(bool mute);
    bool             getMute();
    int32_t          getI2sPort();
    uint32_t         getFileSize();
    uint32_t         getSampleRate();
    uint8_t          getBitsPerSample();
    uint8_t          getChannels();
    uint32_t         getBitRate();
    uint32_t         getAudioFileDuration();
    uint32_t         getAudioCurrentTime();
    uint32_t         getAudioFilePosition();
    bool             setAudioFilePosition(uint32_t pos);
    uint16_t         getVUlevel();
    uint32_t         inBufferFilled();  // returns the number of stored bytes in the inputbuffer
    uint32_t         inBufferFree();    // returns the number of free bytes in the inputbuffer
    uint32_t         getInBufferSize(); // returns the size of the inputbuffer in bytes
    void             inBufferStatus() { InBuff.showStatus(); }
    void             setTone(float gainLowPass, float gainBandPass, float gainHighPass);
    void             setI2SCommFMT_LSB(bool commFMT);
    int              getCodec() { return m_codec; }
    const char*      getCodecname() { return codecname[m_codec]; }
    const char*      getVersion();
    // —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

  private:
    // ------- PRIVATE MEMBERS ----------------------------------------
    bool                     i2s_config();
    std::unique_ptr<Decoder> createDecoder(const std::string& type);
    void                     destroy_decoder();
    bool                     fsRange(uint32_t range);
    void                     latinToUTF8(ps_ptr<char>& buff, bool UTF8check = true);
    void                     htmlToUTF8(char* str);
    void                     setDefaults(); // free buffers and set defaults
    int32_t                  audioFileRead();
    int32_t                  audioFileRead(uint16_t timeout_ms);
    int32_t                  audioFileRead(uint8_t* buff, size_t len);
    int32_t                  audioFileRead(uint8_t* buff, size_t len, uint16_t timeout_ms);
    int32_t                  audioFileSeek(uint32_t position, size_t len = 0);
    void                     initInBuff();
    bool                     httpPrint(const char* host);
    bool                     httpRange(uint32_t range, uint32_t length = UINT32_MAX);
    void                     processLocalFile();
    void                     processWebStream();
    void                     processWebFile();
    void                     processWebStreamTS();
    void                     processWebStreamHLS();
    void                     playAudioData();
    bool                     readPlayListData();
    const char*              parsePlaylist_M3U();
    const char*              parsePlaylist_PLS();
    const char*              parsePlaylist_ASX();
    ps_ptr<char>             parsePlaylist_M3U8();
    uint16_t                 accomplish_m3u8_url();
    int16_t                  prepare_first_m3u8_url(ps_ptr<char>& playlistBuff);
    ps_ptr<char>             m3u8redirection(uint8_t* codec);
    void                     showCodecParams();
    int                      findNextSync(uint8_t* data, size_t len);
    uint32_t                 decodeError(int8_t res, uint8_t* data, int32_t bytesDecoded);
    uint32_t                 decodeContinue(int8_t res, uint8_t* data, int32_t bytesDecoded, int32_t* bytesLeft);
    int                      sendBytes(uint8_t* data, size_t len);
    void                     setDecoderItems();
    void                     calculateAudioTime(uint16_t bytesDecoderIn, uint16_t bytesDecoderOut);
    void                     showID3Tag(const char* tag, const char* val);
    size_t                   readAudioHeader(uint32_t bytes);
    int                      read_WAV_Header(uint8_t* data, size_t len);
    int                      read_FLAC_Header(uint8_t* data, size_t len);
    int                      read_ID3_Header(uint8_t* data, size_t len);
    int                      read_M4A_Header(uint8_t* data, size_t len);
    size_t                   process_m3u8_ID3_Header(uint8_t* packet);
    bool                     setSampleRate(uint32_t hz);
    bool                     setBitsPerSample(int bits);
    bool                     setChannels(int channels);
    uint32_t                 resampleI2Soutput(audiolib::resampler_t& resampler, int32_t* input, uint32_t inputSamples, int32_t* output);
    void                     playChunk();
    void                     calculateVUlevel(int32_t* sample);
    void                     processSpectrum();
    void                     gain_ramp();
    void                     calculateVolumeLimits();
    void                     Gain(int32_t* sample);
    void                     showstreamtitle(char* ml);
    bool                     parseContentType(ps_ptr<char> ct);
    bool                     parseHttpResponseHeader();
    bool                     parseHttpRangeHeader();
    bool                     initializeDecoder();
    esp_err_t                I2Sstart();
    esp_err_t                I2Sstop();
    void                     zeroI2Sbuff();
    void                     reconfigI2S();
    void                     stereo2mono(int32_t* buff, uint16_t validSamples);
    void                     IIR_calculateCoefficients();
    void                     IIR_filter(int32_t* iir_in);
    uint32_t                 streamavail() { return m_client ? m_client->available() : 0; }
    bool                     ts_parsePacket(uint8_t* packet, uint8_t* packetStart, uint8_t* packetLength);
    uint64_t                 getLastGranulePosition(uint8_t codec);

    //+++ create a T A S K  for playAudioData(), output via I2S +++
  public:
    void     setAudioTaskCore(uint8_t coreID);
    uint32_t getHighWatermark();

  private:
    void        startAudioTask(); // starts a task for decode and play
    void        stopAudioTask();  // stops task for audio
    static void audioTaskWrapper(void* param);
    void        audioTask();
    void        performAudioTask();

    //+++ H E L P   F U N C T I O N S +++
    bool                   readMetadata(uint32_t b, uint16_t* readedBytes, bool first = false);
    int32_t                getChunkSize(uint16_t* readedBytes, bool first = false);
    bool                   readID3V1Tag();
    int32_t                newInBuffStart(int32_t resumeFilePos);
    boolean                streamDetection(uint32_t bytesAvail);
    uint32_t               m4a_correctResumeFilePos();
    int32_t                ogg_correctResumeFilePos();
    int32_t                flac_correctResumeFilePos();
    int32_t                mp3_correctResumeFilePos();
    int32_t                wav_correctResumeFilePos();
    uint8_t                determineCodec(uint8_t presumed_codec);
    bool                   get_info();
    void                   trim(char* str);
    bool                   startsWith(const char* base, const char* str);
    int                    indexOf(const char* base, const char* str, int startIndex = 0);
    int                    indexOf(const char* base, char ch, int startIndex = 0);
    int                    specialIndexOf(uint8_t* base, const char* str, int baselen, bool exact = false);
    int32_t                min3(int32_t a, int32_t b, int32_t c);
    uint64_t               bigEndian(uint8_t* base, uint8_t numBytes, uint8_t shiftLeft = 8);
    bool                   b64encode(const char* source, uint16_t sourceLength, char* dest);
    void                   vector_clear_and_shrink(std::vector<ps_ptr<char>>& vec);
    void                   deque_clear_and_shrink(std::deque<ps_ptr<char>>& deq);
    uint32_t               simpleHash(const char* str);
    ps_ptr<char>           urlencode(const char* str, bool spacesOnly);
    audiolib::BiquadCoeffs makeButterworthLPF_Q31(float fs);

  private:
    enum : int { APLL_AUTO = -1, APLL_ENABLE = 1, APLL_DISABLE = 0 };
    enum : int { EXTERNAL_I2S = 0, INTERNAL_DAC = 1, INTERNAL_PDM = 2 };
    enum : int { FORMAT_NONE = 0, FORMAT_M3U = 1, FORMAT_PLS = 2, FORMAT_ASX = 3, FORMAT_M3U8 = 4 }; // playlist formats
    const char* plsFmtStr[5] = {"NONE", "M3U", "PLS", "ASX", "M3U8"};                                // playlist format string
    enum : int { AUDIO_NONE, HTTP_RESPONSE_HEADER, HTTP_RANGE_HEADER, AUDIO_DATA, AUDIO_LOCALFILE, AUDIO_PLAYLISTINIT, AUDIO_PLAYLISTHEADER, AUDIO_PLAYLISTDATA };
    const char* dataModeStr[8] = {"AUDIO_NONE", "HTTP_RESPONSE_HEADER", "HTTP_RANGE_HEADER", "AUDIO_DATA", "AUDIO_LOCALFILE", "AUDIO_PLAYLISTINIT", "AUDIO_PLAYLISTHEADER", "AUDIO_PLAYLISTDATA"};
    enum : int { FLAC_BEGIN = 0, FLAC_MAGIC = 1, FLAC_MBH = 2, FLAC_SINFO = 3, FLAC_PADDING = 4, FLAC_APP = 5, FLAC_SEEK = 6, FLAC_VORBIS = 7, FLAC_CUESHEET = 8, FLAC_PICTURE = 9, FLAC_OKAY = 100 };
    enum : int { MP3_BEGIN = 0, MP3_ID3HEADER, MP3_NEXTID3, MP3_EXTHEADER, MP3_ID3FRAME, MP3_FRAMESIZE, MP3_SKIP, MP3_TAG, MP3_SYLT, MP3_ID3V22, MP3_LASTFRAMES, MP3_XING, MP3_OKAY = 100 };
    enum : int {
        M4A_BEGIN = 0,
        M4A_FTYP = 1,
        M4A_CHK = 2,
        M4A_MOOV = 3,
        M4A_FREE = 4,
        M4A_TRAK = 5,
        M4A_MDAT = 6,
        M4A_ILST = 7,
        M4A_MP4A = 8,
        M4A_ESDS = 9,
        M4A_MDIA = 10,
        M4A_MINF = 11,
        M4A_STBL = 12,
        M4A_STSD = 13,
        M4A_UDTA = 14,
        M4A_STSZ = 15,
        M4A_META = 16,
        M4A_MDHD = 17,
        M4A_CHPL = 18,
        M4A_AMRDY = 99,
        M4A_OKAY = 100,
    };
    enum : int { CODEC_NONE = 0, CODEC_WAV = 1, CODEC_MP3 = 2, CODEC_AAC = 3, CODEC_M4A = 4, CODEC_FLAC = 5, CODEC_OPUS = 6, CODEC_VORBIS = 7, CODEC_OGG = 8 };
    const char* codecname[10] = {"unknown", "WAV", "MP3", "AAC", "M4A", "FLAC", "OPUS", "VORBIS", "OGG"};
    enum : int { ST_NONE = 0, ST_WEBFILE = 1, ST_WEBSTREAM = 2 };
    const char* streamTypeStr[3] = {"NONE", "WEBFILE", "WEBSTREAM"};
    typedef enum { LEFTCHANNEL = 0, RIGHTCHANNEL = 1 } SampleIndex;
    typedef enum { LOWSHELF = 0, PEAKINGEQ = 1, HIFGSHELF = 2 } FilterType;

  private:
    typedef struct _filter {
        float a0;
        float a1;
        float a2;
        float b1;
        float b2;
    } filter_t;

    typedef struct _pis_array {
        int number;
        int pids[4];
    } pid_array;

  public:
    struct audioSettings {
        uint16_t DMA_DESC_NUM = 32;                // number of I2S DMA buffer
        uint16_t DMA_FRAME_NUM = 256;              // number of frames in one DMA buffer
        uint16_t FREQ_LS_HZ = 500;                 // IIR Filter, lowshelf
        uint16_t FREQ_PEAK_HZ = 1800;              // IIR Filter, peakingEQ
        uint16_t FREQ_HS_HZ = 6000;                // IIR Filter, highshelf
        float    QUALITY_SLOPE = 0.707;            // Quality (all shelfes)
        uint16_t PEAK_HOLD_SAMPLES = 2000;         // VU_meter, (2000) ca. 20 ms @ 48 kHz
        uint8_t  PEAK_RELEASE = 1;                 // VU_meter, Fall rate
        bool     VU_LEVEL = true;                  // true: vu meter is enabled
        bool     IIR_FILTER = true;                // true: IIR filter (highshelf, bandpass, lowshelf) are enabled
        bool     SPECTRUM = false;                 // true: spectrum analyzer is enabled
        bool     VOLUME_CONTROL = true;            // true: volume and balance control is enabled
        float    VOL_FADING_SPEED = 50.0;          // mute, volume fading 1.0f (fast) ... 100.0f (slow)
        uint32_t BUFFER_TRESHOLD_HLS = UINT16_MAX; // Level at which the HLS-TS stream starts and is reloaded
    } settings;

  private:
    File                m_audiofile;
    NetworkClient       client;
    NetworkClientSecure clientsecure;
    NetworkClient*      m_client = nullptr;

    SemaphoreHandle_t mutex_playAudioData;
    SemaphoreHandle_t mutex_audioTask;
    SemaphoreHandle_t mutex_audioTaskIsDecoding;
    TaskHandle_t      m_audioTaskHandle = nullptr;

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"

    i2s_chan_handle_t m_i2s_tx_handle = {};
    i2s_chan_config_t m_i2s_chan_cfg = {}; // stores I2S channel values
    i2s_std_config_t  m_i2s_std_cfg = {};  // stores I2S driver values

#pragma GCC diagnostic pop

    std::vector<ps_ptr<char>> m_playlistContent; // m3u8 playlist buffer from responseHeader
    std::vector<ps_ptr<char>> m_playlistURL;     // m3u8 streamURLs buffer
    std::deque<ps_ptr<char>>  m_linesWithURL;    // extract from m_playlistContent, contains URL and MediaSequenceNumber
    std::vector<ps_ptr<char>> m_linesWithEXTINF; // extract from m_playlistContent, contains length and metadata
    std::vector<ps_ptr<char>> m_syltLines;       // SYLT line table
    std::vector<uint32_t>     m_syltTimeStamp;   // SYLT time table

    static const uint8_t m_tsPacketSize = 188;
    static const uint8_t m_tsHeaderSize = 4;

    std::unique_ptr<Decoder> m_decoder = {};
    ps_ptr<int32_t>          m_outBuff;         // Interleaved L/R
    ps_ptr<int32_t>          m_resamplesBuff;   // Interleaved L/R
    ps_ptr<char>             m_metadataBuff;    // icy-metadata max (16 * 256 + 1) bytes
    ps_ptr<char>             m_httpRespHdrBuff; // store http response header
    ps_ptr<char>             m_ibuff;           // used in log_info()
    ps_ptr<char>             m_lastHost;        // Store the last URL to a webstream
    ps_ptr<char>             m_currentHost;     // can be changed by redirection or playlist
    ps_ptr<char>             m_m3u8_host;
    ps_ptr<char>             m_speechtxt;   // stores tts text
    ps_ptr<char>             m_streamTitle; // stores the last StreamTitle
    ps_ptr<char>             m_streamURL;   // stores the last StreamURL
    ps_ptr<char>             m_playlistBuff;
    VolumeCurveFn            m_volumeCurve = nullptr;

    const uint16_t m_plsBuffEntryLen = 256;         // length of each entry in playlistBuff
    int            m_LFcount = 0;                   // Detection of end of header
    uint32_t       m_avr_bitrate = 0;               // average bitrate, median calculated by VBR
    uint32_t       m_nominal_bitrate = 0;           // given br from header
    uint32_t       m_audioFilePosition = 0;         // current position, counts every readed byte
    uint32_t       m_audioDataReadPtr = 0;          // used in playAudioData
    uint32_t       m_audioFileSize = 0;             // local and web files
    int            m_readbytes = 0;                 // bytes read
    uint32_t       m_metacount = 0;                 // counts down bytes between metadata
    int            m_controlCounter = 0;            // Status within readID3data() and readWaveHeader()
    uint8_t        m_timeoutCounter = 0;            // timeout counter
    uint8_t        m_bitsPerSample = 16;            // bitsPerSample
    uint8_t        m_channels = 2;                  //
    uint8_t        m_playlistFormat = 0;            // M3U, PLS, ASX
    uint8_t        m_codec = CODEC_NONE;            //
    uint8_t        m_m3u8Codec = CODEC_AAC;         // codec of m3u8 stream
    uint8_t        m_expectedCodec = CODEC_NONE;    // set in connecttohost (e.g. http://url.mp3 -> CODEC_MP3)
    uint8_t        m_expectedPlsFmt = FORMAT_NONE;  // set in connecttohost (e.g. streaming01.m3u) -> FORMAT_M3U)
    uint8_t        m_streamType = ST_NONE;          //
    uint8_t        m_ID3Size = 0;                   // lengt of ID3frame - ID3header
    uint8_t        m_audioTaskCoreId = 0;           //
    uint8_t        m_M4A_objectType = 0;            // set in read_M4A_Header
    uint8_t        m_M4A_chConfig = 0;              // set in read_M4A_Header
    uint16_t       m_M4A_sampleRate = 0;            // set in read_M4A_Header
    int16_t        m_validSamples = 0;              //
    int16_t        m_curSample = 0;                 //
    uint16_t       m_dataMode = 0;                  // Statemaschine
    uint16_t       m_streamTitleHash = 0;           // remember streamtitle, ignore multiple occurence in metadata
    uint16_t       m_timeout_ms = 250;              //
    uint16_t       m_timeout_ms_ssl = 2700;         //
    uint32_t       m_metaint = 0;                   // Number of databytes between metadata
    uint32_t       m_chunkcount = 0;                // Counter for chunked transfer
    uint32_t       m_t0 = 0;                        // store millis(), is needed for a small delay
    uint32_t       m_bytesNotConsumed = 0;          // pictures or something else that comes with the stream
    uint64_t       m_lastGranulePosition = 0;       // necessary to calculate the duration in OPUS and VORBIS
    int32_t        m_resumeFilePos = -1;            // the return value from stopSong(), (-1) is idle
    int32_t        m_fileStartTime = -1;            // may be set in connecttoFS()
    uint16_t       m_m3u8_targetDuration = 10;      //
    uint32_t       m_stsz_numEntries = 0;           // num of entries inside stsz atom (uint32_t)
    uint32_t       m_stsz_position = 0;             // pos of stsz atom within file
    uint32_t       m_haveNewFilePos = 0;            // user changed the file position
    bool           m_f_alt_user_agent = false;      // use default or alternative user agent
    bool           m_f_I2S_init = false;            //
    bool           m_f_unsync = false;              // set within ID3 tag but not used
    bool           m_f_exthdr = false;              // ID3 extended header
    bool           m_f_ssl = false;                 //
    bool           m_f_running = false;             //
    bool           m_f_firstCall = false;           // InitSequence for processWebstream and processLokalFile
    bool           m_f_firstLoop = false;           // InitSequence in loop()
    bool           m_f_firstPlayCall = false;       // InitSequence for playAudioData
    bool           m_f_ID3v1TagFound = false;       // ID3v1 tag found
    bool           m_f_chunked = false;             // Station provides chunked transfer
    bool           m_f_firstmetabyte = false;       // True if first metabyte (counter)
    bool           m_f_playing = false;             // valid mp3 stream recognized
    bool           m_f_tts = false;                 // text to speech
    bool           m_f_ogg = false;                 // OGG stream
    bool           m_f_forceMono = false;           // if true stereo -> mono
    bool           m_f_rtsp = false;                // set if RTSP is used (m3u8 stream)
    bool           m_f_m3u8data = false;            // used in processM3U8entries
    bool           m_f_continue = false;            // next m3u8 chunk is available
    bool           m_f_ts = true;                   // transport stream
    bool           m_f_m4aID3dataAreRead = false;   // has the m4a-ID3data already been read?
    bool           m_f_psramFound = false;          // set in constructor, result of psramInit()
    bool           m_f_timeout = false;             //
    bool           m_f_audioTaskIsRunning = false;  //
    bool           m_f_allDataReceived = false;     //
    bool           m_f_stream = false;              // stream ready for output?
    bool           m_f_decode_ready = false;        // if true data for decode are ready
    bool           m_f_eof = false;                 // end of file
    bool           m_f_lockInBuffer = false;        // lock inBuffer for manipulation
    bool           m_f_audioTaskIsDecoding = false; //
    bool           m_f_acceptRanges = false;        //
    bool           m_f_reset_m3u8Codec = true;      // reset codec for m3u8 stream
    bool           m_f_connectionClose = false;     // set in parseHttpResponseHeader
    bool           m_f_i2s_channel_enabled = false; // true if enabled
    uint32_t       m_audioFileDuration = 0;         // seconds
    uint32_t       m_audioCurrentTime = 0;          // seconds
    uint32_t       m_audioDataStart = 0;            // in bytes
    OutputSR_t     m_output_sr = SR_ORIGIN;         // output samplerate
    size_t         m_audioDataSize = 0;             //
    size_t         m_ibuffSize = 0;                 // log buffer size for audio_info()
    size_t         m_i2s_bytesWritten = 0;          // set in i2s_write() but not used

    pid_array m_pidsOfPMT;
    int16_t   m_pidOfAAC;
    uint8_t   m_packetBuff[m_tsPacketSize];
    int16_t   m_pesDataLength = 0;

    // audiolib structs
    audiolib::ID3Hdr_t     m_ID3Hdr;
    audiolib::pwsHLS_t     m_pwsHLS;
    audiolib::pplM3u8_t    m_pplM3U8;
    audiolib::m4aHdr_t     m_m4aHdr;
    audiolib::plCh_t       m_plCh;
    audiolib::lVar_t       m_lVar;
    audiolib::prlf_t       m_prlf;
    audiolib::cat_t        m_cat;
    audiolib::ifCh_t       m_ifCh;
    audiolib::tspp_t       m_tspp;
    audiolib::pwst_t       m_pwst;
    audiolib::gchs_t       m_gchs;
    audiolib::pwf_t        m_pwf;
    audiolib::pad_t        m_pad;
    audiolib::sbyt_t       m_sbyt;
    audiolib::rmet_t       m_rmet;
    audiolib::pwsts_t      m_pwsst;
    audiolib::rwh_t        m_rwh;
    audiolib::rflh_t       m_rflh;
    audiolib::phreh_t      m_phreh;
    audiolib::phrah_t      m_phrah;
    audiolib::sdet_t       m_sdet;
    audiolib::fnsy_t       m_fnsy;
    audiolib::audioItems_t m_audio_items;
    audiolib::vu_items_t   m_vu_items;
    audiolib::fft_items_t  m_fft_items;
    audiolib::i2s_items_t  m_i2s_items;
    audiolib::resampler_t  m_resampler;
    audiolib::info_queue_t m_info_queue;
    audiolib::icy_items_t  m_icy_items;

    // —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
  public:
    template <typename... Args> static bool info(Audio& instance, event_t e, const char* fmt, Args&&... args) {
        std::lock_guard<std::mutex> lock(instance.mutex_info);
        if (!fmt) return false;
        if (!audio_info_callback) return false;

        ps_ptr<char> result;
        result.assignf(fmt, std::forward<Args>(args)...);
        if (!result.get()) return false;

        auto extract_last_number = [](std::string_view s) -> std::optional<int32_t> {
            auto is_space = [](char c) { return std::isspace(static_cast<unsigned char>(c)); };
            auto is_digit = [](char c) { return std::isdigit(static_cast<unsigned char>(c)); };

            auto it = s.end();
            // skip trailing whitespace
            while (it != s.begin() && is_space(*(it - 1))) { --it; }
            auto end = it;
            // Reading numbers backwards
            while (it != s.begin() && is_digit(*(it - 1))) { --it; }
            // optional sign
            if (it != s.begin()) {
                char c = *(it - 1);
                if (c == '+' || c == '-') { --it; }
            }

            // found nothing?
            if (it == end) { return std::nullopt; }
            // There must be a leading space or a space before the number
            if (it != s.begin() && !is_space(*(it - 1))) { return std::nullopt; }
            int32_t value{};
            auto [ptr, ec] = std::from_chars(it, end, value);

            // Was the full parse successful?
            if (ec == std::errc{} && ptr == end) { return value; }
            return std::nullopt;
        };

        std::vector<uint32_t> v;
        v.push_back(0);
        instance.m_info_queue.msg.emplace_front(result);
        instance.m_info_queue.s.emplace_front(eventStr[e]);
        instance.m_info_queue.arg1.emplace_front(extract_last_number(result.c_get()).value_or(0));
        instance.m_info_queue.arg2.emplace_front(0);
        instance.m_info_queue.vec.emplace_front(v);
        instance.m_info_queue.e.emplace_front((uint8_t)e);
        result.reset();
        return true;
    }

    static bool info(Audio& instance, event_t e, std::vector<uint32_t>& v) {
        if (!audio_info_callback) return false;
        std::lock_guard<std::mutex> lock(instance.mutex_info); // lock mutex
        ps_ptr<char>                apic;
        apic.assignf("APIC found at pos {}", v[0]);
        // msg_t i;
        // i.msg = apic.c_get();
        // i.e = e;
        // i.s = eventStr[e];
        // i.i2s_num = instance.m_i2s_items.i2s_num;
        // i.vec = v;
        // audio_info_callback(i);

        instance.m_info_queue.msg.emplace_front(apic);
        instance.m_info_queue.s.emplace_front(eventStr[e]);
        instance.m_info_queue.arg1.emplace_front(0);
        instance.m_info_queue.arg2.emplace_front(0);
        instance.m_info_queue.vec.emplace_front(v);
        instance.m_info_queue.e.emplace_front((uint8_t)e);
        return true;
    }
    //----------------------------------------------------------------------------------------------------------------------

    template <typename... Args> static void AUDIO_LOG_IMPL(uint8_t level, const char* path, int line, const char* func, const char* fmt, Args&&... args) {

#define ANSI_ESC_RESET   "\033[0m"
#define ANSI_ESC_BLACK   "\033[30m"
#define ANSI_ESC_RED     "\033[31m"
#define ANSI_ESC_GREEN   "\033[32m"
#define ANSI_ESC_YELLOW  "\033[33m"
#define ANSI_ESC_BLUE    "\033[34m"
#define ANSI_ESC_MAGENTA "\033[35m"
#define ANSI_ESC_CYAN    "\033[36m"
#define ANSI_ESC_WHITE   "\033[37m"

        ps_ptr<char> logStr = path;
        while (logStr.contains("/")) { logStr.remove_before('/', false); }
        logStr.appendf(":{} {}] ", line, func ? func : "");
        logStr.insert("[", 0);

        if (level == 1 && CORE_DEBUG_LEVEL >= 1) {
            logStr.append(ANSI_ESC_RED);
        } else if (level == 2 && CORE_DEBUG_LEVEL >= 2) {
            logStr.append(ANSI_ESC_YELLOW);
        } else if (level == 3 && CORE_DEBUG_LEVEL >= 3) {
            logStr.append(ANSI_ESC_GREEN);
        } else if (level == 4 && CORE_DEBUG_LEVEL >= 4) {
            logStr.append(ANSI_ESC_CYAN);
        } // debug
        else if (level == 5 && CORE_DEBUG_LEVEL >= 4) {
            logStr.append(ANSI_ESC_WHITE);
        } // verbose
        else
            return;

        int add_len = std::snprintf(nullptr, 0, fmt, std::forward<Args>(args)...);
        if (add_len > 0) {
            logStr.appendf(fmt, std::forward<Args>(args)...); // <-- neue appendf()
        }
        logStr.append(ANSI_ESC_RESET);

        msg_t msg;
        msg.msg = logStr.get();
        const char* tag[7] = {"", "LOGE", "LOGW", "LOGI", "LOGD", "LOGV", ""};
        msg.s = tag[level];
        msg.e = evt_log;

        if (audio_info_callback)
            audio_info_callback(msg);
        else {
            if (level == 1)
                log_e("%s", logStr.c_get());
            else if (level == 2)
                log_w("%s", logStr.c_get());
            else if (level == 3)
                log_i("%s", logStr.c_get());
            else if (level == 4)
                log_d("%s", logStr.c_get());
            else
                log_v("%s", logStr.c_get());
        }
        logStr.reset();
    }

// Macro for comfortable calls
#define AUDIO_LOG_ERROR(fmt, ...) AUDIO_LOG_IMPL(1, __FILE__, __LINE__, __func__, fmt, ##__VA_ARGS__)
#define AUDIO_LOG_WARN(fmt, ...)  AUDIO_LOG_IMPL(2, __FILE__, __LINE__, __func__, fmt, ##__VA_ARGS__)
#define AUDIO_LOG_INFO(fmt, ...)  AUDIO_LOG_IMPL(3, __FILE__, __LINE__, __func__, fmt, ##__VA_ARGS__)
#define AUDIO_LOG_DEBUG(fmt, ...) AUDIO_LOG_IMPL(4, __FILE__, __LINE__, __func__, fmt, ##__VA_ARGS__)
};
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
// 📌📌📌  D E C O D E R  📌📌📌
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
class Decoder {
  public:
    virtual ~Decoder() = default;
    virtual bool                  init() = 0;
    virtual void                  clear() = 0;
    virtual void                  reset() = 0;
    virtual bool                  isValid() = 0;
    virtual int32_t               findSyncWord(uint8_t* buf, int32_t nBytes) = 0;
    virtual uint8_t               getChannels() = 0;
    virtual uint32_t              getSampleRate() = 0;
    virtual uint8_t               getBitsPerSample() = 0;
    virtual uint32_t              getBitRate() = 0;
    virtual uint32_t              getAudioDataStart() = 0;
    virtual uint32_t              getAudioFileDuration() = 0;
    virtual uint32_t              getOutputSamples() = 0;
    virtual int32_t               decode(uint8_t* inbuf, int32_t* bytesLeft, int32_t* outbuf1) = 0;
    virtual void                  setRawBlockParams(uint8_t param1, uint32_t param2, uint8_t param3, uint32_t param4, uint32_t param5) = 0;
    virtual const char*           getStreamTitle();
    virtual const char*           whoIsIt();
    virtual std::vector<uint32_t> getMetadataBlockPicture() = 0;
    virtual const char*           arg1() = 0; // decoder specific
    virtual const char*           arg2() = 0; // decoder specific
    virtual int32_t               val1() = 0; // decoder specific
    virtual int32_t               val2() = 0; // decoder specific

  protected:
    Decoder(Audio& audioRef) : audio(audioRef) {}
    Audio& audio; // protected reference, usable by all subclasses
  private:
    Decoder() = delete; // Deactivate default constructor explicitly (optional but good against abuse)
};
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
// 📌📌📌  A U T O L O G G E R     for detecting memory leaks  📌📌📌
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
/* usage
    void myFunction(){
        HEAP_GUARD();  // <--- automatic check
        my code ...
        my code ...
    }

    { // Or in small critical code blocks:
        HEAP_GUARD();
        fill_content(inbuf, to_read);
    }
*/

struct _HeapGuardSnapshot {
    size_t      free_dram_before{};
    size_t      free_psram_before{};
    bool        integrity_before{};
    const char* func{};
    bool        active{false};

    _HeapGuardSnapshot(const char* f) : func(f), active(true) {
        free_dram_before = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
        free_psram_before = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
        integrity_before = heap_caps_check_integrity_all(true);
        if (!integrity_before) {
            printf(ANSI_ESC_RED "HEAPGUARD [%s] ❌ Heap corruption detected BEFORE!" ANSI_ESC_RESET "\n", func);
        } else {
            printf(ANSI_ESC_GREEN "HEAPGUARD [%s] Begin: DRAM=%u, PSRAM=%u" ANSI_ESC_RESET "\n", func, (unsigned)free_dram_before, (unsigned)free_psram_before);
        }
    }

    ~_HeapGuardSnapshot() {
        if (!active) return; // falls moved / deaktiviert
        size_t free_dram_after = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
        size_t free_psram_after = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
        bool   ok = heap_caps_check_integrity_all(true);

        int delta_dram = (int)(free_dram_after - free_dram_before);
        int delta_psram = (int)(free_psram_after - free_psram_before);

        if (!ok) {
            printf(ANSI_ESC_RED "HEAPGUARD [%s] ❌ Heap corruption detected AFTER!" ANSI_ESC_RESET "\n", func);
        } else {
            printf(ANSI_ESC_GREEN "HEAPGUARD [%s] ✅ Heap OK | ΔDRAM=%+d | ΔPSRAM=%+d" ANSI_ESC_RESET "\n", func, delta_dram, delta_psram);
        }
    }
};
#define HEAP_GUARD() _HeapGuardSnapshot _heapguard_instance_##__LINE__(__func__)
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
// 📌📌📌  A U T O P R O F I L E R    RAII-class for timekeeping  📌📌📌
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
/* usage
    void decodeNative(uint8_t* inbuf, int bytesLeft, uint8_t* outbuf) {
        PROFILE_SCOPE_N(1000);  // automatically measures 1000 views on average

        // ... my code ...
    }

    {   // Or in small critical code blocks:
        PROFILE_SCOPE_N(100);  // measures this block over 100 runs
        do_fft_processing(data);
    }
*/
class _AutoProfiler {
  public:
    _AutoProfiler(const char* name, uint32_t report_interval) : tag(name), N(report_interval) { start = esp_timer_get_time(); }

    ~_AutoProfiler() {
        uint64_t elapsed = esp_timer_get_time() - start;
        sum += elapsed;
        count++;
        if (max_dt < elapsed) max_dt = elapsed;

        if (count >= N) {
            double avg_us = (double)sum / count;
            printf(ANSI_ESC_CYAN "PROFILER [%s] avg: %.2f µs over %lu runs, max %lu µs" ANSI_ESC_RESET "\n", tag, avg_us, count, max_dt);
            sum = 0;
            count = 0;
            max_dt = 0;
        }
    }

  private:
    const char*            tag;
    uint32_t               N;
    uint64_t               start;
    static inline uint32_t max_dt = 0;
    static inline uint64_t sum = 0;
    static inline uint32_t count = 0;
};

// Macro for automatic use with function name
#define PROFILE_SCOPE_N(N) _AutoProfiler _prof_instance_##__LINE__(__func__, N)
// —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
