#pragma once
#include <functional>

#include "AudioTools/CoreAudio/AudioBasic/Str.h"
#include "AudioTools/CoreAudio/AudioStreams.h"
#include "AudioTools/CoreAudio/MusicalNotes.h"

namespace audio_tools {

/**
 * @brief RTTTL (Ring Tone Text Transfer Language) parser and player stream.
 *
 * RTTTLOutput<T> parses RTTTL text provided via its `write()` method and
 * generates audio by driving a provided SoundGenerator<T>. It writes the
 * generated PCM bytes to a `Print`-compatible output (typically an
 * AudioStream or AudioOutput that implements `Print`).
 *
 * Template parameter T
 * - T: sample type used by the SoundGenerator (e.g. `int16_t`).
 *
 * Usage:
 * - Construct with a SoundGenerator<T>&. Optionally call a `setOutput(...)`
 *   overload to attach an `AudioStream`, `AudioOutput` or raw `Print` target.
 * - Call `begin()` to reset internal parse state (typically called by the
 *   surrounding audio pipeline when starting a stream).
 * - Write an RTTTL string by calling `write(const uint8_t* data, size_t len)`.
 *   RTTTLOutput will buffer the incoming bytes and parse the title, defaults
 *   and notes. Notes are rendered immediately via `play_note()`.
 *
 * RTTTL semantics implemented:
 * - Default values parsed from the RTTTL header: duration (d=), octave (o=)
 *   and tempo in BPM (b=). Default BPM is 120 if not provided.
 * - Tempo (bpm) is converted to milliseconds-per-whole-note using
 *   `ms_per_whole = 240000.0 / bpm` (i.e. 60s * 4 * 1000 ms / bpm). The
 *   duration field in RTTTL (1,2,4,8,16,...) divides the whole-note length.
 * - Dotted durations are supported (multiplied by 1.5). Rests ('p' or 'r')
 *   are supported and produce silence by setting frequency to 0.0.
 *
 * Important implementation notes / expectations:
 * - The attached SoundGenerator<T> must implement at least:
 *     - setFrequency(float hz)
 *     - readBytes(void* buffer, size_t byteCount)
 *   These are used by `play_note()` to generate PCM frames.
 * - The output target must be a `Print` (or derived) pointer; attaching an
 *   `AudioStream` or `AudioOutput` via `setOutput` will register for audio
 *   format notifications and forward generated bytes to that target.
 * - If `b=0` is parsed, the implementation falls back to a fixed
 *   `m_msec_semi = 750.0` (legacy/fallback behavior). Consider validating
 *   or rejecting `b=0` RTTTL if that is undesired.
 *
 * Edge cases & behavior:
 * - Partial RTTTL inputs: `write()` appends to an internal ring buffer and
 *   parsing continues as bytes arrive. The parser expects the standard
 *   RTTTL format: "name:defaults:notes" and will parse title and defaults on
 *   first `write()` after `begin()`.
 * - Unknown note letters are skipped.
 * - Accidentals (#) that bump past B roll the octave up by one.
 *
 * Example RTTTL string:
 *   "Entertainer:d=4,o=5,b=120:8d6,8d6,8d6"
 *
 * Public API (selected):
 * - RTTTLOutput(SoundGenerator<T>& p_generator)
 * - void setOutput(Print& out)
 * - void setOutput(AudioStream& out)
 * - void setOutput(AudioOutput& out)
 * - bool begin()
 * - size_t write(const uint8_t* data, size_t len) override
 * - void setNoteCallback(std::function<void(float,int,int)>)
 *
 * Note callback details:
 * - Use `setNoteCallback(cb)` to register a callback that will be invoked
 *   for every parsed note or rest. The callback signature is
 *   `void(float frequencyHz, int durationMs, int midiNote)`.
 * - frequencyHz: frequency in Hz (0.0 for rests)
 * - durationMs: duration in milliseconds computed from bpm and duration
 * - midiNote: MIDI note number (0-127) for notes, or -1 for rests/unknown
 * - The callback is invoked from `play_note()` before audio generation
 *   begins. If you require the callback after playback, you should wrap
 *   `setNoteCallback` and buffer events or change ordering.
 * - MIDI mapping: if the library's `MusicalNotes` helper doesn't expose a
 *   `toMidi(...)` helper, the midiNote may be computed using the standard
 *   formula: `midi = 12*(octave + 1) + semitoneIndex` where semitoneIndex is
 *   the note's index within the 12 semitones (C=0, C#=1, ..., B=11).
 *
 * Known limitations:
 * - No explicit error reporting for malformed RTTTL. Parser silently skips
 *   invalid tokens.
 * - The fallback for bpm==0 may be surprising; consider using the default
 *   bpm instead.
 * @ingroup dsp
 * @ingroup transform
 * @author Phil Schatzmann
 * @copyright GPLv3
 */
template <class T = int16_t>
class RTTTLOutput : public AudioOutput {
 public:
  RTTTLOutput() = default;
  RTTTLOutput(SoundGenerator<T>& generator) { setGenerator(generator); }
  RTTTLOutput(SoundGenerator<T>& generator, AudioStream& out)
      : RTTTLOutput(generator) {
    setOutput(out);
  }
  RTTTLOutput(SoundGenerator<T>& generator, AudioOutput& out)
      : RTTTLOutput(generator) {
    setOutput(out);
  }
  RTTTLOutput(SoundGenerator<T>& generator, Print& out)
      : RTTTLOutput(generator) {
    setOutput(out);
  }

