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

#include "thalia/metadata_queue.h"
#include "thalia/thalia_plugin.h"
#include "thalia/playlist.h"
#include "thalia/main_wnd.h"
#include "thalia/plugin.h"
#include "thalia/app_info.h"
#include "third_party/sqlite/sqlite3.h"

static void delete_str(WDL_FastString *p) { delete p; }

THA_MetadataQueue::THA_MetadataQueue()
  : m_killthread(false)
  , m_metadata(true, delete_str)
  , m_save(false)
{
  StartThread();
}

THA_MetadataQueue::~THA_MetadataQueue()
{
  Finalize();
  StopThread();
  m_filepaths.Empty(true);
  m_queuedfilepaths.Empty(true);
}

void THA_MetadataQueue::AddFile(const char *filepath)
{
  if (strncmp(filepath, "http", 4))
  {
    m_filepaths.Add(new WDL_FastString(filepath));
  }
}

int THA_MetadataQueue::GetFilePathCount() const
{
  return m_filepaths.GetSize();
}

bool THA_MetadataQueue::HasFilesInQueue() const
{
  return m_queuedfilepaths.GetSize() > 0;
}

void THA_MetadataQueue::GetMetadata(WDL_StringKeyedArray<WDL_FastString *> **metadata)
{
  m_metadata.DeleteAll(false);
  if (GetFilePathCount() == 1)
  {
    THA_IFileTag *ft = CreateEditFileTag(m_filepaths.Get(0)->Get());
    if (ft)
    {
      m_metadata.Insert("Title", new WDL_FastString(ft->GetTitle()));
      m_metadata.Insert("Artist", new WDL_FastString(ft->GetArtist()));
      m_metadata.Insert("Album", new WDL_FastString(ft->GetAlbum()));
      m_metadata.Insert("Genre", new WDL_FastString(ft->GetGenre()));
      m_metadata.Insert("Track", new WDL_FastString(ft->GetTrack()));
      m_metadata.Insert("Year", new WDL_FastString(ft->GetYear()));
      m_metadata.Insert("Comment", new WDL_FastString(ft->GetComment()));
      delete ft;
    }
  }

  *metadata = &m_metadata;
}

void THA_MetadataQueue::UpdateMetadata(const char *key, const char *val)
{
  m_metadata.Insert(key, new WDL_FastString(val));
}

void THA_MetadataQueue::Save()
{
  char text[2048];
  int subitem = g_mainwnd->GetHeaderSubitem(ID_LIBRARY_HEADER_FILEPATH);
  for (int i = 0; i < m_filepaths.GetSize(); i++)
  {
    WDL_FastString *fp = m_filepaths.Get(i);
    HWND list = GetDlgItem(g_mainwnd->Handle(), IDC_LIST1);
    WDL_UTF8_HookListView(list);
    for (int j = 0; j < ListView_GetItemCount(list); j++)
    {
      ListView_GetItemText(list, j, subitem, text, sizeof(text));
      if (!wdl_filename_cmp(fp->Get(), text))
      {
        g_playlist->WantPlayList();
        int titlesubitem = g_mainwnd->GetHeaderSubitem(ID_LIBRARY_HEADER_TITLE);
        int artistsubitem = g_mainwnd->GetHeaderSubitem(ID_LIBRARY_HEADER_ARTIST);
        int albumsubitem = g_mainwnd->GetHeaderSubitem(ID_LIBRARY_HEADER_ALBUM);
        int genresubitem = g_mainwnd->GetHeaderSubitem(ID_LIBRARY_HEADER_GENRE);
        int commentsubitem = g_mainwnd->GetHeaderSubitem(ID_LIBRARY_HEADER_COMMENT);
        WDL_PtrList<THA_PlayList::PlayListEntry> *pl = g_playlist->GetPlayList();
        THA_PlayList::PlayListEntry *ple = pl->Get(j);
        WDL_FastString *val;
        val = m_metadata.Get("Title", NULL);
        if (val)
        {
          if (strlen(val->Get()))
          {
            ple->title.Set(val->Get());
            ListView_SetItemText(list, j, titlesubitem, (char *)val->Get());
          }
          else
          {
            m_strbuf.Set(ple->filename.Get());
            m_strbuf.remove_fileext();
            ple->title.Set(m_strbuf.Get());
            ListView_SetItemText(list, j, titlesubitem, (char *)m_strbuf.Get());
          }
        }
        val = m_metadata.Get("Artist", NULL);
        if (val)
        {
          ple->artist.Set(val->Get());
          ListView_SetItemText(list, j, artistsubitem, (char *)val->Get());
        }
        val = m_metadata.Get("Album", NULL);
        if (val)
        {
          ple->album.Set(val->Get());
          ListView_SetItemText(list, j, albumsubitem, (char *)val->Get());
        }
        val = m_metadata.Get("Genre", NULL);
        if (val)
        {
          ple->genre.Set(val->Get());
          ListView_SetItemText(list, j, genresubitem, (char *)val->Get());
        }
        val = m_metadata.Get("Comment", NULL);
        if (val)
        {
          ple->comment.Set(val->Get());
          ListView_SetItemText(list, j, commentsubitem, (char *)val->Get());
        }
        g_playlist->DoneWithPlayList();
      }
    }
  }

  for (int i = 0; i < m_filepaths.GetSize(); i++)
  {
    m_queuedfilepaths.Add(m_filepaths.Get(i));
  }
  m_filepaths.Empty(false);
  m_save = true;
}

