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

#include "RSI/database.h"
#include "RSI/plugin.h"
#include "RSI/preferences.h"

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

#define RSI_DB_ACTION_NONE 1
#define RSI_DB_ACTION_READ_METADATA 2

RSI_Database::RSI_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);
}

RSI_Database::~RSI_Database()
{
  if (m_thread) Close();
  if (m_paths) delete m_paths;
}

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

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

  WDL_FileRead *fr = new WDL_FileRead(m_fn.Get());
  if (fr && fr->IsOpen())
  {
    delete fr; fr = NULL;
    DeleteFile(m_fn.Get());
  }
  if (fr) delete fr;

  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 RSI_Database::IsOpen() const
{
  return m_open;
}

void RSI_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 RSI_Database::ScanFiles()
{
  m_action = RSI_DB_ACTION_READ_METADATA;
}

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

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

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

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

void RSI_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;

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

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

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

        if (WDL_unlikely(!fi))
        {
          delete ft; ft = 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));
        }

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

        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 RSI_Database::Run()
{
  bool want_sleep = true;

  switch (m_action)
  {
  case RSI_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");
      }

      m_scaninprogress = false;

      m_action = RSI_DB_ACTION_NONE;
    }
    break;
  }

  return want_sleep ? 1 : 0;
}

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