#include "mixer_speaker.h"

#ifdef USE_ESP32

#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"

#include <mixer.h>        // esp-audio-libs
#include <pcm_convert.h>  // esp-audio-libs

#include <algorithm>
#include <cstring>

namespace esphome::mixer_speaker {

static const UBaseType_t MIXER_TASK_PRIORITY = 10;

static const uint32_t STOPPING_TIMEOUT_MS = 5000;
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
static const uint32_t TASK_DELAY_MS = 25;
static const uint32_t MIXER_AUTO_STOP_DEBOUNCE_MS = 200;

static const size_t TASK_STACK_SIZE = 4096;

static const char *const TAG = "speaker_mixer";

// Event bits for SourceSpeaker command processing
enum SourceSpeakerEventBits : uint32_t {
  SOURCE_SPEAKER_COMMAND_START = (1 << 0),
  SOURCE_SPEAKER_COMMAND_STOP = (1 << 1),
  SOURCE_SPEAKER_COMMAND_FINISH = (1 << 2),
};

// Event bits for mixer task control and state
enum MixerTaskEventBits : uint32_t {
  MIXER_TASK_COMMAND_START = (1 << 0),
  MIXER_TASK_COMMAND_STOP = (1 << 1),
  MIXER_TASK_STATE_STARTING = (1 << 10),
  MIXER_TASK_STATE_RUNNING = (1 << 11),
  MIXER_TASK_STATE_STOPPING = (1 << 12),
  MIXER_TASK_STATE_STOPPED = (1 << 13),
  MIXER_TASK_ERR_ESP_NO_MEM = (1 << 19),
  MIXER_TASK_ALL_BITS = 0x00FFFFFF,  // All valid FreeRTOS event group bits
};

static inline uint32_t atomic_subtract_clamped(std::atomic<uint32_t> &var, uint32_t amount) {
  uint32_t current = var.load(std::memory_order_acquire);
  uint32_t subtracted = 0;
  if (current > 0) {
    uint32_t new_value;
    do {
      subtracted = std::min(amount, current);
      new_value = current - subtracted;
    } while (!var.compare_exchange_weak(current, new_value, std::memory_order_release, std::memory_order_acquire));
  }
  return subtracted;
}

static bool create_event_group(EventGroupHandle_t &event_group, Component *component) {
  event_group = xEventGroupCreate();
  if (event_group == nullptr) {
    ESP_LOGE(TAG, "Failed to create event group");
    component->mark_failed();
    return false;
  }
  return true;
}

void SourceSpeaker::dump_config() {
  ESP_LOGCONFIG(TAG,
                "Mixer Source Speaker\n"
                "  Buffer Duration: %" PRIu32 " ms",
                this->buffer_duration_ms_);
  if (this->timeout_ms_.has_value()) {
    ESP_LOGCONFIG(TAG, "  Timeout: %" PRIu32 " ms", this->timeout_ms_.value());
  } else {
    ESP_LOGCONFIG(TAG, "  Timeout: never");
  }
}

void SourceSpeaker::setup() {
  if (!create_event_group(this->event_group_, this)) {
    return;
  }

  // Start with loop disabled since we begin in STATE_STOPPED with no pending commands
  this->disable_loop();

  this->parent_->get_output_speaker()->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
    // First, drain the playback delay (frames in pipeline before this source started contributing)
    uint32_t delay_to_drain = atomic_subtract_clamped(this->playback_delay_frames_, new_frames);
    uint32_t remaining_frames = new_frames - delay_to_drain;

    // Then, count towards this source's pending playback frames
    if (remaining_frames > 0) {
      uint32_t speakers_playback_frames = atomic_subtract_clamped(this->pending_playback_frames_, remaining_frames);
      if (speakers_playback_frames > 0) {
        this->audio_output_callback_(speakers_playback_frames, write_timestamp);
      }
    }
  });
}