void THA_MetadataQueue::Abort()
{
  m_filepaths.Empty(true);
}

void THA_MetadataQueue::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 = THREAD_PRIORITY_NORMAL;
    SetThreadPriority(rt[i].thread, priority);
  }
}

void THA_MetadataQueue::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);
  }
}

void THA_MetadataQueue::Finalize()
{
  WDL_MutexLock lock(&m_mutex);
  while (m_queuedfilepaths.GetSize())
  {
    WDL_FastString *fp = m_queuedfilepaths.Get(0);

    THA_IFileTag *ft = CreateEditFileTag(fp->Get());
    if (ft)
    {
      WDL_FastString *val;
      val = m_metadata.Get("Title", NULL);
      if (val) ft->SetTitle(val->Get());
      val = m_metadata.Get("Artist", NULL);
      if (val) ft->SetArtist(val->Get());
      val = m_metadata.Get("Album", NULL);
      if (val) ft->SetAlbum(val->Get());
      val = m_metadata.Get("Genre", NULL);
      if (val) ft->SetGenre(val->Get());
      val = m_metadata.Get("Track", NULL);
      if (val) ft->SetTrack(val->Get());
      val = m_metadata.Get("Year", NULL);
      if (val) ft->SetYear(val->Get());
      val = m_metadata.Get("Comment", NULL);
      if (val) ft->SetComment(val->Get());

      if (ft->Save())
      {
        int rc = 0;
        char *errmsg = NULL;
        sqlite3 *db = NULL;
        sqlite3_stmt *stmt = NULL;

        WDL_FastString fn(g_setpath.Get());
        fn.Append(THA_NAME ".db");

        rc = sqlite3_open(fn.Get(), &db);
        if (WDL_NOT_NORMALLY(rc))
        {
          wdl_log("cannot open sqlite database\n");
          sqlite3_close(db);
          db = NULL;
          return;
        }

        const char *sql_update_metadata =
          "UPDATE metadata "
          "SET title = ?1,"
          "artist = ?2,"
          "album = ?3,"
          "genre = ?4,"
          "comment = ?5 "
          "WHERE "
          "file_path = ?6;";

        WDL_FastString sqlstr(sql_update_metadata);
        rc = sqlite3_prepare_v2(db, sqlstr.Get(), -1, &stmt, NULL);
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot prepare statement %s\n", sqlstr.Get());
          sqlite3_free(errmsg);
          sqlite3_close(db);
          db = NULL;
        }

        val = m_metadata.Get("Title", NULL);
        if (val && strlen(val->Get()))
        {
          rc = sqlite3_bind_text(stmt, 1, val->Get(), -1, NULL);
          if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
          {
            wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
          }
        }
        else if (strlen(ft->GetTitle()))
        {
          rc = sqlite3_bind_text(stmt, 1, ft->GetTitle(), -1, NULL);
          if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
          {
            wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
          }
        }
        else
        {
          m_strbuf.Set(ft->GetFileName());
          m_strbuf.remove_fileext();
          rc = sqlite3_bind_text(stmt, 1, m_strbuf.Get(), -1, NULL);
          if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
          {
            wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
          }
        }

        val = m_metadata.Get("Artist", NULL);
        rc = sqlite3_bind_text(stmt, 2, val ? val->Get() : ft->GetArtist(), -1, NULL);
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
        }

        val = m_metadata.Get("Album", NULL);
        rc = sqlite3_bind_text(stmt, 3, val ? val->Get() : ft->GetAlbum(), -1, NULL);
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
        }

        val = m_metadata.Get("Genre", NULL);
        rc = sqlite3_bind_text(stmt, 4, val ? val->Get() : ft->GetGenre(), -1, NULL);
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
        }

        val = m_metadata.Get("Comment", NULL);
        rc = sqlite3_bind_text(stmt, 5, val ? val->Get() : ft->GetComment(), -1, NULL);
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
        }

        rc = sqlite3_bind_text(stmt, 6, fp->Get(), -1, NULL);
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
        }

        rc = sqlite3_step(stmt);
        if (WDL_NOT_NORMALLY(rc != SQLITE_DONE))
        {
          wdl_log("cannot insert statement %s\n", sqlite3_errmsg(db));
        }

        rc = sqlite3_reset(stmt);
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
        }

        if (stmt)
        {
          sqlite3_finalize(stmt); stmt = NULL;
        }

        if (db)
        {
          sqlite3_close(db); db = NULL;
        }
      }

      delete ft;
    }
    m_queuedfilepaths.Delete(0, true);
  }
  m_save = false;
}

