#include "player.h"

#include "core/track/TrackLockProxy.h"
#include "playerDefs.h"

#include <iostream>

using namespace std;
using namespace player;
//--------------------------------------------------------------
/* Constructor */
Player::Player(sdi_toolBox::desktop::eventBus::Bus &hBus)
  : Node(hBus)
  , m_eventProcessing(*this)
{
  // Initialization

  // Event subscriptions
  subscribe(HashMessageType("remote.loadFile"));
  subscribe(HashMessageType("remote.setState"));
  // subscribe(HashMessageType("remote.setSongTime"));
  // subscribe(HashMessageType("remote.setSpeed"));
}
//--------------------------------------------------------------
/* Update the player state based on the current state and time */
void Player::update()
{
  if (m_playback.state == State::Stopped)
  {
    m_activeNotes.playing.clear();      // Clear the currently playing notes buffer
    m_activeNotes.upcoming.clear();     // Clear the upcoming notes buffer
  }
  else if (m_playback.state == State::WaitingForStart || m_playback.state == State::WaitingForResume)
  {
    // First time the update is called after receiving a "Play" command, initialize the playback state
    m_playback.startPlaying();

    const auto &notes = getNotes();     // Get a lock proxy to access the notes safely without copying the data
    notes.getActiveNotesAt(m_playback.currentPosition, WindowDuration, m_activeNotes);
  }
  else if (m_playback.state == State::Playing)
  {
    // Update the current position based on the elapsed time since the start of playback
    const auto now = std::chrono::steady_clock::now();

    const auto elapsedSinceStart = chrono::duration_cast<Seconds>(now - m_playback.startTime);
    m_playback.currentPosition   = m_playback.pauseOffset + elapsedSinceStart;

    // Check if the current position exceeds the track duration and stop playback if necessary
    if (m_playback.currentPosition >= m_data.track.getDuration())
    {
      m_playback.reset();
      m_playback.currentPosition = m_data.track.getDuration();     // Set the current position to the end of the track
      emit<LogMessage>(LogMessage::LogLevel::Info, "Playback finished");
    }

    const auto &notes = getNotes();     // Get a lock proxy to access the notes safely without copying the data
    notes.getActiveNotesAt(m_playback.currentPosition, WindowDuration, m_activeNotes);
  }
  else if (m_playback.state == State::Paused)
  {
    // In the paused state, we do not update the current position or time.
    // We simply keep the current position and active notes as they were when paused.
  }
}
//--------------------------------------------------------------
/* Get the currently active notes */
const track::ActiveNotes &Player::getActiveNotes() const
{
  return m_activeNotes;
}
//--------------------------------------------------------------
/* Load a MIDI file from disk */
void Player::loadFile(const std::filesystem::path &filePath)
{
  try
  {
    track::Track tmpTrack;
    tmpTrack.loadFromFile(filePath);

    scoped_lock lock(m_data.mtx);     // Lock the mutex to protect access to the track data
    m_data.track = std::move(tmpTrack);
  }
  catch (const std::exception &e)
  {
    cerr << "Error loading file: " << e.what() << endl;
  }
}
//--------------------------------------------------------------
/* Close the currently loaded MIDI file */
void Player::closeFile()
{
  scoped_lock lock(m_data.mtx);     // Lock the mutex to protect access to the track data
  m_data.track = {};                // Reset the track to an empty state
}
//--------------------------------------------------------------
/* Get a lock proxy to access the notes safely without copying the data */
track::TrackLockProxy Player::getNotes() const
{
  // The TrackLockProxy is automatically moved when returned (NRVO)
  return track::TrackLockProxy(m_data.mtx, &m_data.track);
}
//--------------------------------------------------------------
/* Reset the playback clock to its initial state (stopped, zero position) */
void Player::PlaybackClock::reset()
{
  state           = State::Stopped;
  startTime       = TimePoint();
  pauseOffset     = Seconds(0);
  currentPosition = Seconds(0);
}
//--------------------------------------------------------------
/* Start a new playback session, resetting the clock and position */
void Player::PlaybackClock::startNew()
{
  state           = State::WaitingForStart;
  pauseOffset     = Seconds(0);
  currentPosition = Seconds(0);
}
//--------------------------------------------------------------
/* Start playing from the current position */
void Player::PlaybackClock::startPlaying()
{
  startTime = std::chrono::steady_clock::now();
  state     = State::Playing;
}
//--------------------------------------------------------------
/* Pause the playback clock and store the current position */
void Player::PlaybackClock::pause()
{
  if (state == State::Playing)
  {
    pauseOffset = currentPosition;
    state       = State::Paused;
  }
}
//--------------------------------------------------------------
/* Resume the playback clock from the paused position */
void Player::PlaybackClock::resume()
{
  if (state == State::Paused)
    state = State::WaitingForResume;
}
//--------------------------------------------------------------
/* Default constructor */
Player::EventProcessing::EventProcessing(Player &pPlayer)
  : player(pPlayer)
{
  start();     // Start the event processing thread
}
//--------------------------------------------------------------
/* Default destructor */
Player::EventProcessing::~EventProcessing()
{
  stop();     // Stop any existing thread before starting a new one
}
//--------------------------------------------------------------
/* Start the event processing thread */
void Player::EventProcessing::start()
{
  // If the thread is already running, do nothing
  if (thread.joinable())
    return;

  // Start the event processing thread
  thread = std::jthread([&](const std::stop_token &stopToken)
                        { process(stopToken); });
}
//--------------------------------------------------------------
/* Stop the event processing thread */
void Player::EventProcessing::stop()
{
  thread.request_stop();
  player.messageNotify();
  if (thread.joinable())
    thread.join();
}
//--------------------------------------------------------------
/* Event processing loop */
void Player::EventProcessing::process(const std::stop_token &stopToken) const
{
  while (!stopToken.stop_requested())
  {
    player.syncWaitForMessage();

    while (player.getMessageCount() > 0)
    {
      auto message = player.popMessage();
      if (message)
      {
        const auto messageType = message->getMessageTypeID();

        switch (messageType)
        {
          case HashMessageType("remote.loadFile"):
          {
            player.on_remoteLoadFile(dynamic_pointer_cast<GenericMessageEvent>(message));
            break;
          }
          case HashMessageType("remote.setState"):
            player.on_remoteSetState(dynamic_pointer_cast<GenericMessageEvent>(message));
            break;
          default:
            break;
        }
      }
    }
  }
}
//--------------------------------------------------------------
/* Handle the "remote.loadFile" message */
void Player::on_remoteLoadFile(const std::shared_ptr<GenericMessageEvent> &message)
{
  // Sanity check: Ensure the message is valid
  if (!message)
    return;

  try
  {
    const auto filePath = std::any_cast<std::string>(message->m_payload);
    loadFile(filePath);
  }
  catch (const std::exception &e)
  {
    emit<LogMessage>(LogMessage::LogLevel::Error, "Failed to load file: " + std::string(e.what()));
  }
}
//--------------------------------------------------------------
/* Handle the "remote.setState" message */
void Player::on_remoteSetState(const std::shared_ptr<GenericMessageEvent> &message)
{
  // Sanity check: Ensure the message is valid
  if (!message)
    return;

  try
  {
    const auto command = std::any_cast<Command>(message->m_payload);
    switch (command)
    {
      case Command::Pause:
      {
        // Handle paused state
        m_playback.pause();
        emit<LogMessage>(LogMessage::LogLevel::Info, "Player paused");
        break;
      }
      case Command::Play:
      {
        // Handle playing state
        if (m_playback.state == State::Paused)
        {
          m_playback.resume();
          emit<LogMessage>(LogMessage::LogLevel::Info, "Player resumed");
        }
        else if (m_playback.state == State::Stopped)
        {
          m_playback.startNew();
          emit<LogMessage>(LogMessage::LogLevel::Info, "Player playing");
        }
        break;
      }
      case Command::Stop:
      {
        // Handle stopped state
        m_playback.reset();
        emit<LogMessage>(LogMessage::LogLevel::Info, "Player stopped");
        break;
      }
    }
  }
  catch (const std::exception &e)
  {
    emit<LogMessage>(LogMessage::LogLevel::Error, "Failed to set state: " + std::string(e.what()));
  }
}
//--------------------------------------------------------------
