// Copyright (c) 2021 Giorgos Vougioukas
//
// The license can be found in the LICENSE file.

#include "thalia/render_system.h"
#include "thalia/concurrency.h"
#include "thalia/preferences.h"
#include "thalia/playlist.h"

#include "WDL/fpcmp.h"
#include "WDL/db2val.h"

THA_RenderSystem::THA_RenderSystem()
  : m_killthread(false)
  , m_running(false)
  , m_vol(0.0)
{}

THA_RenderSystem::~THA_RenderSystem()
{
  if (IsRunning()) StopThread();
  m_tracks.Empty(true);
}

void THA_RenderSystem::AddTrack(THA_Track *track)
{
  WDL_MutexLock lock(&m_mutex);
  m_tracks.Empty(true);
  m_tracks.Add(track);
}

void THA_RenderSystem::DeleteAllTracks()
{
  WDL_MutexLock lock(&m_mutex);
  m_tracks.Empty(true);
  m_time_elapsed.Set("");
  m_time_remaining.Set("");
}

void THA_RenderSystem::StartThread()
{
  WDL_ASSERT(!m_threads.GetSize());

  int priority;
  m_killthread = false;

  m_threads.Resize(1);
  //m_threads.Resize(THA_GetCPUCores());
  for (int i = 0; i < m_threads.GetSize(); i++)
  {
    ThreadDetails *rt = m_threads.Get();
    rt[i].thread = (HANDLE)_beginthreadex(NULL, 0,
      ThreadFunction, (void *)this, 0, &rt[i].id);

    priority = g_preferences->GetRenderSystemPriority();
    SetThreadPriority(rt[i].thread, priority);
  }

  m_running = true;
}

void THA_RenderSystem::StopThread()
{
  m_killthread = true;
  for (int i = 0; i < m_threads.GetSize(); i++)
  {
    ThreadDetails *rt = m_threads.Get();
    WaitForSingleObject(rt[i].thread, INFINITE);
    CloseHandle(rt[i].thread);
    m_threads.Resize(0);
  }

  m_running = false;
}

bool THA_RenderSystem::IsRunning()
{
  WDL_MutexLock lock(&m_mutex);
  return m_running;
}

bool THA_RenderSystem::IsActive()
{
  WDL_MutexLock lock(&m_mutex);
  return m_running && m_tracks.GetSize() > 0;
}

bool THA_RenderSystem::IsStreaming()
{
  WDL_MutexLock lock(&m_mutex);
  if (m_tracks.GetSize())
  {
    return m_tracks.Get(0)->IsStreaming();
  }

  return false;
}

const char *THA_RenderSystem::GetTime(bool remaining) const
{
  if (WDL_unlikely(remaining)) return m_time_remaining.Get();
  else return m_time_elapsed.Get();
}

const char *THA_RenderSystem::GetStreamInfo() const
{
  if (m_tracks.GetSize())
  {
    return m_tracks.Get(0)->GetStreamInfo();
  }
  return "";
}

