/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "FileSystemModule.h"

#include "sqlite3.h"
#include "nsComponentManagerUtils.h"
#include "nsString.h"
#include "nsIDirectoryEnumerator.h"
#include "nsIFile.h"

namespace {

struct VirtualTableCursorBase {
  VirtualTableCursorBase() { memset(&mBase, 0, sizeof(mBase)); }

  sqlite3_vtab_cursor mBase;
};

struct VirtualTableCursor : public VirtualTableCursorBase {
 public:
  VirtualTableCursor() : mRowId(-1) { mCurrentFileName.SetIsVoid(true); }

  const nsString& DirectoryPath() const { return mDirectoryPath; }

  const nsString& CurrentFileName() const { return mCurrentFileName; }

  int64_t RowId() const { return mRowId; }

  nsresult Init(const nsAString& aPath);
  nsresult NextFile();

 private:
  nsCOMPtr<nsIDirectoryEnumerator> mEntries;

  nsString mDirectoryPath;
  nsString mCurrentFileName;

  int64_t mRowId;
};

nsresult VirtualTableCursor::Init(const nsAString& aPath) {
  nsCOMPtr<nsIFile> directory = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID);
  NS_ENSURE_TRUE(directory, NS_ERROR_FAILURE);

  nsresult rv = directory->InitWithPath(aPath);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = directory->GetPath(mDirectoryPath);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = directory->GetDirectoryEntries(getter_AddRefs(mEntries));
  NS_ENSURE_SUCCESS(rv, rv);

