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

#include "thalia/query.h"
#include "thalia/app_info.h"
#include "thalia/preferences.h"
#include "thalia/main_wnd.h"

#include "WDL/fpcmp.h"

#define THA_QUERY_NO_ACTION 0
#define THA_QUERY_SEARCH_DB 1

THA_Query::THA_Query()
  : m_thread(NULL)
  , m_killthread(false)
  , m_action(0)
  , m_open(false)
  , m_db(NULL)
  , m_stmt(NULL)
  , m_errmsg(NULL)
  , m_step(0)
  , m_querylimit(0)
  , m_artists(NULL)
  , m_wantartists(false)
  , m_matches(NULL)
  , m_wantmatches(false)
{
  m_querylimit = g_preferences->GetMediaLibraryQueryLimit();
  m_artists = new Artist(false);
  m_matches = new WDL_PtrList<WDL_FastString>;
}

THA_Query::~THA_Query()
{
  if (IsOpen()) Close();
  DeleteAllArtists();
  delete m_artists;

  m_matches->Empty(true);
  delete m_matches;
}

bool THA_Query::Open()
{
  int rc = 0;
  char *errmsg = NULL;

  m_fn.Set(g_setpath.Get());
  m_fn.Append(THA_NAME ".db");

  rc = sqlite3_open(m_fn.Get(), &m_db);

  if (WDL_NOT_NORMALLY(rc))
  {
    wdl_log("cannot open sqlite database\n");
    sqlite3_close(m_db);
    m_db = NULL;
    return false;
  }

  const char *sql_create_table =
    "CREATE TABLE IF NOT EXISTS metadata ("
    "type TEXT NOT NULL,"
    "title TEXT NOT NULL,"
    "artist TEXT NOT NULL,"
    "album TEXT NOT NULL,"
    "length REAL DEFAULT 0.0,"
    "genre TEXT NOT NULL,"
    "samplerate REAL DEFAULT 44100.0,"
    "file_name TEXT NOT NULL,"
    "file_path TEXT NOT NULL,"
    "file_ext TEXT NOT NULL,"
    "file_size INTEGER DEFAULT 0,"
    "comment TEXT NOT NULL);";

  rc = sqlite3_exec(m_db, sql_create_table, 0, 0, &errmsg);

  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot execute SQL command (%s)\n", errmsg);
    sqlite3_free(errmsg);
    sqlite3_close(m_db);
    m_db = NULL;
    return false;
  }

  unsigned int thread_id;
  m_thread = (HANDLE)_beginthreadex(NULL, 0,
    ThreadFunc, (void *)this, 0, &thread_id);

  if (WDL_NOT_NORMALLY(!m_thread))
  {
    wdl_log("cannot start database thread\n");
    sqlite3_close(m_db);
    m_thread = m_db = NULL;
    return false;
  }

  int priority = g_preferences->GetDiskIOPriority();
  SetThreadPriority(m_thread, priority);

  m_open = true;

  return true;
}

bool THA_Query::IsOpen() const
{
  return m_open;
}

void THA_Query::Close()
{
  WDL_MutexLock lock(&m_mutex);
  m_killthread = true;

  if (WDL_NORMALLY(m_thread))
  {
    WaitForSingleObject(m_thread, INFINITE);
    CloseHandle(m_thread);
    m_thread = NULL;
  }

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

  if (WDL_NORMALLY(m_db))
  {
    sqlite3_close(m_db);
    m_db = NULL;
  }

  m_open = false;
}

bool THA_Query::Query()
{
  WDL_MutexLock lock(&m_mutex);

  WDL_ASSERT(!m_stmt);

  m_querytext.Set("");
  m_querysql.Set("");

  //const char *sql =
  //  "SELECT * FROM metadata LIMIT ?1;";
    const char *sql =
    "SELECT * FROM metadata;";

  int rc = sqlite3_prepare_v2(m_db, sql, -1, &m_stmt, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot execute sqlite3_prepare_v2\n");
  }

  //rc = sqlite3_bind_int(m_stmt, 1, m_querylimit);
  //if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  //{
  //  wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  //}

  m_step = sqlite3_step(m_stmt);
  bool row = (m_step == SQLITE_ROW);
  LoadArtists();
  //m_action = THA_QUERY_SEARCH_DB;
  return row;
}

