#include "Track.h"

#include <choc/audio/choc_MIDI.h>
#include <choc/audio/choc_MIDIFile.h>
#include <fstream>
#include <iostream>
#include <ranges>
#include <syncstream>
#include <unordered_map>

using namespace std;
using namespace track;
//--------------------------------------------------------------
/* Log an informational message */
void Track::logInfo(const std::string &message) const
{
  osyncstream(cout) << message << std::endl;
}
//--------------------------------------------------------------
/* Log an error message */
void Track::logError(const std::string &message) const
{
  osyncstream(cerr) << message << std::endl;
}
//--------------------------------------------------------------
/* Load a MIDI file from disk */
void Track::loadFromFile(const std::filesystem::path &filePath)
{
  cout << "Loading MIDI file: " << filePath.string() << endl;

  // Open the MIDI file in binary mode and place the file pointer at the end to determine its size
  std::ifstream file(filePath, std::ifstream::binary | std::ifstream::ate);
  if (!file.is_open())
    throw runtime_error("Failed to open MIDI file: " + filePath.string());

  const auto fileSize = file.tellg();
  file.seekg(0, std::ios::beg);

  // Read the entire file into a buffer
  std::vector<uint8_t> buffer(fileSize);
  if (!file.read(reinterpret_cast<char *>(buffer.data()), fileSize))
    throw runtime_error("Failed to read MIDI file: " + filePath.string());

  // Load the MIDI data from memory
  return loadFromMemory(buffer);
}
//--------------------------------------------------------------
/* Load a MIDI file from memory */
void Track::loadFromMemory(const std::span<uint8_t> midiBytes)
{
  logInfo("Loading MIDI data from memory, size: " + std::to_string(midiBytes.size()) + " bytes");

  try
  {
    // Parse the MIDI data from memory using choc::midi::File
    choc::midi::File midiFile;
    midiFile.load(midiBytes.data(), midiBytes.size());

    // Prepare structures to store notes and active notes
    NotesBuffer                          notes;
    std::unordered_map<uint16_t, size_t> activeNotes;

    // Lambda functions to handle enabling and disabling notes based on MIDI messages
    auto makeKey = [](const choc::midi::ShortMessage &message) -> uint16_t
    {
      return static_cast<uint16_t>((message.getChannel0to15() << 8) | message.getNoteNumber().note);
    };
    const auto disableNote = [makeKey, &notes, &activeNotes](const choc::midi::ShortMessage &message, const Seconds time)
    {
      // Create a unique key for the note based on channel and pitch
      const uint16_t keyNote = makeKey(message);

      // If the note is not active, return early
      const auto it = activeNotes.find(keyNote);
      if (it == activeNotes.end())
        return;

      // Update the end timestamp of the note and remove it from the active notes map
      notes[it->second].endTime = time;
      activeNotes.erase(it);
    };
    const auto enableNote = [makeKey, disableNote, &notes, &activeNotes](const choc::midi::ShortMessage &message, const Seconds time)
    {
      // Create a unique key for the note based on channel and pitch
      const uint16_t keyNote = makeKey(message);

      // If the velocity is zero, treat it as a Note Off event and disable the note
      if (message.getVelocity() == 0)
      {
        disableNote(message, time);
        return;
      }

      // If the note is already active, disable it before enabling it again
      if (activeNotes.contains(keyNote))
        disableNote(message, time);

      // Enable note
      const auto note = Note{
        .channel   = message.getChannel0to15(),
        .pitch     = message.getNoteNumber().note,
        .velocity  = message.getVelocity(),
        .name      = string(message.getNoteNumber().getNameWithSharps()),
        .octave    = message.getNoteNumber().getOctaveNumber(),
        .frequency = message.getNoteNumber().getFrequency(),
        .startTime = time,
        .endTime   = time
      };
      notes.push_back(note);
      activeNotes[keyNote] = notes.size() - 1;
    };

    // Iterate over all events in the MIDI file and print their details
    Seconds lastTime;
    midiFile.iterateEvents([this, enableNote, disableNote, &lastTime](const choc::midi::MessageView &message, const double timeInSeconds)
                           {
      // Update the last event time
      lastTime = Seconds(timeInSeconds);

      // Process short messages (Note On, Note Off, etc.)
      if (message.isShortMessage())
      {
        if (message.isNoteOn())
        {
          // Enable note
          enableNote(message, Seconds(timeInSeconds));
        }

        else if (message.isNoteOff())
        {
          // Disable note
          disableNote(message, Seconds(timeInSeconds));
        }

        else
        {
          // Handle other short messages if needed
        }
      } });

    // After processing all events, ensure that any remaining active notes are properly closed
    for (const auto &noteIndex : activeNotes | views::values)
      notes[noteIndex].endTime = lastTime;

    // Store the parsed notes in the track's data structure and log statistics
    {
      m_notes = std::move(notes);

      ostringstream logMessage;
      logMessage << "MIDI parsing completed\n"
                 << "  Total notes: " << m_notes.size() << "\n"
                 << "  Total duration: " << lastTime;
      logInfo(logMessage.str());
    }
  }
  catch (const std::exception &e)
  {
    logError(std::string("Unexpected error while parsing MIDI data: ") + e.what());
  }
}
//--------------------------------------------------------------
/* Check if a MIDI file is loaded */
bool Track::isEmpty() const
{
  return m_notes.empty();
}
//--------------------------------------------------------------
/* Get the duration of the track */
Seconds Track::getDuration() const
{
  if (m_notes.empty())
    return Seconds(0);

  const auto &lastNote = m_notes.back();
  return lastNote.endTime;     // Return the end timestamp of the last note
}
//--------------------------------------------------------------
/* Debug function to print the track data */
void Track::debug() const
{
  for (const auto &note : m_notes)
  {
    logInfo(std::format(
      "Note: channel={:<2} | note={:<3} | startTime={:>7.3f}s | duration={:>7.3f}s | frequency={:>7.2f} Hz",
      note.channel,
      std::format("{}{}", note.name, note.octave),
      note.startTime.count(),
      (note.endTime - note.startTime).count(),
      note.frequency));
  }
}
//--------------------------------------------------------------
///* Get a lock proxy to access the notes safely without copying the data */
//TrackLockProxy Track::getNotes() const
//{
//  // The TrackLockProxy is automatically moved when returned (NRVO)
//  return TrackLockProxy(m_notes.mtx, m_notes.list);
//}
////--------------------------------------------------------------