  // Expose base-class begin overloads (e.g. begin(AudioInfo)) to avoid
  // hiding them with the no-arg begin() override below.
  using AudioOutput::begin;

  /// Defines/Changes the output target (Print)
  void setOutput(Print& out) { p_print = &out; }

  /// Defines the Generator that is used to
  /// generate the sound
  void setGenerator(SoundGenerator<T>& generator) { p_generator = &generator; }

  /// Defines/Changes the output target
  /// (AudioStream) and register for audio info
  void setOutput(AudioStream& out) {
    setOutput((Print&)out);
    addNotifyAudioChange(out);
  }

  /// Defines/Changes the output target
  /// (AudioOutput) and register for audio info
  void setOutput(AudioOutput& out) {
    setOutput((Print&)out);
    addNotifyAudioChange(out);
  }

  /**
   * @brief Start the RTTTL stream: we start with
   * parsing the title and defaults
   * @return True if initialization was
   * successful.
   */
  bool begin() override {
    LOGD("RTTTLOutput::begin");
    if (p_generator) {
      p_generator->begin(audioInfo());
    }
    is_start = true;
    return true;
  }

  /// Returns the parsed title of the RTTTL
  /// ringtone
  const char* getTitle() { return m_title.c_str(); }

  /// Returns the default octave parsed from the
  /// RTTTL string
  int getDefaultOctave() const { return m_octave; }

  /// Returns the default duration parsed from the
  /// RTTTL string
  int getDefaultDuration() const { return m_duration; }

  /// Returns the default bpm parsed from the
  /// RTTTL string
  int getDefaultBpm() const { return m_bpm; }

  /// Writes RTTTL data to the parser and plays the notes
  size_t write(const uint8_t* data, size_t len) override {
    LOGD("write: %d", len);
    ring_buffer.resize(len);
    ring_buffer.writeArray(const_cast<uint8_t*>(data), len);
    // If we haven't started yet and we find a ':', we need to call begin()
    if (!is_start && find_byte(data, len, ':') >= 0) {
      LOGI("found ':' - resetting parser state by calling begin()");
      begin();
    }
    // start parsing of new rtttl string
    if (is_start) {
      // parse rtttl string
      parse_title();
      parse_defaults();
      is_start = false;
    }
    parse_notes();
    return len;
  }

  /// Set an optional callback to be invoked for
  /// each parsed note/rest. Callback signature:
  /// void(float frequencyHz, int durationMs, int
  /// midiNote)
  /// - frequencyHz: 0.0 for rests
  /// - durationMs: computed duration in
  /// milliseconds
  /// - midiNote: MIDI note number (0-127)
  void setNoteCallback(
      std::function<void(float freqHz, int durationMs, int midiNote, void* ref)>
          cb) {
    noteCallback = cb;
  }

  /// Provide reference for callback
  void setReference(void* ref) { reference = ref; }

  /// transpose all notes by the specified number of octaves e.g. -2 = 2 octaves
  /// down
  void setTransposeOctaves(int8_t octaves) { m_tranpose_octaves = octaves; }

 protected:
  MusicalNotes m_notes;
  SoundGenerator<T>* p_generator = nullptr;
  RingBuffer<uint8_t> ring_buffer{0};
  Print* p_print = nullptr;
  int8_t m_tranpose_octaves = 0;
  bool is_start = true;
  char m_actual = 0;
  char m_prec = 0;
  Str m_title{40};
  int m_octave = 4;
  int m_duration = 4;
  int m_bpm = 120;
  float m_msec_semi = 750;
  void* reference = nullptr;
  std::function<void(float, int, int, void*)> noteCallback;

  int find_byte(const uint8_t* haystack, size_t haystack_len, uint8_t needle) {
    for (size_t i = 0; i < haystack_len; i++) {
      if (haystack[i] == needle) {
        return i;  // Return the index of the first match
      }
    }
    return -1;  // Return -1 if the byte is not found
  }

  void play_note(float freq, int msec, int midi = -1) {
    // invoke the optional callback first
    LOGI("play_note: freq=%.2f Hz, msec=%d, midi=%d", freq, msec, midi);
    if (noteCallback) noteCallback(freq, msec, midi, reference);
    if (p_print == nullptr || p_generator == nullptr) return;
    p_generator->setFrequency(freq);
    AudioInfo info = audioInfo();
    if (!info) return;
    int frames = (int)((uint64_t)info.sample_rate * (uint64_t)msec / 1000);
    int frame_size = (info.channels * info.bits_per_sample) / 8;
    int open = frames * frame_size;
    uint8_t buffer[1024];
    while (open > 0) {
      int toCopy = std::min(open, (int)sizeof(buffer));
      p_generator->readBytes(buffer, toCopy);
      p_print->write(buffer, toCopy);
      open -= toCopy;
      delay(1);
    }
  }