void SourceSpeaker::loop() {
  uint32_t event_bits = xEventGroupGetBits(this->event_group_);

  // Process commands with priority: STOP > FINISH > START
  // This ensures stop commands take precedence over conflicting start commands
  if (event_bits & SOURCE_SPEAKER_COMMAND_STOP) {
    if (this->state_ == speaker::STATE_RUNNING) {
      // Clear both STOP and START bits - stop takes precedence
      xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START);
      this->enter_stopping_state_();
    } else if (this->state_ == speaker::STATE_STOPPED) {
      // Already stopped, just clear the command bits
      xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START);
    }
    // Leave bits set if transitioning states (STARTING/STOPPING) - will be processed once state allows
  } else if (event_bits & SOURCE_SPEAKER_COMMAND_FINISH) {
    if (this->state_ == speaker::STATE_RUNNING) {
      xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH);
      this->stop_gracefully_ = true;
    } else if (this->state_ == speaker::STATE_STOPPED) {
      // Already stopped, just clear the command bit
      xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH);
    }
    // Leave bit set if transitioning states - will be processed once state allows
  } else if (event_bits & SOURCE_SPEAKER_COMMAND_START) {
    if (this->state_ == speaker::STATE_STOPPED) {
      xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START);
      this->state_ = speaker::STATE_STARTING;
    } else if (this->state_ == speaker::STATE_RUNNING) {
      // Already running, just clear the command bit
      xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START);
    }
    // Leave bit set if transitioning states - will be processed once state allows
  }
  // Process state machine
  switch (this->state_) {
    case speaker::STATE_STARTING: {
      esp_err_t err = this->start_();
      if (err == ESP_OK) {
        this->pending_playback_frames_.store(0, std::memory_order_release);  // reset pending playback frames
        this->playback_delay_frames_.store(0, std::memory_order_release);    // reset playback delay
        this->has_contributed_.store(false, std::memory_order_release);      // reset contribution tracking
        this->state_ = speaker::STATE_RUNNING;
        this->stop_gracefully_ = false;
        this->last_seen_data_ms_ = millis();
        this->status_clear_error();
      } else {
        switch (err) {
          case ESP_ERR_NO_MEM:
            this->status_set_error(LOG_STR("Not enough memory"));
            break;
          case ESP_ERR_NOT_SUPPORTED:
            this->status_set_error(LOG_STR("Unsupported bit depth"));
            break;
          case ESP_ERR_INVALID_ARG:
            this->status_set_error(LOG_STR("Incompatible audio streams"));
            break;
          case ESP_ERR_INVALID_STATE:
            this->status_set_error(LOG_STR("Task failed"));
            break;
          default:
            this->status_set_error(LOG_STR("Failed"));
            break;
        }

        this->enter_stopping_state_();
      }
      break;
    }
    case speaker::STATE_RUNNING:
      if (!this->audio_source_->has_buffered_data() &&
          (this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) {
        // No audio data in buffer waiting to get mixed and no frames are pending playback
        if ((this->timeout_ms_.has_value() && ((millis() - this->last_seen_data_ms_) > this->timeout_ms_.value())) ||
            this->stop_gracefully_) {
          // Timeout exceeded or graceful stop requested
          this->enter_stopping_state_();
        }
      }
      break;
    case speaker::STATE_STOPPING: {
      if ((this->parent_->get_output_speaker()->get_pause_state()) ||
          ((millis() - this->stopping_start_ms_) > STOPPING_TIMEOUT_MS)) {
        // If parent speaker is paused or if the stopping timeout is exceeded, force stop the output speaker
        this->parent_->get_output_speaker()->stop();
      }

      if (this->parent_->get_output_speaker()->is_stopped() ||
          (this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) {
        // Output speaker is stopped OR all pending playback frames have played
        this->pending_playback_frames_.store(0, std::memory_order_release);
        this->stop_gracefully_ = false;

        this->state_ = speaker::STATE_STOPPED;
      }
      break;
    }
    case speaker::STATE_STOPPED:
      // Re-check event bits for any new commands that may have arrived
      event_bits = xEventGroupGetBits(this->event_group_);
      if (!(event_bits &
            (SOURCE_SPEAKER_COMMAND_START | SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_FINISH))) {
        // No pending commands, disable loop to save CPU cycles
        this->disable_loop();
      }
      break;
  }
}

size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
  if (this->is_stopped()) {
    this->start();
  }
  size_t bytes_written = 0;
  std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
  if (temp_ring_buffer.use_count() > 0) {
    // Only write to the ring buffer if the reference is valid
    bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
    if (bytes_written > 0) {
      this->last_seen_data_ms_ = millis();
    }
  } else {
    // Delay to avoid repeatedly hammering while waiting for the speaker to start
    vTaskDelay(ticks_to_wait);
  }
  return bytes_written;
}