int THA_MetadataQueue::Run()
{
  if (m_save)
  {
    if (m_queuedfilepaths.GetSize())
    {
      WDL_FastString *fp = m_queuedfilepaths.Get(0);

      THA_PlayList::ActiveEntry ae;
      g_playlist->GetActiveEntry(&ae);
      if (!wdl_filename_cmp(fp->Get(), ae.filepath.Get()))
      {
        m_queuedfilepaths.Add(fp);
        m_queuedfilepaths.Delete(0, false);
        return 1;
      }

      THA_IFileTag *ft = CreateEditFileTag(fp->Get());
      if (ft)
      {
        WDL_FastString *val;
        val = m_metadata.Get("Title", NULL);
        if (val) ft->SetTitle(val->Get());
        val = m_metadata.Get("Artist", NULL);
        if (val) ft->SetArtist(val->Get());
        val = m_metadata.Get("Album", NULL);
        if (val) ft->SetAlbum(val->Get());
        val = m_metadata.Get("Genre", NULL);
        if (val) ft->SetGenre(val->Get());
        val = m_metadata.Get("Track", NULL);
        if (val) ft->SetTrack(val->Get());
        val = m_metadata.Get("Year", NULL);
        if (val) ft->SetYear(val->Get());
        val = m_metadata.Get("Comment", NULL);
        if (val) ft->SetComment(val->Get());

        if (ft->Save())
        {
          int rc = 0;
          char *errmsg = NULL;
          sqlite3 *db = NULL;
          sqlite3_stmt *stmt = NULL;

          WDL_FastString fn(g_setpath.Get());
          fn.Append(THA_NAME ".db");

          rc = sqlite3_open(fn.Get(), &db);
          if (WDL_NOT_NORMALLY(rc))
          {
            wdl_log("cannot open sqlite database\n");
            sqlite3_close(db);
            db = NULL;
            return 1;
          }

          const char *sql_update_metadata =
            "UPDATE metadata "
            "SET title = ?1,"
            "artist = ?2,"
            "album = ?3,"
            "genre = ?4,"
            "comment = ?5 "
            "WHERE "
            "file_path = ?6;";

          WDL_FastString sqlstr(sql_update_metadata);
          rc = sqlite3_prepare_v2(db, sqlstr.Get(), -1, &stmt, NULL);
          if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
          {
            wdl_log("cannot prepare statement %s\n", sqlstr.Get());
            sqlite3_free(errmsg);
            sqlite3_close(db);
            db = NULL;
          }

          val = m_metadata.Get("Title", NULL);
          if (val && strlen(val->Get()))
          {
            rc = sqlite3_bind_text(stmt, 1, val->Get(), -1, NULL);
            if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
            {
              wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
            }
          }
          else if (strlen(ft->GetTitle()))
          {
            rc = sqlite3_bind_text(stmt, 1, ft->GetTitle(), -1, NULL);
            if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
            {
              wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
            }
          }
          else
          {
            m_strbuf.Set(ft->GetFileName());
            m_strbuf.remove_fileext();
            rc = sqlite3_bind_text(stmt, 1, m_strbuf.Get(), -1, NULL);
            if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
            {
              wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
            }
          }

          val = m_metadata.Get("Artist", NULL);
          rc = sqlite3_bind_text(stmt, 2, val ? val->Get() : ft->GetArtist(), -1, NULL);
          if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
          {
            wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
          }

          val = m_metadata.Get("Album", NULL);
          rc = sqlite3_bind_text(stmt, 3, val ? val->Get() : ft->GetAlbum(), -1, NULL);
          if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
          {
            wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
          }

          val = m_metadata.Get("Genre", NULL);
          rc = sqlite3_bind_text(stmt, 4, val ? val->Get() : ft->GetGenre(), -1, NULL);
          if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
          {
            wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
          }

          val = m_metadata.Get("Comment", NULL);
          rc = sqlite3_bind_text(stmt, 5, val ? val->Get() : ft->GetComment(), -1, NULL);
          if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
          {
            wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
          }

          rc = sqlite3_bind_text(stmt, 6, fp->Get(), -1, NULL);
          if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
          {
            wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
          }

          rc = sqlite3_step(stmt);
          if (WDL_NOT_NORMALLY(rc != SQLITE_DONE))
          {
            wdl_log("cannot insert statement %s\n", sqlite3_errmsg(db));
          }

          rc = sqlite3_reset(stmt);
          if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
          {
            wdl_log("cannot bind value %s\n", sqlite3_errmsg(db));
          }

          if (stmt)
          {
            sqlite3_finalize(stmt); stmt = NULL;
          }

          if (db)
          {
            sqlite3_close(db); db = NULL;
          }
        }

        delete ft;
      }
      m_queuedfilepaths.Delete(0, true);
    }
    else
    {
      m_save = false;
      if (g_mainwnd) g_mainwnd->Search();
    }
  }

  return 1;
}

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

  THA_MetadataQueue *self = (THA_MetadataQueue *)arg;

  if (WDL_NORMALLY(self))
  {
    int sleepstep = 50;

    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;
}