  char next_char(bool convertToLower = true) {
    uint8_t c;
    if (!ring_buffer.read(c)) {
      c = 0;
    }
    m_prec = m_actual;
    m_actual = convertToLower ? tolower(c) : c;
    return m_actual;
  }

  void parse_title() {
    next_char(false);
    m_title = "";
    for (; m_actual != ':' && m_actual != '\0'; next_char(false)) {
      m_title += m_actual;
    }
    if (!m_title.isEmpty()) LOGI("title: %s", m_title.c_str());
  }

  int parse_num() {
    int val = 0;
    do {
      val *= 10;
      val += m_actual - '0';
    } while (isdigit(next_char()));

    return val;
  }

  void parse_defaults() {
    char id{' '};

    while (' ' == next_char());
    do {
      if (isdigit(m_actual)) {
        switch (id) {
          case 'o':
            m_octave = parse_num();
            LOGI("default octave: %d", m_octave);
            break;
          case 'd':
            m_duration = parse_num();
            LOGI("default duration: %d", m_duration);
            break;
          case 'b':
            m_bpm = parse_num();
            LOGI("default bpm: %d", m_bpm);
            break;
        }
        continue;
      } else if (isalpha(m_actual)) {
        id = m_actual;
      }
      next_char();
    } while (':' != m_actual);

    if (m_bpm != 0)
      m_msec_semi = 240000.0 / m_bpm;
    else
      m_msec_semi = 750.0;

    LOGI("msec per semi: %.2f", m_msec_semi);
  }

  float transpose(float frequency, int8_t octaves) {
    if (octaves == 0) return frequency;
    return frequency * powf(2.0f, octaves);
  }

  void parse_notes() {
    // Ensure we start reading after the defaults
    // section
    if (m_actual == ':') next_char();

    while (m_actual != 0) {
      // skip separators
      while (m_actual == ' ' || m_actual == ',') {
        if (next_char() == 0) return;
      }

      if (m_actual == 0) break;

      // optional duration (number)
      int duration = m_duration;
      if (isdigit(m_actual)) {
        duration = parse_num();
        if (m_actual == 0) break;
      }

      // note letter or rest
      char name = m_actual;
      if (name == 'p' || name == 'r') {  // pause/rest
        next_char();
        double mult = 1.0;
        if (m_actual == '.') {
          mult = 1.5;
          next_char();
        }
        int msec = (int)((m_msec_semi / duration) * mult);
        play_note(0.0f, msec, -1);
        continue;
      }

      // map letter to MusicalNotes enum
      MusicalNotes::MusicalNotesEnum noteEnum = MusicalNotes::C;
      switch (name) {
        case 'c':
          noteEnum = MusicalNotes::C;
          break;
        case 'd':
          noteEnum = MusicalNotes::D;
          break;
        case 'e':
          noteEnum = MusicalNotes::E;
          break;
        case 'f':
          noteEnum = MusicalNotes::F;
          break;
        case 'g':
          noteEnum = MusicalNotes::G;
          break;
        case 'a':
          noteEnum = MusicalNotes::A;
          break;
        case 'b':
        case 'h':
          noteEnum = MusicalNotes::B;
          break;
        default:
          // unknown symbol -> skip
          next_char();
          continue;
      }

      // advance to next char to inspect
      // accidental/octave/dot
      if (next_char() == 0) break;

      // accidental
      int semitone = (int)noteEnum;
      if (m_actual == '#') {
        semitone++;
        if (semitone > 11) {
          semitone = 0;
          m_octave += 1;
        }
        noteEnum = (MusicalNotes::MusicalNotesEnum)semitone;
        if (next_char() == 0) break;
      }

      // octave
      int octave = m_octave;
      if (m_actual >= '0' && m_actual <= '9') {
        octave = m_actual - '0';
        if (next_char() == 0) {
          // compute and play
          float freq = m_notes.frequency(noteEnum, (uint8_t)octave);
          freq = transpose(freq, m_tranpose_octaves);
          int msec = (int)((m_msec_semi / duration) * 1.0);
          int midi = m_notes.frequencyToMidiNote(freq);
          play_note(freq, msec, midi);
          break;
        }
      }

      // dotted duration
      double mult_duration = 1.0;
      if (m_actual == '.') {
        mult_duration = 1.5;
        next_char();
      }

      float freq = m_notes.frequency(noteEnum, (uint8_t)octave);
      freq = transpose(freq, m_tranpose_octaves);
      int msec = (int)((m_msec_semi / duration) * mult_duration);
      int midi = m_notes.frequencyToMidiNote(freq);
      play_note(freq, msec, midi);
    }
  }
};

}  // namespace audio_tools
