/*
 * Copyright 2023 jacqueline <me@jacqueline.id.au>
 *
 * SPDX-License-Identifier: GPL-3.0-only
 */

#include "audio/audio_fsm.hpp"

#include <cstdint>
#include <future>
#include <memory>
#include <sstream>
#include <variant>

#include "audio/audio_source.hpp"
#include "audio/sine_source.hpp"
#include "cppbor.h"
#include "cppbor_parse.h"
#include "drivers/pcm_buffer.hpp"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#include "freertos/projdefs.h"
#include "tinyfsm.hpp"

#include "audio/audio_decoder.hpp"
#include "audio/audio_events.hpp"
#include "audio/audio_sink.hpp"
#include "audio/bt_audio_output.hpp"
#include "audio/fatfs_stream_factory.hpp"
#include "audio/i2s_audio_output.hpp"
#include "audio/stream_cues.hpp"
#include "audio/track_queue.hpp"
#include "database/future_fetcher.hpp"
#include "database/track.hpp"
#include "drivers/bluetooth.hpp"
#include "drivers/bluetooth_types.hpp"
#include "drivers/i2s_dac.hpp"
#include "drivers/nvs.hpp"
#include "drivers/storage.hpp"
#include "drivers/wm8523.hpp"
#include "events/event_queue.hpp"
#include "sample.hpp"
#include "system_fsm/service_locator.hpp"
#include "system_fsm/system_events.hpp"
#include "tts/player.hpp"