void SourceSpeaker::send_command_(uint32_t command_bit, bool wake_loop) {
  this->enable_loop_soon_any_context();
  uint32_t event_bits = xEventGroupGetBits(this->event_group_);
  if (!(event_bits & command_bit)) {
    xEventGroupSetBits(this->event_group_, command_bit);
    if (wake_loop) {
      App.wake_loop_threadsafe();
    }
  }
}

void SourceSpeaker::start() { this->send_command_(SOURCE_SPEAKER_COMMAND_START, true); }

esp_err_t SourceSpeaker::start_() {
  const size_t bytes_per_frame = this->audio_stream_info_.frames_to_bytes(1);
  // Round the ring buffer size down to a multiple of bytes_per_frame so the wrap boundary stays frame-aligned and
  // avoids unnecessary single-frame splices.
  const size_t ring_buffer_size =
      (this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_) / bytes_per_frame) * bytes_per_frame;
  if (this->audio_source_.use_count() == 0) {
    std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
    if (!temp_ring_buffer) {
      temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size);
      this->ring_buffer_ = temp_ring_buffer;
    }

    if (!temp_ring_buffer) {
      return ESP_ERR_NO_MEM;
    }

    std::unique_ptr<audio::RingBufferAudioSource> source = audio::RingBufferAudioSource::create(
        temp_ring_buffer, this->audio_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS),
        static_cast<uint8_t>(bytes_per_frame));
    if (source == nullptr) {
      return ESP_ERR_NO_MEM;
    }
    this->audio_source_ = std::move(source);
  }

  return this->parent_->start(this->audio_stream_info_);
}

void SourceSpeaker::stop() { this->send_command_(SOURCE_SPEAKER_COMMAND_STOP); }

void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); }

bool SourceSpeaker::has_buffered_data() const {
  return ((this->audio_source_.use_count() > 0) && this->audio_source_->has_buffered_data());
}

void SourceSpeaker::set_mute_state(bool mute_state) {
  this->mute_state_ = mute_state;
  this->parent_->get_output_speaker()->set_mute_state(mute_state);
}

bool SourceSpeaker::get_mute_state() { return this->parent_->get_output_speaker()->get_mute_state(); }

void SourceSpeaker::set_volume(float volume) {
  this->volume_ = volume;
  this->parent_->get_output_speaker()->set_volume(volume);
}

float SourceSpeaker::get_volume() { return this->parent_->get_output_speaker()->get_volume(); }

size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::RingBufferAudioSource> &audio_source,
                                               TickType_t ticks_to_wait) {
  if (audio_source->available() > 0) {
    // Existing exposure was ducked when fill() promoted it; do not re-duck on partial-consume re-entry.
    return 0;
  }

  size_t bytes_read = audio_source->fill(ticks_to_wait, false);

  uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
  if (samples_to_duck > 0) {
    esp_audio_libs::ducking::apply(audio_source->mutable_data(),
                                   static_cast<uint8_t>(this->audio_stream_info_.get_bits_per_sample() / 8),
                                   samples_to_duck, this->ducking_state_);
  }

  return bytes_read;
}