  rv = NextFile();
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult VirtualTableCursor::NextFile() {
  bool hasMore;
  nsresult rv = mEntries->HasMoreElements(&hasMore);
  NS_ENSURE_SUCCESS(rv, rv);

  if (!hasMore) {
    mCurrentFileName.SetIsVoid(true);
    return NS_OK;
  }

  nsCOMPtr<nsISupports> entry;
  rv = mEntries->GetNext(getter_AddRefs(entry));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIFile> file = do_QueryInterface(entry);
  NS_ENSURE_TRUE(file, NS_ERROR_FAILURE);

  rv = file->GetLeafName(mCurrentFileName);
  NS_ENSURE_SUCCESS(rv, rv);

  mRowId++;

  return NS_OK;
}

int Connect(sqlite3* aDB, void* aAux, int aArgc, const char* const* aArgv,
            sqlite3_vtab** aVtab, char** aErr) {
  static const char virtualTableSchema[] =
      "CREATE TABLE fs ("
      "name TEXT, "
      "path TEXT"
      ")";

  int rc = sqlite3_declare_vtab(aDB, virtualTableSchema);
  if (rc != SQLITE_OK) {
    return rc;
  }

  sqlite3_vtab* vt = new sqlite3_vtab();
  memset(vt, 0, sizeof(*vt));

  *aVtab = vt;

  return SQLITE_OK;
}

int Disconnect(sqlite3_vtab* aVtab) {
  delete aVtab;

  return SQLITE_OK;
}

int BestIndex(sqlite3_vtab* aVtab, sqlite3_index_info* aInfo) {
  // Here we specify what index constraints we want to handle. That is, there
  // might be some columns with particular constraints in which we can help
  // SQLite narrow down the result set.
  //
  // For example, take the "path = x" where x is a directory. In this case,
  // we can narrow our search to just this directory instead of the entire file
  // system. This can be a significant optimization. So, we want to handle that
  // constraint. To do so, we would look for two specific input conditions:
  //
  // 1. aInfo->aConstraint[i].iColumn == 1
  // 2. aInfo->aConstraint[i].op == SQLITE_INDEX_CONSTRAINT_EQ
  //
  // The first states that the path column is being used in one of the input
  // constraints and the second states that the constraint involves the equal
  // operator.
  //
  // An even more specific search would be for name='xxx', in which case we
  // can limit the search to a single file, if it exists.
  //
  // What we have to do here is look for all of our index searches and select
  // the narrowest. We can only pick one, so obviously we want the one that
  // is the most specific, which leads to the smallest result set.

  for (int i = 0; i < aInfo->nConstraint; i++) {
    if (aInfo->aConstraint[i].iColumn == 1 && aInfo->aConstraint[i].usable) {
      if (aInfo->aConstraint[i].op & SQLITE_INDEX_CONSTRAINT_EQ) {
        aInfo->aConstraintUsage[i].argvIndex = 1;
      }
      break;
    }

    // TODO: handle single files (constrained also by the name column)
  }

  return SQLITE_OK;
}

int Open(sqlite3_vtab* aVtab, sqlite3_vtab_cursor** aCursor) {
  VirtualTableCursor* cursor = new VirtualTableCursor();

  *aCursor = reinterpret_cast<sqlite3_vtab_cursor*>(cursor);

  return SQLITE_OK;
}

int Close(sqlite3_vtab_cursor* aCursor) {
  VirtualTableCursor* cursor = reinterpret_cast<VirtualTableCursor*>(aCursor);

  delete cursor;

  return SQLITE_OK;
}

int Filter(sqlite3_vtab_cursor* aCursor, int aIdxNum, const char* aIdxStr,
           int aArgc, sqlite3_value** aArgv) {
  VirtualTableCursor* cursor = reinterpret_cast<VirtualTableCursor*>(aCursor);

  if (aArgc <= 0) {
    return SQLITE_OK;
  }

  const char16_t* value =
      static_cast<const char16_t*>(::sqlite3_value_text16(aArgv[0]));

  nsDependentString path(value,
                         ::sqlite3_value_bytes16(aArgv[0]) / sizeof(char16_t));

  nsresult rv = cursor->Init(path);
  NS_ENSURE_SUCCESS(rv, SQLITE_ERROR);

  return SQLITE_OK;
}

int Next(sqlite3_vtab_cursor* aCursor) {
  VirtualTableCursor* cursor = reinterpret_cast<VirtualTableCursor*>(aCursor);

  nsresult rv = cursor->NextFile();
  NS_ENSURE_SUCCESS(rv, SQLITE_ERROR);

  return SQLITE_OK;
}

int Eof(sqlite3_vtab_cursor* aCursor) {
  VirtualTableCursor* cursor = reinterpret_cast<VirtualTableCursor*>(aCursor);
  return cursor->CurrentFileName().IsVoid() ? 1 : 0;
}

int Column(sqlite3_vtab_cursor* aCursor, sqlite3_context* aContext,
           int aColumnIndex) {
  VirtualTableCursor* cursor = reinterpret_cast<VirtualTableCursor*>(aCursor);

  switch (aColumnIndex) {
    // name
    case 0: {
      const nsString& name = cursor->CurrentFileName();
      sqlite3_result_text16(aContext, name.get(),
                            name.Length() * sizeof(char16_t), SQLITE_TRANSIENT);
      break;
    }

    // path
    case 1: {
      const nsString& path = cursor->DirectoryPath();
      sqlite3_result_text16(aContext, path.get(),
                            path.Length() * sizeof(char16_t), SQLITE_TRANSIENT);
      break;
    }
    default:
      MOZ_ASSERT_UNREACHABLE("Unsupported column!");
  }

  return SQLITE_OK;
}

int RowId(sqlite3_vtab_cursor* aCursor, sqlite3_int64* aRowid) {
  VirtualTableCursor* cursor = reinterpret_cast<VirtualTableCursor*>(aCursor);

  *aRowid = cursor->RowId();

  return SQLITE_OK;
}

}  // namespace

namespace mozilla {
namespace storage {

int RegisterFileSystemModule(sqlite3* aDB, const char* aName) {
  static sqlite3_module module = {
      1,       Connect, Connect, BestIndex, Disconnect, Disconnect, Open,
      Close,   Filter,  Next,    Eof,       Column,     RowId,      nullptr,
      nullptr, nullptr, nullptr, nullptr,   nullptr,    nullptr};

  return sqlite3_create_module(aDB, aName, &module, nullptr);
}

}  // namespace storage
}  // namespace mozilla