namespace audio {

[[maybe_unused]] static const char kTag[] = "audio_fsm";

std::shared_ptr<system_fsm::ServiceLocator> AudioState::sServices;

std::shared_ptr<FatfsStreamFactory> AudioState::sStreamFactory;

std::unique_ptr<Decoder> AudioState::sDecoder;
std::shared_ptr<SampleProcessor> AudioState::sSampleProcessor;

std::shared_ptr<IAudioOutput> AudioState::sOutput;
std::shared_ptr<I2SAudioOutput> AudioState::sI2SOutput;
std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput;

// For tracks, keep about two seconds' worth of samples at 2ch 48kHz. This
// is more headroom than we need for small playback, but it doesn't hurt to
// keep some PSRAM in our pockets for a rainy day.
constexpr size_t kTrackDrainLatencySamples = 48000 * 2 * 2;

// For system sounds, we intentionally choose codecs that are very fast to
// decode. This lets us get away with a much smaller drain buffer.
constexpr size_t kSystemDrainLatencySamples = 48000;

std::unique_ptr<drivers::OutputBuffers> AudioState::sDrainBuffers;
std::optional<IAudioOutput::Format> AudioState::sDrainFormat;

StreamCues AudioState::sStreamCues;

bool AudioState::sIsPaused = true;
bool AudioState::sIsTtsPlaying = false;

uint8_t AudioState::sUpdateCounter = 0;

auto AudioState::emitPlaybackUpdate(bool paused) -> void {
  std::optional<uint32_t> position;
  auto current = sStreamCues.current();
  if (current.first && sDrainFormat) {
    position = ((current.second +
                 (sDrainFormat->num_channels * sDrainFormat->sample_rate / 2)) /
                (sDrainFormat->num_channels * sDrainFormat->sample_rate)) +
               current.first->start_offset.value_or(0);
  }

  // If we've got an elapsed duration and it's more than 5 minutes
  // increment a counter. Every 60 counts (ie, every minute) save the current
  // elapsed position
  if (position && *position > (5 * 60)) {
    sUpdateCounter++;
    if (sUpdateCounter >= 60) {
      sUpdateCounter = 0;
      updateSavedPosition(current.first->uri, *position);
    }
  }

  PlaybackUpdate event{
      .current_track = current.first,
      .track_position = position,
      .paused = paused,
  };

  events::System().Dispatch(event);
  events::Ui().Dispatch(event);
}

void AudioState::react(const QueueUpdate& ev) {
  SetTrack cmd{
      .new_track = std::monostate{},
      .seek_to_second = ev.seek_to_second,
  };

  auto current = sServices->track_queue().current();
  cmd.new_track = current;

  switch (ev.reason) {
    case QueueUpdate::kExplicitUpdate:
      if (!ev.current_changed) {
        return;
      }
      break;
    case QueueUpdate::kRepeatingLastTrack:
      break;
    case QueueUpdate::kTrackFinished:
      if (!ev.current_changed) {
        cmd.new_track = std::monostate{};
      }
      break;
    case QueueUpdate::kBulkLoadingUpdate:
      // Bulk loading updates are informational only; a separate QueueUpdate
      // event will be sent when loading is done.
    case QueueUpdate::kDeserialised:
      // The current track is deserialised separately in order to retain seek
      // position.
    default:
      return;
  }

  tinyfsm::FsmList<AudioState>::dispatch(cmd);
}

void AudioState::react(const SetTrack& ev) {
  if (std::holds_alternative<std::monostate>(ev.new_track)) {
    ESP_LOGI(kTag, "playback finished, awaiting drain");
    sDecoder->open({});
    sStreamCues.clear();
    return;
  }

  // Move the rest of the work to a background worker, since it may require db
  // lookups to resolve a track id into a path.
  auto new_track = ev.new_track;
  uint32_t seek_to = ev.seek_to_second.value_or(0);
  sServices->bg_worker().Dispatch<void>([=]() {
    std::shared_ptr<TaggedStream> stream;
    if (std::holds_alternative<database::TrackId>(new_track)) {
      stream = sStreamFactory->create(std::get<database::TrackId>(new_track),
                                      seek_to);
    } else if (std::holds_alternative<std::string>(new_track)) {
      stream =
          sStreamFactory->create(std::get<std::string>(new_track), seek_to);
    }

    // Always give the stream to the decoder, even if it turns out to be empty.
    // This has the effect of stopping the current playback, which is generally
    // what the user expects to happen when they say "Play this track!", even
    // if the new track has an issue.
    sDecoder->open(stream);

    // ...but if the stream that failed is the front of the queue, then we
    // should advance to the next track in order to keep the tunes flowing.
    if (!stream) {
      auto& queue = sServices->track_queue();
      if (new_track == queue.current()) {
        queue.finish();
      }
    }
  });
}

void AudioState::react(const PlaySineWave& ev) {
  auto tags = std::make_shared<database::TrackTags>();

  std::stringstream title;
  title << ev.frequency << "Hz Sine Wave";
  tags->title(title.str());

  sDecoder->open(std::make_shared<TaggedStream>(
      tags, std::make_unique<SineSource>(ev.frequency), title.str()));
}

void AudioState::react(const TogglePlayPause& ev) {
  sIsPaused = !ev.set_to.value_or(sIsPaused);
  if (!sIsPaused && is_in_state<states::Standby>() &&
      sStreamCues.current().first) {
    transit<states::Playback>();
  } else if (sIsPaused && is_in_state<states::Playback>()) {
    transit<states::Standby>();
  }
}

void AudioState::react(const TtsPlaybackChanged& ev) {
  sIsTtsPlaying = ev.is_playing;
  updateOutputMode();
}

void AudioState::react(const internal::DecodingFinished& ev) {
  // If we just finished playing whatever's at the front of the queue, then we
  // need to advanve and start playing the next one ASAP in order to continue
  // gaplessly.
  sServices->bg_worker().Dispatch<void>([=]() {
    auto& queue = sServices->track_queue();
    auto current = queue.current();
    if (std::holds_alternative<std::monostate>(current)) {
      return;
    }
    auto db = sServices->database().lock();
    if (!db) {
      return;
    }
    std::string path;
    if (std::holds_alternative<std::string>(current)) {
      path = std::get<std::string>(current);
    } else if (std::holds_alternative<database::TrackId>(current)) {
      auto tid = std::get<database::TrackId>(current);
      path = db->getTrackPath(tid).value_or("");
    }
    if (path == ev.track->uri) {
      queue.finish();
      incrementPlayCount(ev.track->uri);
    }
  });
}

void AudioState::react(const internal::StreamStarted& ev) {
  if (sDrainFormat != ev.sink_format) {
    sDrainFormat = ev.sink_format;
    ESP_LOGI(kTag, "sink_format=%u ch @ %lu hz", sDrainFormat->num_channels,
             sDrainFormat->sample_rate);
  }

  sStreamCues.addCue(ev.track, ev.cue_at_sample);
  sStreamCues.update(sDrainBuffers->first.totalReceived());

  if (!sIsPaused && !is_in_state<states::Playback>()) {
    transit<states::Playback>();
  } else {
    // Make sure everyone knows we've got a track ready to go, even if we're
    // not playing it yet. This mostly matters when restoring the queue from
    // disk after booting.
    emitPlaybackUpdate(true);
  }
}

void AudioState::react(const internal::StreamEnded& ev) {
  sStreamCues.addCue({}, ev.cue_at_sample);
}

void AudioState::react(const system_fsm::HasPhonesChanged& ev) {
  if (ev.has_headphones) {
    events::Audio().Dispatch(audio::OutputModeChanged{
        .set_to = drivers::NvsStorage::Output::kHeadphones});
  } else {
    if (sServices->bluetooth().enabled()) {
      events::Audio().Dispatch(audio::OutputModeChanged{
          .set_to = drivers::NvsStorage::Output::kBluetooth});
    } else {
      // Nothing connected
      transit<states::Standby>();
    }
  }
}

void AudioState::react(const system_fsm::BluetoothEvent& ev) {
  using drivers::bluetooth::SimpleEvent;
  if (std::holds_alternative<SimpleEvent>(ev.event)) {
    auto simpleEvent = std::get<SimpleEvent>(ev.event);
    switch (simpleEvent) {
      case SimpleEvent::kConnectionStateChanged: {
        auto bt = sServices->bluetooth();
        if (bt.connectionState() !=
            drivers::Bluetooth::ConnectionState::kConnected) {
          // If BT Disconnected, move to standby state
          events::Audio().Dispatch(audio::OutputModeChanged{
              .set_to = drivers::NvsStorage::Output::kHeadphones});
          transit<states::Standby>();
          return;
        }
        auto dev = sServices->bluetooth().pairedDevice();
        if (!dev) {
          return;
        }
        events::Audio().Dispatch(audio::OutputModeChanged{
            .set_to = drivers::NvsStorage::Output::kBluetooth});
        sBtOutput->SetVolume(sServices->nvs().BluetoothVolume(dev->mac));
        events::Ui().Dispatch(VolumeChanged{
            .percent = sOutput->GetVolumePct(),
            .db = sOutput->GetVolumeDb(),
        });
        break;
      }
      default:
        break;
    }
  }
  if (std::holds_alternative<drivers::bluetooth::RemoteVolumeChanged>(
          ev.event)) {
    auto volume_chg =
        std::get<drivers::bluetooth::RemoteVolumeChanged>(ev.event).new_vol;
    events::Ui().Dispatch(RemoteVolumeChanged{.value = volume_chg});
  }
}

void AudioState::react(const StepUpVolume& ev) {
  if (sOutput->AdjustVolumeUp()) {
    commitVolume();
    events::Ui().Dispatch(VolumeChanged{
        .percent = sOutput->GetVolumePct(),
        .db = sOutput->GetVolumeDb(),
    });
  }
}

void AudioState::react(const StepDownVolume& ev) {
  if (sOutput->AdjustVolumeDown()) {
    commitVolume();
    events::Ui().Dispatch(VolumeChanged{
        .percent = sOutput->GetVolumePct(),
        .db = sOutput->GetVolumeDb(),
    });
  }
}

void AudioState::react(const SetVolume& ev) {
  if (ev.db.has_value()) {
    if (sOutput->SetVolumeDb(ev.db.value())) {
      commitVolume();
      events::Ui().Dispatch(VolumeChanged{
          .percent = sOutput->GetVolumePct(),
          .db = sOutput->GetVolumeDb(),
      });
    }

  } else if (ev.percent.has_value()) {
    if (sOutput->SetVolumePct(ev.percent.value())) {
      commitVolume();
      events::Ui().Dispatch(VolumeChanged{
          .percent = sOutput->GetVolumePct(),
          .db = sOutput->GetVolumeDb(),
      });
    }
  }
}

void AudioState::react(const SetVolumeLimit& ev) {
  uint16_t limit_in_dac_units =
      drivers::wm8523::kLineLevelReferenceVolume + (ev.limit_db * 4);

  sI2SOutput->SetMaxVolume(limit_in_dac_units);
  sServices->nvs().AmpMaxVolume(limit_in_dac_units);

  events::Ui().Dispatch(VolumeLimitChanged{
      .new_limit_db = ev.limit_db,
  });
  events::Ui().Dispatch(VolumeChanged{
      .percent = sOutput->GetVolumePct(),
      .db = sOutput->GetVolumeDb(),
  });
}

void AudioState::react(const SetVolumeBalance& ev) {
  sOutput->SetVolumeImbalance(ev.left_bias);
  sServices->nvs().AmpLeftBias(ev.left_bias);

  events::Ui().Dispatch(VolumeBalanceChanged{
      .left_bias = ev.left_bias,
  });
}

void AudioState::react(const OutputModeChanged& ev) {
  ESP_LOGI(kTag, "output mode changed");
  auto new_mode = sServices->nvs().OutputMode();
  if (ev.set_to) {
    new_mode = *ev.set_to;
  }
  std::shared_ptr<IAudioOutput> new_output;
  switch (new_mode) {
    case drivers::NvsStorage::Output::kBluetooth:
      new_output = sBtOutput;
      break;
    case drivers::NvsStorage::Output::kHeadphones:
      new_output = sI2SOutput;
      break;
  }
  if (new_output == sOutput) {
    return;
  }
  sOutput->mode(IAudioOutput::Modes::kOff);
  sOutput = new_output;
  sSampleProcessor->SetOutput(sOutput);
  updateOutputMode();

  // Bluetooth volume isn't 'changed' until we've connected to a device.
  if (new_mode == drivers::NvsStorage::Output::kHeadphones) {
    events::Ui().Dispatch(VolumeChanged{
        .percent = sOutput->GetVolumePct(),
        .db = sOutput->GetVolumeDb(),
    });
  }
}

auto AudioState::updateTrackData(std::string uri,
                                 std::function<void(database::TrackData&)> fn)
    -> void {
  sServices->bg_worker().Dispatch<void>([=]() {
    auto db = sServices->database().lock();
    if (!db) {
      return;
    }
    auto id = db->getTrackID(uri);
    if (!id) {
      return;
    }
    auto track = db->getTrack(*id);
    if (!track) {
      return;
    }
    auto data = track->data().clone();
    std::invoke(fn, *data);
    db->setTrackData(*id, *data);
  });
}

auto AudioState::updateSavedPosition(std::string uri,
                                     uint32_t position) -> void {
  updateTrackData(
      uri, [=](database::TrackData& data) { data.last_position = position; });
}

auto AudioState::incrementPlayCount(std::string uri) -> void {
  updateTrackData(uri, [&](database::TrackData& data) { data.play_count++; });
}

auto AudioState::updateOutputMode() -> void {
  if (is_in_state<states::Playback>() || sIsTtsPlaying) {
    sOutput->mode(IAudioOutput::Modes::kOnPlaying);
  } else {
    sOutput->mode(IAudioOutput::Modes::kOnPaused);
  }
}

auto AudioState::commitVolume() -> void {
  auto mode = sServices->nvs().OutputMode();
  auto vol = sOutput->GetVolume();
  if (mode == drivers::NvsStorage::Output::kHeadphones) {
    sServices->nvs().AmpCurrentVolume(vol);
  } else if (mode == drivers::NvsStorage::Output::kBluetooth) {
    auto dev = sServices->bluetooth().pairedDevice();
    if (!dev) {
      return;
    }
    sServices->nvs().BluetoothVolume(dev->mac, vol);
  }
}

namespace states {

void Uninitialised::react(const system_fsm::BootComplete& ev) {
  sServices = ev.services;

  sDrainBuffers = std::make_unique<drivers::OutputBuffers>(
      kTrackDrainLatencySamples, kSystemDrainLatencySamples);
  sDrainBuffers->first.suspend(true);

  sStreamFactory.reset(
      new FatfsStreamFactory(sServices->database(), sServices->tag_parser()));
  sI2SOutput.reset(new I2SAudioOutput(sServices->gpios(), *sDrainBuffers));
  sBtOutput.reset(new BluetoothAudioOutput(
      sServices->bluetooth(), *sDrainBuffers, sServices->bg_worker()));

  auto& tts_provider = sServices->tts();
  auto tts_player = std::make_unique<tts::Player>(
      sServices->bg_worker(), sDrainBuffers->second, *sStreamFactory);
  tts_provider.player(std::move(tts_player));

  auto& nvs = sServices->nvs();
  sI2SOutput->SetMaxVolume(nvs.AmpMaxVolume());
  sI2SOutput->SetVolume(nvs.AmpCurrentVolume());
  sI2SOutput->SetVolumeImbalance(nvs.AmpLeftBias());

  // Always set to headphones output initially
  // Connecting bluetooth will change this
  sOutput = sI2SOutput;
  if (sServices->nvs().OutputMode() ==
      drivers::NvsStorage::Output::kBluetooth) {
    // Ensure Bluetooth gets enabled if it's the default sink.
    sServices->bluetooth().enable(true);
  }
  sOutput->mode(IAudioOutput::Modes::kOnPaused);

  events::Ui().Dispatch(VolumeLimitChanged{
      .new_limit_db =
          (static_cast<int>(nvs.AmpMaxVolume()) -
           static_cast<int>(drivers::wm8523::kLineLevelReferenceVolume)) /
          4,
  });
  events::Ui().Dispatch(VolumeChanged{
      .percent = sOutput->GetVolumePct(),
      .db = sOutput->GetVolumeDb(),
  });
  events::Ui().Dispatch(VolumeBalanceChanged{
      .left_bias = nvs.AmpLeftBias(),
  });

  sSampleProcessor.reset(new SampleProcessor(sDrainBuffers->first));
  sSampleProcessor->SetOutput(sOutput);

  sDecoder.reset(Decoder::Start(sSampleProcessor));

  transit<Standby>();
}

static const char kQueueKey[] = "audio:queue";
static const char kCurrentFileKey[] = "audio:current";

auto Standby::entry() -> void {
  updateOutputMode();
}

void Standby::react(const system_fsm::KeyLockChanged& ev) {
  if (!ev.locking) {
    return;
  }
  auto current = sStreamCues.current();
  sServices->bg_worker().Dispatch<void>([=]() {
    auto db = sServices->database().lock();
    if (!db) {
      return;
    }
    auto& queue = sServices->track_queue();
    if (queue.totalSize() <= queue.currentPosition()) {
      // Nothing is playing, so don't bother saving the queue.
      db->put(kQueueKey, "");
      return;
    }
    db->put(kQueueKey, queue.serialise());

    if (current.first && sDrainFormat) {
      uint32_t seconds = (current.second / (sDrainFormat->num_channels *
                                            sDrainFormat->sample_rate)) +
                         current.first->start_offset.value_or(0);
      cppbor::Array current_track{
          cppbor::Tstr{current.first->uri},
          cppbor::Uint{seconds},
      };
      db->put(kCurrentFileKey, current_track.toString());
    }
  });
}

void Standby::react(const system_fsm::SdStateChanged& ev) {
  auto state = sServices->sd();
  if (state != drivers::SdState::kMounted) {
    return;
  }
  sServices->bg_worker().Dispatch<void>([]() {
    auto db = sServices->database().lock();
    if (!db) {
      return;
    }

    // Open the queue file
    sServices->track_queue().open();

    // Restore the currently playing file before restoring the queue. This way,
    // we can fall back to restarting the queue's current track if there's any
    // issue restoring the current file.
    auto current = db->get(kCurrentFileKey);
    if (current) {
      // Again, ensure we don't boot-loop by trying to play a track that causes
      // a crash over and over again.
      db->put(kCurrentFileKey, "");
      auto [parsed, unused, err] = cppbor::parse(
          reinterpret_cast<uint8_t*>(current->data()), current->size());
      if (parsed->type() == cppbor::ARRAY) {
        std::string filename = parsed->asArray()->get(0)->asTstr()->value();
        uint32_t pos = parsed->asArray()->get(1)->asUint()->value();

        events::Audio().Dispatch(SetTrack{
            .new_track = filename,
            .seek_to_second = pos,
        });
      }
    }

    auto queue = db->get(kQueueKey);
    if (queue) {
      // Don't restore the same queue again. This ideally should do nothing,
      // but guards against bad edge cases where restoring the queue ends up
      // causing a crash.
      db->put(kQueueKey, "");
      sServices->track_queue().deserialise(*queue);
    }
  });
}

static TimerHandle_t sHeartbeatTimer;

static void heartbeat(TimerHandle_t) {
  events::Audio().Dispatch(internal::StreamHeartbeat{});
}

void Playback::entry() {
  ESP_LOGI(kTag, "audio output resumed");
  sDrainBuffers->first.suspend(false);
  updateOutputMode();
  emitPlaybackUpdate(false);

  if (!sHeartbeatTimer) {
    sHeartbeatTimer =
        xTimerCreate("stream", pdMS_TO_TICKS(1000), true, NULL, heartbeat);
  }
  xTimerStart(sHeartbeatTimer, portMAX_DELAY);
}

void Playback::exit() {
  ESP_LOGI(kTag, "audio output paused");
  xTimerStop(sHeartbeatTimer, portMAX_DELAY);
  sDrainBuffers->first.suspend(true);
  emitPlaybackUpdate(true);
}

void Playback::react(const system_fsm::SdStateChanged& ev) {
  if (sServices->sd() != drivers::SdState::kMounted) {
    transit<Standby>();
  }
}

void Playback::react(const internal::StreamHeartbeat& ev) {
  sStreamCues.update(sDrainBuffers->first.totalReceived());

  if (sStreamCues.hasStream()) {
    emitPlaybackUpdate(false);
  } else {
    // Finished the current stream, and there's nothing upcoming. We must be
    // finished.
    transit<Standby>();
  }
}

}  // namespace states

}  // namespace audio

FSM_INITIAL_STATE(audio::AudioState, audio::states::Uninitialised)
