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

#include "thalia/database.h"
#include "thalia/preferences.h"
#include "thalia/plugin.h"
#include "thalia/app_info.h"

#include "WDL/dirscan.h"
#include "WDL/fileread.h"

#define THA_DB_ACTION_NONE 1
#define THA_DB_ACTION_READ_METADATA 2

THA_Database::THA_Database()
  : m_thread(NULL)
  , m_killthread(false)
  , m_action(0)
  , m_scaninprogress(false)
  , m_paths(NULL)
  , m_cancel(false)
  , m_open(false)
  , m_db(NULL)
  , m_stmt(NULL)
{
  g_preferences->GetMediaLibraryPaths(&m_paths);
}

THA_Database::~THA_Database()
{
  if (IsScanInProgress())
  {
    StopScan();
  }
  if (IsOpen()) Close();
  if (m_paths) delete m_paths;
}

bool THA_Database::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, NULL, NULL, &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;
  }

  m_cancel = 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_lp.parse(g_media_ext.Get());

  m_open = true;
  return true;
}

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

void THA_Database::Close()
{
  WDL_MutexLock lock(&m_mutex);
  StopScan();

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

void THA_Database::ScanFiles()
{
  int rc = 0;
  char *errmsg = NULL;

  const char *sql_delete_table =
    "DROP TABLE IF EXISTS metadata;";

  rc = sqlite3_exec(m_db, sql_delete_table, NULL, NULL, &errmsg);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot execute sql command (%s)\n", errmsg);
    sqlite3_free(errmsg);
  }

  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, NULL, NULL, &errmsg);
  if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
  {
    wdl_log("cannot execute sql command (%s)\n", errmsg);
    sqlite3_free(errmsg);
  }

  m_action = THA_DB_ACTION_READ_METADATA;
}

void THA_Database::StopScan()
{
  m_cancel = true;
}

bool THA_Database::IsScanInProgress() const
{
  return m_scaninprogress;
}

const char *THA_Database::GetProgressStage() const
{
  return m_progressstage.Get();
}

const char *THA_Database::GetProgressFilename() const
{
  return m_progfn.Get();
}

void THA_Database::ReadMetadata(const char *path)
{
  WDL_DirScan dir;
  WDL_FastString fn;
  int rc = 0;
  char *errmsg = NULL;

  if (!dir.First(path))
  {
    do
    {
      if (dir.GetCurrentIsDirectory())
      {
        dir.GetCurrentFullFN(&fn);

        if (strcmp(fn.get_filepart(), ".") &&
            strcmp(fn.get_filepart(), ".."))
        {
          ReadMetadata(fn.Get());
        }
      }
      else
      {
        dir.GetCurrentFullFN(&fn);

        bool valid_ext = false;

        for (int i = 0; i < m_lp.getnumtokens(); i++)
        {
          if (!wdl_filename_cmp(fn.get_fileext(), m_lp.gettoken_str(i)))
          {
            valid_ext = true;
            break;
          }
        }

        if (!valid_ext) continue;

        THA_IFileInput *fi = CreateFileInput(fn.Get());

        if (WDL_unlikely(!fi))
        {
          continue;
        }

        THA_IFileTag *ft = CreateFileTag(fn.Get());

        if (WDL_unlikely(!ft))
        {
          delete fi; fi = NULL;
          continue;
        }

        m_progfn.Set(fn.get_filepart());

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


        if (strlen(ft->GetTitle()))
        {
          rc = sqlite3_bind_text(m_stmt, 2, ft->GetTitle(), -1, NULL);
          if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
          {
            wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
          }
        }
        else
        {
          m_strbuf.Set(ft->GetFileName());
          m_strbuf.remove_fileext();
          rc = sqlite3_bind_text(m_stmt, 2, m_strbuf.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, ft->GetArtist(), -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, 4, ft->GetAlbum(), -1, NULL);
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
        }

        rc = sqlite3_bind_double(m_stmt, 5, fi->GetLength());
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
        }

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

        rc = sqlite3_bind_double(m_stmt, 7, fi->GetSampleRate());
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
        }

        rc = sqlite3_bind_text(m_stmt, 8, ft->GetFileName(), -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, 9, ft->GetFilePath(), -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, 10, ft->GetFileExtension(), -1, NULL);
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
        }

        rc = sqlite3_bind_int64(m_stmt, 11, ft->GetFileSize());
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot bind value %s\n", sqlite3_errmsg(m_db));
        }

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

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

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

        delete ft;
        delete fi;
      }

      if (WDL_unlikely(m_cancel)) break;
    }
    while (!dir.Next());
  }
}

int THA_Database::Run()
{
  bool want_sleep = true;

  switch (m_action)
  {
  case THA_DB_ACTION_READ_METADATA:
    {
      want_sleep = false;
      char *errmsg = NULL;

      m_scaninprogress = true;
      m_progressstage.Set("Reading metadata...");

      const char *sql_insert_metadata =
        "INSERT OR REPLACE INTO metadata ("
        "type,"
        "title,"
        "artist,"
        "album,"
        "length,"
        "genre,"
        "samplerate,"
        "file_name,"
        "file_path,"
        "file_ext,"
        "file_size,"
        "comment)"
        " VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12);";

      m_sqlstr.Set(sql_insert_metadata);
      int rc = sqlite3_prepare_v2(m_db, m_sqlstr.Get(), -1, &m_stmt, NULL);
      if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
      {
        wdl_log("cannot prepare statement %s\n", m_sqlstr.Get());
      }

      rc = sqlite3_exec(m_db, "BEGIN TRANSACTION;", NULL, NULL, &errmsg);
      if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
      {
        wdl_log("cannot begin transaction\n");
        sqlite3_free(errmsg);
      }

      for (int i = 0; i < m_paths->GetSize(); i++)
      {
        ReadMetadata(m_paths->Get(i)->Get());
      }

      m_progfn.Set("");

      if (m_cancel)
      {
        rc = sqlite3_exec(m_db, "ROLLBACK TRANSACTION;", NULL, NULL, &errmsg);
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot rollback transaction\n");
          sqlite3_free(errmsg);
        }

        wdl_log("Database: Abort\n");
        m_progressstage.Set("Abort");
      }
      else
      {
        m_progressstage.Set("Saving to database...");

        rc = sqlite3_exec(m_db, "COMMIT TRANSACTION;", NULL, NULL, &errmsg);
        if (WDL_NOT_NORMALLY(rc != SQLITE_OK))
        {
          wdl_log("cannot commit transaction\n");
          sqlite3_free(errmsg);
        }

        wdl_log("Database: Done\n");
        m_progressstage.Set("Done");
      }

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

      m_scaninprogress = false;

      m_action = THA_DB_ACTION_NONE;
    }
    break;
  }

  return want_sleep ? 1 : 0;
}

unsigned WINAPI THA_Database::ThreadFunc(void *arg)
{
  THA_Database *self = (THA_Database *)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;
}