bool THA_Query::Query(const char *text)
{
  WDL_MutexLock lock(&m_mutex);

  WDL_ASSERT(!m_stmt);

  m_querytext.Set(text);
  m_querysql.Set(text);
  m_querysql.Insert("%", 0);
  m_querysql.Append("%");

  const char *sql =
    "SELECT * FROM metadata WHERE title LIKE ?1"
    " OR artist LIKE ?2 OR album LIKE ?3 LIMIT ?4;";

  int rc = sqlite3_prepare_v2(m_db, sql, -1, &m_stmt, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot execute sqlite3_prepare_v2\n");
  }

  rc = sqlite3_bind_text(m_stmt, 1, m_querysql.Get(), -1, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  rc = sqlite3_bind_text(m_stmt, 2, m_querysql.Get(), -1, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  rc = sqlite3_bind_text(m_stmt, 3, m_querysql.Get(), -1, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  rc = sqlite3_bind_int(m_stmt, 4, m_querylimit);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  m_step = sqlite3_step(m_stmt);
  bool row = (m_step == SQLITE_ROW);
  //m_action = THA_QUERY_SEARCH_DB;
  LoadArtists();
  return row;
}

bool THA_Query::Order(const char *col, bool asc)
{
  WDL_MutexLock lock(&m_mutex);

  WDL_FastString sql;

  if (m_querysql.GetLength())
  {
    if (WDL_likely(asc))
    {
      const char *asc =
        "SELECT * FROM metadata WHERE title LIKE ?1"
        " OR artist LIKE ?2 OR album LIKE ?3"
        " ORDER BY %s COLLATE NOCASE ASC LIMIT ?4;";
      sql.SetFormatted(2048, asc, col);
    }
    else
    {
      const char *desc =
        "SELECT * FROM metadata WHERE title LIKE ?1"
        " OR artist LIKE ?2 OR album LIKE ?3"
        " ORDER BY %s COLLATE NOCASE DESC LIMIT ?4;";
      sql.SetFormatted(2048, desc, col);
    }
  }
  else
  {
    if (WDL_likely(asc))
    {
      const char *asc =
        "SELECT * FROM metadata ORDER BY %s COLLATE NOCASE ASC LIMIT ?1;";
      sql.SetFormatted(2048, asc, col);
    }
    else
    {
      const char *desc =
        "SELECT * FROM metadata ORDER BY %s COLLATE NOCASE DESC LIMIT ?1;";
      sql.SetFormatted(2048, desc, col);
    }
  }

  WDL_ASSERT(!m_stmt);

  int rc = sqlite3_prepare_v2(m_db, sql.Get(), -1, &m_stmt, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot execute sqlite3_prepare_v2\n");
  }

  if (m_querysql.GetLength())
  {
    rc = sqlite3_bind_text(m_stmt, 1, m_querysql.Get(), -1, NULL);
    if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
    {
      wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
    }

    rc = sqlite3_bind_text(m_stmt, 2, m_querysql.Get(), -1, NULL);
    if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
    {
      wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
    }

    rc = sqlite3_bind_text(m_stmt, 3, m_querysql.Get(), -1, NULL);
    if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
    {
      wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
    }

    rc = sqlite3_bind_int(m_stmt, 4, m_querylimit);
    if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
    {
      wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
    }
  }
  else
  {
    rc = sqlite3_bind_int(m_stmt, 1, m_querylimit);
    if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
    {
      wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
    }
  }

  m_step = sqlite3_step(m_stmt);
  return (m_step == SQLITE_ROW);

  //m_action = THA_QUERY_SEARCH_DB;
}

const char *THA_Query::QueryText()
{
  WDL_MutexLock lock(&m_mutex);
  return m_querytext.Get();
}

bool THA_Query::HasRow()
{
  WDL_MutexLock lock(&m_mutex);
  bool row = (m_step == SQLITE_ROW);

  if (!row)
  {
    sqlite3_finalize(m_stmt); m_stmt = NULL;
  }

  return row;
}

const char *THA_Query::GetColumnText(int col)
{
  WDL_MutexLock lock(&m_mutex);

  if (m_stmt)
  {
    return (const char *)sqlite3_column_text(m_stmt, col);
  }

  return "";
}

double THA_Query::GetColumnDouble(int col)
{
  WDL_MutexLock lock(&m_mutex);

  if (m_stmt)
  {
    return sqlite3_column_double(m_stmt, col);
  }

  return 0.0;
}

WDL_INT64 THA_Query::GetColumnInt64(int col)
{
  WDL_MutexLock lock(&m_mutex);

  if (m_stmt)
  {
    return sqlite3_column_int64(m_stmt, col);
  }

  return WDL_INT64_CONST(0);
}

void THA_Query::NextStep()
{
  WDL_MutexLock lock(&m_mutex);
  m_step = sqlite3_step(m_stmt);
}

void THA_Query::LoadArtists()
{
  WDL_MutexLock lock(&m_mutex);

  DeleteAllArtists();

  while (HasRow())
  {
    Album *album = m_artists->Get(GetColumnText(2), NULL);
    if (!album) album = new Album(false);

    Title *title = album->Get(GetColumnText(3), NULL);
    if (!title) title = new Title;

    bool found = false;

    for (int i = 0; i < title->GetSize(); i++)
    {
      if (!strcmp(GetColumnText(1), title->Get(i)->title.Get())) found = true;
    }

    if (!found)
    {
      TitlePath *tp = new TitlePath;
      tp->title.Set(GetColumnText(1));
      tp->path.Set(GetColumnText(8));
      title->InsertSorted(tp, titlecmp);
    }

    album->Insert(GetColumnText(3), title);
    m_artists->Insert(GetColumnText(2), album);

    NextStep();
  }

  m_wantartists = true;
}

bool THA_Query::WantArtists()
{
  m_mutex.Enter();
  if (m_wantartists)
  {
    HWND tree = GetDlgItem(g_mainwnd->Handle(), IDC_TREE1);
    SendMessage(tree, WM_SETREDRAW, FALSE, 0);
  }

  return m_wantartists;
}

void THA_Query::DoneWithArtists()
{
  if (m_wantartists)
  {
    HWND tree = GetDlgItem(g_mainwnd->Handle(), IDC_TREE1);
    SendMessage(tree, WM_SETREDRAW, TRUE, 0);
  }
  m_wantartists = false;
  m_mutex.Leave();
}

THA_Query::Artist *THA_Query::GetArtists()
{
  WDL_MutexLock lock(&m_mutex);
  return m_artists;
}

void THA_Query::GetPlayListEntry(const char *filepath,
  WDL_PtrList<THA_PlayList::PlayListEntry> *entry)
{
  WDL_MutexLock lock(&m_mutex);

  WDL_ASSERT(!m_stmt);

  m_querysql.Set(filepath);
  //m_querysql.Insert("%", 0);
  //m_querysql.Append("%");

  const char *sql =
    "SELECT * FROM metadata WHERE file_path = ?1 LIMIT ?2;";

  int rc = sqlite3_prepare_v2(m_db, sql, -1, &m_stmt, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot execute sqlite3_prepare_v2\n");
  }

  rc = sqlite3_bind_text(m_stmt, 1, m_querysql.Get(), -1, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  rc = sqlite3_bind_int(m_stmt, 2, 1);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  m_step = sqlite3_step(m_stmt);

  if (HasRow())
  {
    THA_PlayList::PlayListEntry *ple =
      new THA_PlayList::PlayListEntry;
    ple->type.Set(GetColumnText(0));
    ple->title.Set(GetColumnText(1));
    ple->artist.Set(GetColumnText(2));
    ple->album.Set(GetColumnText(3));
    ple->length = GetColumnDouble(4);
    ple->genre.Set(GetColumnText(5));
    ple->srate = GetColumnDouble(6);
    ple->filename.Set(GetColumnText(7));
    ple->filepath.Set(GetColumnText(8));
    ple->fileext.Set(GetColumnText(9));
    ple->filesize = GetColumnInt64(10);
    ple->comment.Set(GetColumnText(11));
    entry->Add(ple);
  }

  sqlite3_finalize(m_stmt); m_stmt = NULL;
}

void THA_Query::GetUserPlayListEntry(const char *filepath,
  WDL_PtrList<THA_PlayList::PlayListEntry> *entry)
{
  WDL_MutexLock lock(&m_mutex);

  WDL_ASSERT(!m_stmt);

  //m_querysql.Set(filepath);
  //m_querysql.Insert("%", 0);
  //m_querysql.Append("%");

  const char *sql =
    "SELECT * FROM playlists WHERE file_path = ?1 LIMIT ?2;";

  int rc = sqlite3_prepare_v2(m_db, sql, -1, &m_stmt, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot execute sqlite3_prepare_v2\n");
  }

  rc = sqlite3_bind_text(m_stmt, 1, filepath, -1, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  rc = sqlite3_bind_int(m_stmt, 2, 1);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  m_step = sqlite3_step(m_stmt);

  if (HasRow())
  {
    THA_PlayList::PlayListEntry *ple =
      new THA_PlayList::PlayListEntry;
    ple->type.Set(GetColumnText(0));
    ple->title.Set(GetColumnText(1));
    ple->artist.Set(GetColumnText(2));
    ple->album.Set(GetColumnText(3));
    ple->length = GetColumnDouble(4);
    ple->genre.Set(GetColumnText(5));
    ple->srate = GetColumnDouble(6);
    ple->filename.Set(GetColumnText(7));
    ple->filepath.Set(GetColumnText(8));
    ple->fileext.Set(GetColumnText(9));
    ple->filesize = GetColumnInt64(10);
    ple->comment.Set(GetColumnText(11));
    entry->Add(ple);
  }

  sqlite3_finalize(m_stmt); m_stmt = NULL;
}

bool THA_Query::QueryForPlayList(const char *text)
{
  WDL_MutexLock lock(&m_mutex);

  WDL_ASSERT(!m_stmt);

  m_querytext.Set(text);
  m_querysql.Set(text);
  m_querysql.Insert("%", 0);
  m_querysql.Append("%");

  const char *sql =
    "SELECT * FROM metadata WHERE title LIKE ?1"
    " OR artist LIKE ?2 OR album LIKE ?3 LIMIT ?4;";

  int rc = sqlite3_prepare_v2(m_db, sql, -1, &m_stmt, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot execute sqlite3_prepare_v2\n");
  }

  rc = sqlite3_bind_text(m_stmt, 1, m_querysql.Get(), -1, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  rc = sqlite3_bind_text(m_stmt, 2, m_querysql.Get(), -1, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  rc = sqlite3_bind_text(m_stmt, 3, m_querysql.Get(), -1, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  rc = sqlite3_bind_int(m_stmt, 4, m_querylimit);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  m_step = sqlite3_step(m_stmt);
  bool row = (m_step == SQLITE_ROW);

  if (row)
  {
    LoadPlayListMatches();
    return row;
  }

  sqlite3_reset(m_stmt);

  const char *sqlpl =
    "SELECT * FROM playlists WHERE title LIKE ?1"
    " OR artist LIKE ?2 OR album LIKE ?3 LIMIT ?4;";

  rc = sqlite3_prepare_v2(m_db, sqlpl, -1, &m_stmt, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot execute sqlite3_prepare_v2\n");
  }

  rc = sqlite3_bind_text(m_stmt, 1, m_querysql.Get(), -1, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  rc = sqlite3_bind_text(m_stmt, 2, m_querysql.Get(), -1, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  rc = sqlite3_bind_text(m_stmt, 3, m_querysql.Get(), -1, NULL);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  rc = sqlite3_bind_int(m_stmt, 4, m_querylimit);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
  }

  m_step = sqlite3_step(m_stmt);
  row = (m_step == SQLITE_ROW);
  LoadPlayListMatches();
  return row;
}

void THA_Query::LoadPlayListMatches()
{
  WDL_MutexLock lock(&m_mutex);

  m_matches->Empty(true);

  while (HasRow())
  {
    m_matches->Add(new WDL_FastString(GetColumnText(8)));
    NextStep();
  }

  m_wantmatches = true;
}

bool THA_Query::WantMatches()
{
  m_mutex.Enter();
  if (m_wantmatches)
  {
    HWND list = GetDlgItem(g_mainwnd->Handle(), IDC_LIST1);
    SendMessage(list, WM_SETREDRAW, FALSE, 0);
  }

  return m_wantmatches;
}

void THA_Query::DoneWithMatches()
{
  if (m_wantmatches)
  {
    HWND list = GetDlgItem(g_mainwnd->Handle(), IDC_LIST1);
    SendMessage(list, WM_SETREDRAW, TRUE, 0);
  }

  m_matches->Empty(true);
  m_wantmatches = false;
  m_mutex.Leave();
}

WDL_PtrList<WDL_FastString> *THA_Query::GetMatches()
{
  WDL_MutexLock lock(&m_mutex);
  return m_matches;
}

void THA_Query::DeleteAllArtists()
{
  WDL_MutexLock lock(&m_mutex);

  for (int i = 0; i < m_artists->GetSize(); i++)
  {
    THA_Query::Album *al = m_artists->Enumerate(i);

    for (int j = 0; j < al->GetSize(); j++)
    {
      THA_Query::Title *ti = al->Enumerate(j);
      ti->Empty(true);
      delete ti;
    }

    al->DeleteAll();
    delete al;
  }

  m_artists->DeleteAll();
}

int THA_Query::Run()
{
  switch (m_action)
  {
  case THA_QUERY_SEARCH_DB:
    {
      LoadArtists();
      m_action = THA_QUERY_NO_ACTION;
    }
  }

  return 1;
}

unsigned WINAPI THA_Query::ThreadFunc(void *arg)
{
  THA_Query *self = (THA_Query *)arg;

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

    self->m_killthread = false;

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

  return 0;
}