void THA_RenderSystem::AudioStreamerData(WDL_TypedBuf<SAM> *output, int frame_count, int channels)
{
  //WDL_MutexLock lock(&m_mutex);
  memset(output->Get(), 0, frame_count * sizeof(SAM));

  if (m_tracks.GetSize())
  {
    int written = 0;
    BQ_Block *blk = NULL;

    THA_Track *trk = m_tracks.Get(0);

    while (written < frame_count)
    {
      if (trk && trk->m_bq.GetBlock(&blk))
      {
        if (WDL_DefinitelyGreaterThan(blk->time, 0.0))
        {
          int hours = 0;
          int minutes = 0;
          int seconds = 0;

          minutes = (int)blk->time / 60;
          seconds = (int)blk->time % 60;
          hours = minutes / 60;
          minutes = minutes % 60;

          if (hours)
          {
            m_time_elapsed.SetFormatted(64, "%01d:%02d:%02d", hours, minutes, seconds);
          }
          else
          {
            m_time_elapsed.SetFormatted(64, "%02d:%02d", minutes, seconds);
          }

          if (WDL_DefinitelyGreaterThan(trk->GetLength(), 0.0))
          {
            double remaining = trk->GetLength() - blk->time;

            minutes = (int)remaining / 60;
            seconds = (int)remaining % 60;
            hours = minutes / 60;
            minutes = minutes % 60;

            if (hours)
            {
              m_time_remaining.SetFormatted(64, "-%01d:%02d:%02d", hours, minutes, seconds);
            }
            else
            {
              m_time_remaining.SetFormatted(64, "-%02d:%02d", minutes, seconds);
            }
          }
          else
          {
            m_time_remaining.Set(m_time_elapsed.Get());
          }
        }
        else
        {
          m_time_elapsed.SetFormatted(64, "%02d:%02d", 0, 0);
          m_time_remaining.SetFormatted(64, "-%02d:%02d", 0, 0);
        }

        if (blk->samq.Available() / trk->GetChannels() * channels > frame_count - written)
        {
          const int sz = frame_count - written;

          if (trk->GetChannels() <= channels)
          {
            for (int i = 0; i < sz; i += channels)
            {
              WDL_ASSERT(blk->samq.GetSize() >= trk->GetChannels());
              for (int j = 0; j < trk->GetChannels(); j++)
              {
                blk->samq.Get()[j] *= DB2VAL(GetVolume());
                output->Get()[written + i + j] = blk->samq.Get()[j];
              }
              blk->samq.Advance(trk->GetChannels());
            }
          }
          else
          {
            for (int i = 0; i < sz; i += channels)
            {
              WDL_ASSERT(blk->samq.GetSize() >= channels);
              for (int j = 0; j < channels; j++)
              {
                blk->samq.Get()[j] *= DB2VAL(GetVolume());
                output->Get()[written + i + j] = blk->samq.Get()[j];
              }
              blk->samq.Advance(trk->GetChannels());
            }
          }

          written += sz;

          blk->samq.Compact();
          if (blk->samq.Available()) trk->m_bq.ReturnBlock(blk);
          else trk->m_bq.DisposeBlock(blk);
        }
        else
        {
          int sz = blk->samq.GetSize() / trk->GetChannels() * channels;

          if (trk->GetChannels() <= channels)
          {
            for (int i = 0; i < sz; i += channels)
            {
              WDL_ASSERT(blk->samq.GetSize() >= trk->GetChannels());
              for (int j = 0; j < trk->GetChannels(); j++)
              {
                blk->samq.Get()[j] *= DB2VAL(GetVolume());
                output->Get()[written + i + j] = blk->samq.Get()[j];
              }
              blk->samq.Advance(trk->GetChannels());
            }
          }
          else
          {
            for (int i = 0; i < sz; i += channels)
            {
              WDL_ASSERT(blk->samq.GetSize() >= channels);
              for (int j = 0; j < channels; j++)
              {
                blk->samq.Get()[j] *= DB2VAL(GetVolume());
                output->Get()[written + i + j] = blk->samq.Get()[j];
              }
              blk->samq.Advance(trk->GetChannels());
            }
          }

          written += sz;

          WDL_ASSERT(!blk->samq.GetSize());

          blk->samq.Clear();
          trk->m_bq.DisposeBlock(blk);
        }
      }
      else
      {
        break;
      }
    }
  }
}

int THA_RenderSystem::Run()
{
  if (m_tracks.GetSize())
  {
    THA_Track *trk = m_tracks.Get(0);

    if (trk->IsDrained())
    {
      WDL_FastString trkfp(g_playlist->GetNextActiveFilepath());
      if (trkfp.GetLength())
      {
        THA_Track *newtrk = new WDL_NEW THA_Track;
        if (newtrk)
        {
          if (newtrk->Open(trkfp.Get()))
          {
            m_tracks.Add(newtrk);
            if (g_audiostreamer->IsRunning())
            {
              g_audiostreamer->Stop();
              m_tracks.Delete(0, true);
            }
            g_audiostreamer->Start(THA_AudioOnSamples);
            WDL_ASSERT(m_tracks.GetSize() == 1);
          }
        }
      }
      else
      {
        if (g_audiostreamer->IsRunning())
        {
          g_audiostreamer->Stop();
        }
        DeleteAllTracks();
      }

      return 0;
    }

    while (trk->WantMore())
    {
      WDL_ASSERT(m_tracks.GetSize() == 1);
      if (!trk->DoBuffering()) break;
    }
  }

  return 1;
}

unsigned int WINAPI THA_RenderSystem::ThreadFunction(void *arg)
{
#if defined(_WIN32)
  CoInitialize(NULL);
#endif

  THA_RenderSystem *self = (THA_RenderSystem *)arg;

  if (WDL_NORMALLY(self))
  {
    int sleepstep = g_preferences->GetRenderSystemSleepStep();

    self->m_killthread = false;

    while (!self->m_killthread)
    {
      self->m_mutex.Enter();
      while (!self->Run());
      self->m_mutex.Leave();
      Sleep(sleepstep);
    }
  }

#if defined(_WIN32)
  CoUninitialize();
#endif

  return 0;
}