void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) {
  const uint32_t transition_samples = duration > 0 ? this->audio_stream_info_.ms_to_samples(duration) : 0;
  esp_audio_libs::ducking::set_target(this->ducking_state_, decibel_reduction, transition_samples);
}

void SourceSpeaker::enter_stopping_state_() {
  this->state_ = speaker::STATE_STOPPING;
  this->stopping_start_ms_ = millis();
  this->audio_source_.reset();
}

void MixerSpeaker::dump_config() {
  ESP_LOGCONFIG(TAG,
                "Speaker Mixer:\n"
                "  Number of output channels: %" PRIu8 "\n"
                "  Output bits per sample: %" PRIu8,
                this->output_channels_, this->output_bits_per_sample_);
}

void MixerSpeaker::setup() {
  if (!create_event_group(this->event_group_, this)) {
    return;
  }

  // Register callback to track frames in the output pipeline
  this->output_speaker_->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
    atomic_subtract_clamped(this->frames_in_pipeline_, new_frames);
  });

  // Start with loop disabled since no task is running and no commands are pending
  this->disable_loop();
}

void MixerSpeaker::loop() {
  uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);

  // Handle pending start request
  if (event_group_bits & MIXER_TASK_COMMAND_START) {
    // Only start the task if it's fully stopped and cleaned up
    if (!this->status_has_error() && !this->task_.is_created()) {
      if (this->task_.create(audio_mixer_task, "mixer", TASK_STACK_SIZE, (void *) this, MIXER_TASK_PRIORITY,
                             this->task_stack_in_psram_)) {
        xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_START);
      } else {
        ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
        this->status_momentary_error("failure", 1000);
        return;
      }
    }
  }

  if (event_group_bits & MIXER_TASK_STATE_STARTING) {
    ESP_LOGD(TAG, "Starting");
    xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STARTING);
  }
  if (event_group_bits & MIXER_TASK_ERR_ESP_NO_MEM) {
    this->status_set_error(LOG_STR("Not enough memory"));
    xEventGroupClearBits(this->event_group_, MIXER_TASK_ERR_ESP_NO_MEM);
  }
  if (event_group_bits & MIXER_TASK_STATE_RUNNING) {
    ESP_LOGV(TAG, "Started");
    this->status_clear_error();
    xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_RUNNING);
  }
  if (event_group_bits & MIXER_TASK_STATE_STOPPING) {
    ESP_LOGV(TAG, "Stopping");
    xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING);
  }
  if (event_group_bits & MIXER_TASK_STATE_STOPPED) {
    this->task_.deallocate();
    ESP_LOGD(TAG, "Stopped");
    xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS);
    this->all_stopped_since_ms_ = 0;
  }

  if (this->task_.is_created()) {
    // If the mixer task is running, check if all source speakers are stopped

    bool all_stopped = true;

    for (auto &speaker : this->source_speakers_) {
      all_stopped &= speaker->is_stopped();
    }

    if (all_stopped) {
      if (this->all_stopped_since_ms_ == 0) {
        this->all_stopped_since_ms_ = millis();
      } else if ((millis() - this->all_stopped_since_ms_) >= MIXER_AUTO_STOP_DEBOUNCE_MS) {
        // Send stop command only after a short debounce to avoid stop/start thrash during rapid seeks.
        xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP);
      }
    } else {
      this->all_stopped_since_ms_ = 0;
      // New activity detected; clear any stale auto-stop request before it can stop the running task.
      if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
        xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_STOP);
      }
    }
  } else {
    // Task is fully stopped and cleaned up, check if we can disable loop
    event_group_bits = xEventGroupGetBits(this->event_group_);
    if (event_group_bits == 0) {
      // No pending events, disable loop to save CPU cycles
      this->disable_loop();
    }
  }
}

esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) {
  if (!this->audio_stream_info_.has_value()) {
    this->audio_stream_info_ =
        audio::AudioStreamInfo(this->output_bits_per_sample_, this->output_channels_, stream_info.get_sample_rate());
    this->output_speaker_->set_audio_stream_info(this->audio_stream_info_.value());
  } else {
    if (!this->queue_mode_ && (stream_info.get_sample_rate() != this->audio_stream_info_.value().get_sample_rate())) {
      // The two audio streams must have the same sample rate to mix properly if not in queue mode
      return ESP_ERR_INVALID_ARG;
    }
  }

  this->enable_loop_soon_any_context();  // ensure loop processes command

  // Starting a new stream supersedes any previously queued stop request.
  xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_STOP);

  uint32_t event_bits = xEventGroupGetBits(this->event_group_);
  if (!(event_bits & MIXER_TASK_COMMAND_START)) {
    // Set MIXER_TASK_COMMAND_START bit if not already set, and then immediately wake for low latency
    xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_START);
    App.wake_loop_threadsafe();
  }

  return ESP_OK;
}

// NOLINTBEGIN(bugprone-unchecked-optional-access) -- audio_stream_info_ always set before this task is created
void MixerSpeaker::audio_mixer_task(void *params) {
  MixerSpeaker *this_mixer = static_cast<MixerSpeaker *>(params);

  xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING);

  {  // Ensure C++ objects fall out of scope to ensure proper cleanup before stopping the task
    std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
        this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));

    if (output_transfer_buffer == nullptr) {
      xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM);

      vTaskSuspend(nullptr);  // Suspend this task indefinitely until the loop method deletes it
    }

    output_transfer_buffer->set_sink(this_mixer->output_speaker_);

    xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING);

    bool sent_finished = false;

    // Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema)
    FixedVector<SourceSpeaker *> speakers_with_data;
    FixedVector<std::shared_ptr<audio::RingBufferAudioSource>> audio_sources_with_data;
    speakers_with_data.init(this_mixer->source_speakers_.size());
    audio_sources_with_data.init(this_mixer->source_speakers_.size());

    while (true) {
      uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
      if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
        break;
      }

      // Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves
      output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false);

      const uint32_t output_frames_free =
          this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());

      speakers_with_data.clear();
      audio_sources_with_data.clear();

      for (auto &speaker : this_mixer->source_speakers_) {
        if (speaker->is_running() && !speaker->get_pause_state()) {
          // Speaker is running and not paused, so it possibly can provide audio data
          std::shared_ptr<audio::RingBufferAudioSource> audio_source = speaker->get_audio_source().lock();
          if (audio_source.use_count() == 0) {
            // No audio source allocated, so skip processing this speaker
            continue;
          }
          speaker->process_data_from_source(audio_source, 0);  // Exposes and ducks audio from source ring buffers

          if (audio_source->available() > 0) {
            // Retain shared ownership across the mixing pass so the source isn't released mid-mix
            audio_sources_with_data.push_back(audio_source);
            speakers_with_data.push_back(speaker);
          }
        }
      }

      if (audio_sources_with_data.empty()) {
        // No audio available for transferring, block task temporarily
        delay(TASK_DELAY_MS);
        continue;
      }

      uint32_t frames_to_mix = output_frames_free;

      const audio::AudioStreamInfo &output_info = this_mixer->audio_stream_info_.value();
      const uint8_t output_bps = output_info.get_bits_per_sample() / 8;
      const uint8_t output_channels = output_info.get_channels();

      if ((audio_sources_with_data.size() == 1) || this_mixer->queue_mode_) {
        // Only one speaker has audio data, just copy samples over

        audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info();

        if (active_stream_info.get_sample_rate() ==
            this_mixer->output_speaker_->get_audio_stream_info().get_sample_rate()) {
          // Speaker's sample rate matches the output speaker's, convert directly into the output buffer

          const uint32_t frames_available_in_buffer =
              active_stream_info.bytes_to_frames(audio_sources_with_data[0]->available());
          frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
          esp_audio_libs::pcm_convert::copy_frames(
              audio_sources_with_data[0]->data(), output_transfer_buffer->get_buffer_end(),
              static_cast<uint8_t>(active_stream_info.get_bits_per_sample() / 8), active_stream_info.get_channels(),
              output_bps, output_channels, frames_to_mix);

          // Set playback delay for newly contributing source
          if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) {
            speakers_with_data[0]->playback_delay_frames_.store(
                this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release);
            speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release);
          }

          // Update source speaker pending frames
          speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
          audio_sources_with_data[0]->consume(active_stream_info.frames_to_bytes(frames_to_mix));

          // Update output transfer buffer length and pipeline frame count
          output_transfer_buffer->increase_buffer_length(output_info.frames_to_bytes(frames_to_mix));
          this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
        } else {
          // Speaker's stream info doesn't match the output speaker's, so it's a new source speaker
          if (!this_mixer->output_speaker_->is_stopped()) {
            if (!sent_finished) {
              this_mixer->output_speaker_->finish();
              sent_finished = true;  // Avoid repeatedly sending the finish command
            }
          } else {
            // Speaker has finished writing the current audio, update the stream information and restart the speaker
            this_mixer->audio_stream_info_ =
                audio::AudioStreamInfo(this_mixer->output_bits_per_sample_, this_mixer->output_channels_,
                                       active_stream_info.get_sample_rate());
            this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
            this_mixer->output_speaker_->start();
            // Reset pipeline frame count since we're starting fresh with a new sample rate
            this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
            sent_finished = false;
          }
        }
      } else {
        // Determine how many frames to mix
        for (size_t i = 0; i < audio_sources_with_data.size(); ++i) {
          const uint32_t frames_available_in_buffer =
              speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(audio_sources_with_data[i]->available());
          frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
        }
        const uint8_t *primary_buffer = audio_sources_with_data[0]->data();
        audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info();

        // Mix two streams together at a time, accumulating into the output buffer.
        for (size_t i = 1; i < audio_sources_with_data.size(); ++i) {
          esp_audio_libs::mixer::mix_frames(
              primary_buffer, static_cast<uint8_t>(primary_stream_info.get_bits_per_sample() / 8),
              primary_stream_info.get_channels(), audio_sources_with_data[i]->data(),
              static_cast<uint8_t>(speakers_with_data[i]->get_audio_stream_info().get_bits_per_sample() / 8),
              speakers_with_data[i]->get_audio_stream_info().get_channels(), output_transfer_buffer->get_buffer_end(),
              output_bps, output_channels, frames_to_mix);

          if (i != audio_sources_with_data.size() - 1) {
            // Need to mix more streams together, point primary buffer and stream info to the already mixed output
            primary_buffer = output_transfer_buffer->get_buffer_end();
            primary_stream_info = output_info;
          }
        }

        // Get current pipeline depth for delay calculation (before incrementing)
        uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire);

        // Update source audio source consumption and add new audio durations to the source speaker pending playbacks
        for (size_t i = 0; i < audio_sources_with_data.size(); ++i) {
          // Set playback delay for newly contributing sources
          if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) {
            speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release);
            speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release);
          }

          speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
          audio_sources_with_data[i]->consume(
              speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix));
        }

        // Update output transfer buffer length and pipeline frame count (once, not per source)
        output_transfer_buffer->increase_buffer_length(output_info.frames_to_bytes(frames_to_mix));
        this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
      }
    }

    xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING);
  }

  // Reset pipeline frame count since the task is stopping
  this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);

  xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED);

  vTaskSuspend(nullptr);  // Suspend this task indefinitely until the loop method deletes it
}
// NOLINTEND(bugprone-unchecked-optional-access)

}  // namespace esphome::mixer_speaker

#endif
