text: #include <unistd.h>
  #include <algorithm>
  
  #include <sqlite3.h>
  
  #include <QTimer>
  #include <QDir>
  #include <QSet>
  #include <QDesktopServices>
  #include <QThreadPool>
  
  #include "configurator.h"
  #include "seafile-applet.h"
  #include "rpc/rpc-client.h"
  #include "rpc/local-repo.h"
  #include "account-mgr.h"
  #include "api/server-repo.h"
  #include "api/requests.h"
  #include "ui/main-window.h"
  #include "utils/utils.h"
  #include "utils/file-utils.h"
  #include "utils/uninstall-helpers.h"
  #include "rpc/rpc-client.h"
  #include "rpc/clone-task.h"
  
  #include "filebrowser/file-browser-manager.h"
  #include "filebrowser/data-cache.h"
  #include "filebrowser/data-mgr.h"
  
  #include "repo-service.h"
  #include "repo-service-helper.h"
  
  namespace {
  
  class SyncedSubfolder {
  public:
      SyncedSubfolder() {}
      SyncedSubfolder(const QString &repo_id, const QString &parent_repo_id,
                      const QString &parent_path)
          : repo_id_(repo_id), parent_repo_id_(parent_repo_id),
            parent_path_(parent_path) {}
      SyncedSubfolder(const ServerRepo &repo, const ServerRepo &parent_repo,
                      const QString &parent_path)
          : repo_id_(repo.id), parent_repo_id_(parent_repo.id),
            parent_path_(parent_path) {}
      SyncedSubfolder(const SyncedSubfolder &rhs)
          : repo_id_(rhs.repo_id_), parent_repo_id_(rhs.parent_repo_id_),
            parent_path_(rhs.parent_path_) {}
  
      SyncedSubfolder& operator=(const SyncedSubfolder &rhs)
      {
          repo_id_ = rhs.repo_id_;
          parent_repo_id_ = rhs.parent_repo_id_;
          parent_path_ = rhs.parent_path_;
          return *this;
      }
  
      const QString& repoId() const { return repo_id_; }
      const QString& parentRepoId() const { return parent_repo_id_; }
      const QString& parentPath() const { return parent_path_; }
  
      bool isValid() const { return !repo_id_.isEmpty(); }
  
  private:
      QString repo_id_;
      QString parent_repo_id_;
      QString parent_path_;
  };
  
  const int kRefreshReposInterval = 1000 * 60 * 5; // 5 min
  
  bool loadSyncedFolderCB(sqlite3_stmt *stmt, void *data)
  {
      std::vector<SyncedSubfolder> *synced_subfolders = static_cast<std::vector<SyncedSubfolder> *>(data);
      const char *repo_id = (const char *)sqlite3_column_text (stmt, 0);
      const char *parent_repo_id = (const char *)sqlite3_column_text (stmt, 1);
      const char *parent_path = (const char *)sqlite3_column_text (stmt, 2);
      synced_subfolders->push_back(SyncedSubfolder(repo_id, parent_repo_id, parent_path));
  
      return true;
  }
  
  // TODO: use lambda to replace this helper class if we are switching to C++11
  template<class T>
  class RepohasRepoID {
  public:
      RepohasRepoID(const QString &repo_id) : repo_id_(repo_id) {}
      bool operator()(const T &repo) {
          return repo.id == repo_id_;
      }
  
  private:
      const QString& repo_id_;
  };
  
  class SyncedSubfolderhasRepoID {
  public:
      SyncedSubfolderhasRepoID(const QString &repo_id) : repo_id_(repo_id) {}
      bool operator()(const SyncedSubfolder &subfolder) {
          return subfolder.repoId() == repo_id_;
      }
  
  private:
      const QString& repo_id_;
  };
  
  // don't rely on this,
  // it is used for get repo request only
  std::vector<SyncedSubfolder> synced_subfolders_;
  
  } // namespace
  
  SINGLETON_IMPL(RepoService)
  
  RepoService::RepoService(QObject *parent)
      : QObject(parent), synced_subfolder_db_(NULL)
  {
      rpc_ = new SeafileRpcClient;
      rpc_->connectDaemon();
      refresh_timer_ = new QTimer(this);
      connect(refresh_timer_, SIGNAL(timeout()), this, SLOT(refresh()));
      list_repo_req_ = NULL;
      in_refresh_ = false;
      wipe_in_progress_ = false;
  }
  
  RepoService::~RepoService()
  {
      if (synced_subfolder_db_)
          sqlite3_close(synced_subfolder_db_);
  }
  
  void RepoService::start()
  {
      const char *errmsg;
  
      do {
          QString db_path = QDir(seafApplet->configurator()->seafileDir()).filePath("accounts.db");
          if (sqlite3_open (db_path.toUtf8().data(), &synced_subfolder_db_)) {
              errmsg = sqlite3_errmsg (synced_subfolder_db_);
              qWarning("failed to open synced subfolder database %s: %s",
                      db_path.toUtf8().data(), errmsg ? errmsg : "no error given");
  
              sqlite3_close(synced_subfolder_db_);
              synced_subfolder_db_ = NULL;
              break;
          }
  
          // enabling foreign keys, it must be done manually from each connection
          // and this feature is only supported from sqlite 3.6.19
          const char *sql = "PRAGMA foreign_keys=ON;";
          if (sqlite_query_exec (synced_subfolder_db_, sql) < 0) {
              qWarning("sqlite version is too low to support foreign key feature\n");
              qWarning("feature synced_sub_folder is disabled\n");
              sqlite3_close(synced_subfolder_db_);
              synced_subfolder_db_ = NULL;
              break;
          }
  
          // create SyncedSubfolder table
          sql = "CREATE TABLE IF NOT EXISTS SyncedSubfolder ("
              "repo_id TEXT PRIMARY KEY, parent_repo_id TEXT NOT NULL, "
              "url VARCHAR(24), username VARCHAR(15), "
              "parent_path TEXT NOT NULL, "
              "FOREIGN KEY(url, username) REFERENCES Accounts(url, username) "
              "ON DELETE CASCADE ON UPDATE CASCADE )";
          if (sqlite_query_exec (synced_subfolder_db_, sql) < 0) {
              qWarning("failed to create synced subfolder table\n");
              sqlite3_close(synced_subfolder_db_);
              synced_subfolder_db_ = NULL;
          }
      } while (0);
  
      refresh_timer_->start(kRefreshReposInterval);
  }
  
  void RepoService::stop()
  {
      refresh_timer_->stop();
  }
  
  void RepoService::refreshLocalRepoList() {
      // local_repos_.clear(); is called intenally
      if (rpc_->listLocalRepos(&local_repos_) < 0) {
          qWarning("unable to refresh local repos\n");
      }
  }
  
  void RepoService::refresh()
  {
      if (in_refresh_) {
          return;
      }
  
      const std::vector<Account>& accounts = seafApplet->accountManager()->accounts();
      if (accounts.empty()) {
          in_refresh_ = false;
          return;
      }
  
      const Account *account = &accounts.front();
  
      if (!account->isValid())
          return;
  
      in_refresh_ = true;
  
      if (!get_repo_reqs_.empty()) {
          for (std::list<GetRepoRequest*>::iterator pos = get_repo_reqs_.begin(); pos != get_repo_reqs_.end(); ++pos)
            delete (*pos);
          get_repo_reqs_.clear();
      }
  
      refreshLocalRepoList();
  
      synced_subfolders_.clear();
      if (synced_subfolder_db_) {
  
          char *zql = sqlite3_mprintf("SELECT repo_id, parent_repo_id, parent_path FROM SyncedSubfolder "
                                      "WHERE url = %Q AND username = %Q",
                                      account->serverUrl.toEncoded().data(),
                                      account->username.toUtf8().data());;
          sqlite_foreach_selected_row (synced_subfolder_db_, zql,
                                       loadSyncedFolderCB, &synced_subfolders_);
          sqlite3_free(zql);
  
          std::vector<CloneTask> tasks;
          seafApplet->rpcClient()->getCloneTasks(&tasks);
  
          QSet<QString> local_repos;
          for (size_t i = 0; i < tasks.size(); ++i) {
              local_repos.insert(tasks[i].repo_id);
          }
          for (size_t i = 0; i < local_repos_.size(); ++i) {
              local_repos.insert(local_repos_[i].id);
          }
  
          // If the synced virtual repo no longer exists locally, remove it from
          // applet database.
          for (size_t i = 0; i < synced_subfolders_.size(); ++i) {
              if (!local_repos.contains(synced_subfolders_[i].repoId())) {
                removeSyncedSubfolder(synced_subfolders_[i].repoId());
              }
          }
      }
  
      if (list_repo_req_) {
          list_repo_req_->deleteLater();
      }
  
      list_repo_req_ = new ListReposRequest(*account);
  
      connect(list_repo_req_, SIGNAL(success(const std::vector<ServerRepo>&)),
              this, SLOT(onRefreshSuccess(const std::vector<ServerRepo>&)));
  
      connect(list_repo_req_, SIGNAL(failed(const ApiError&)),
              this, SLOT(onRefreshFailed(const ApiError&)));
      list_repo_req_->send();
  }
  
  void RepoService::onRefreshSuccess(const std::vector<ServerRepo>& repos)
  {
      in_refresh_ = false;
  
      server_repos_ = repos;
  
      // if we have local repo missed in server_repos
      // start a GetRepoRequest for it
      for (size_t i = 0; i < synced_subfolders_.size(); ++i) {
          bool found = false;
          for (size_t j = 0; j < server_repos_.size(); ++j) {
              if (synced_subfolders_[i].repoId() == server_repos_[j].id) {
                  found = true;
                  server_repos_[j].parent_repo_id = synced_subfolders_[i].parentRepoId();
                  server_repos_[j].parent_path = synced_subfolders_[i].parentPath();
                  break;
              }
          }
          // create GetRepoRequest
          if (!found)
              startGetRequestFor(synced_subfolders_[i].repoId());
      }
  
      if (get_repo_reqs_.empty())
          emit refreshSuccess(repos);
  }
  
  void RepoService::onRefreshFailed(const ApiError& error)
  {
      in_refresh_ = false;
  
      if (list_repo_req_->reply()->hasRawHeader("X-Seafile-Wiped")) {
          qWarning ("current device is marked to be remote wiped\n");
          // TODO: Remote wipe should be managed in a separate module, not here.
          if (!wipe_in_progress_) {
              wipe_in_progress_ = true;
              wipeLocalFiles();
          }
          return;
      }
  
      // for the new version of seafile server
      // we may have a 401 response whenever invalid token is used.
      // see more: https://github.com/haiwen/seahub/commit/94dcfe338a52304f5895914ac59540b6176c679e
      // but we only handle this error here to avoid complicate code since it is
      // general enough.
      // TODO move this handling to all possible cases
      if (error.type() == ApiError::HTTP_ERROR && error.httpErrorCode() == 401) {
          seafApplet->accountManager()->invalidateCurrentLogin();
          return;
      }
  
      emit refreshFailed(error);
  }
  
  void RepoService::refresh(bool force)
  {
      if (!force || !in_refresh_) {
          refresh();
          return;
      }
  
      // Abort the current request and send another
      in_refresh_ = false;
      refresh();
  }
  
  ServerRepo
  RepoService::getRepo(const QString& repo_id) const
  {
      size_t n = server_repos_.size();
      for (size_t i = 0; i < n; i++) {
          const ServerRepo &repo = server_repos_[i];
          if (repo.id == repo_id) {
              return repo;
          }
      }
  
      return ServerRepo();
  }
  
  void RepoService::openLocalFile(const QString& repo_id,
                                  const QString& path_in_repo,
                                  QWidget *dialog_parent)
  {
      if (path_in_repo.endsWith("/")) {
          openFolder(repo_id, path_in_repo.left(path_in_repo.size() - 1));
          return;
      }
      qDebug("trying to open file %s in library %s", path_in_repo.toUtf8().data(), repo_id.toUtf8().data());
  
      LocalRepo r;
  
      seafApplet->rpcClient()->getLocalRepo(repo_id, &r);
  
      if (r.isValid()) {
          QString local_path = ::pathJoin(r.worktree, path_in_repo);
  
          FileDownloadHelper::openFile(local_path, false);
      } else {
          ServerRepo repo = getRepo(repo_id);
          if (!repo.isValid()) {
              QString msg = tr("Unable to open file \"%1\" from nonexistent library \"%2\"").arg(path_in_repo).arg(repo_id);
              seafApplet->warningBox(msg);
              return;
          }
  
          const QString path = "/" + path_in_repo;
          //TODO select the correct account
          const Account account = seafApplet->accountManager()->currentAccount();
          if (!account.isValid()) {
              qWarning("no valid account found");
              return;
          }
          FileDownloadHelper *helper =
            new FileDownloadHelper(account, repo, path, dialog_parent);
          helper->setParent(dialog_parent);
          helper->start();
      }
  }
  
  void RepoService::openFolder(const QString &repo_id,
                               const QString &path_in_repo)
  {
      qDebug("trying to open folder %s in library %s", path_in_repo.toUtf8().data(), repo_id.toUtf8().data());
      LocalRepo r;
  
      seafApplet->rpcClient()->getLocalRepo(repo_id, &r);
  
      if (r.isValid()) {
          QString local_path = ::pathJoin(r.worktree, path_in_repo);
  
          FileDownloadHelper::openFile(local_path, false);
      } else {
          QString fixed_path = path_in_repo == "." ? "/" : path_in_repo;
          ServerRepo repo = getRepo(repo_id);
          if (!repo.isValid()) {
              QString msg = tr("Unable to open file \"%1\" from nonexistent library \"%2\"").arg(path_in_repo).arg(repo_id);
              seafApplet->warningBox(msg);
              return;
          }
  
          //TODO select the correct account
          const Account account = seafApplet->accountManager()->currentAccount();
          if (!account.isValid()) {
              qWarning("no valid account found");
              return;
          }
          FileBrowserManager::getInstance()->openOrActivateDialog(account, repo, fixed_path);
      }
  }
  
  void RepoService::onGetRequestSuccess(const ServerRepo& repo)
  {
      GetRepoRequest* req = qobject_cast<GetRepoRequest*>(sender());
      if (!req)
          return;
  
      SyncedSubfolderhasRepoID subfolder_helper(repo.id);
      std::vector<SyncedSubfolder>::iterator pos = std::find_if(synced_subfolders_.begin(), synced_subfolders_.end(), subfolder_helper);
  
      if (pos != synced_subfolders_.end()) {
          ServerRepo fixed_repo = repo;
          fixed_repo.parent_path = pos->parentPath();
          fixed_repo.parent_repo_id = pos->parentRepoId();
          server_repos_.push_back(fixed_repo);
      }
  
      // delete current request
      req->deleteLater();
      get_repo_reqs_.pop_front();
  
      // start the next request or mark it as success
      if (!get_repo_reqs_.empty())
          get_repo_reqs_.front()->send();
      else
          emit refreshSuccess(server_repos_);
  }
  
  void RepoService::onGetRequestFailed(const ApiError& /*error*/)
  {
      GetRepoRequest* req = qobject_cast<GetRepoRequest*>(sender());
      if (!req)
          return;
  
      // delete current request
      req->deleteLater();
      get_repo_reqs_.pop_front();
  
      // start the next request or mark it as success
      if (!get_repo_reqs_.empty())
          get_repo_reqs_.front()->send();
      else
          emit refreshSuccess(server_repos_);
  }
  
  void RepoService::startGetRequestFor(const QString &repo_id)
  {
      bool was_empty = get_repo_reqs_.empty();
      GetRepoRequest *req = new GetRepoRequest(seafApplet->accountManager()->currentAccount(), repo_id);
      get_repo_reqs_.push_back(req);
      connect(req, SIGNAL(success(const ServerRepo&)), this, SLOT(onGetRequestSuccess(const ServerRepo&)));
      connect(req, SIGNAL(failed(const ApiError&)), this, SLOT(onGetRequestFailed(const ApiError&)));
      if (was_empty)
          req->send();
  }
  
  void RepoService::saveSyncedSubfolder(const ServerRepo& subfolder)
  {
      const std::vector<Account>& accounts = seafApplet->accountManager()->accounts();
      if (accounts.empty() || !subfolder.isSubfolder())
          return;
      const Account *account = &accounts.front();
  
      // if we have it before
      RepohasRepoID<ServerRepo> repo_helper(subfolder.id);
      std::vector<ServerRepo>::iterator pos = std::find_if(server_repos_.begin(), server_repos_.end(), repo_helper);
      if (pos == server_repos_.end()) {
          server_repos_.push_back(subfolder);
      } else {
          pos->parent_path = subfolder.parent_path;
          pos->parent_repo_id = subfolder.parent_repo_id;
      }
  
      if (!synced_subfolder_db_) {
          qWarning("synced subfolder database is not available\n");
          return;
      }
  
      char *zql = sqlite3_mprintf(
          "REPLACE INTO SyncedSubfolder(repo_id, parent_repo_id, url, username, "
          "parent_path) VALUES (%Q, %Q, %Q, %Q, %Q)",
          subfolder.id.toUtf8().data(), subfolder.parent_repo_id.toUtf8().data(),
          account->serverUrl.toEncoded().data(),
          account->username.toUtf8().data(),
          subfolder.parent_path.toUtf8().data());
      sqlite_query_exec (synced_subfolder_db_, zql);
      sqlite3_free(zql);
  
      emit refreshSuccess(server_repos_);
  }
  
  void RepoService::removeSyncedSubfolder(const QString& repo_id)
  {
      char *zql = sqlite3_mprintf("DELETE FROM SyncedSubfolder WHERE repo_id = %Q", repo_id.toUtf8().data());
      sqlite_query_exec(synced_subfolder_db_, zql);
      sqlite3_free(zql);
  
      SyncedSubfolderhasRepoID subfolder_helper(repo_id);
      synced_subfolders_.erase(std::remove_if(synced_subfolders_.begin(), synced_subfolders_.end(), subfolder_helper), synced_subfolders_.end());
  
      RepohasRepoID<ServerRepo> repo_helper(repo_id);
      std::vector<ServerRepo>::iterator pos = std::remove_if(server_repos_.begin(), server_repos_.end(), repo_helper);
      if (pos != server_repos_.end() && pos->isSubfolder()) {
          server_repos_.erase(pos, server_repos_.end());
  
          emit refreshSuccess(server_repos_);
      }
  }
  
  void RepoService::removeCloudFileBrowserCache()
  {
      QList<FileCache::CacheEntry> all_files =
          FileCache::instance()->getAllCachedFiles();
      const Account account = seafApplet->accountManager()->currentAccount();
      foreach (const FileCache::CacheEntry& entry, all_files) {
          if (account.getSignature() == entry.account_sig) {
              QString fullpath = DataManager::getLocalCacheFilePath(entry.repo_id, entry.path);
              printf ("removing cached file %s\n", toCStr(fullpath));
              ::unlink((const char*)(toCStr(fullpath)));
          }
      }
  }
  
  
  void WipeFilesThread::run()
  {
      for (size_t i = 0; i < local_repos_.size(); ++i) {
          const LocalRepo& repo = local_repos_[i];
          qWarning ("removing all files of repo %s: %s\n",
                    toCStr(repo.name),
                    toCStr(repo.worktree));
          if (repo.worktree.length() > 1) {
              delete_dir_recursively(repo.worktree);
          }
      }
  
      for (int i = 0; i < cached_files_.size(); i++) {
          ::unlink(toCStr(cached_files_[i]));
      }
  
      emit done();
  }
  
  void RepoService::wipeLocalFiles()
  {
      const Account& account = seafApplet->accountManager()->currentAccount();
  
      // Collect repo worktrees
      std::vector<LocalRepo> repos_to_wipe;
      rpc_->listLocalRepos(&local_repos_);
      QStringList worktrees;
      for (size_t i = 0; i < local_repos_.size(); ++i) {
          const LocalRepo& repo = local_repos_[i];
  
          QString repo_server_url;
          if (rpc_->getRepoProperty(repo.id, "server-url", &repo_server_url) < 0) {
              continue;
          }
          if (QUrl(repo_server_url).host() != account.serverUrl.host()) {
              continue;
          }
  
          rpc_->unsync(repo.id);
          repos_to_wipe.push_back(repo);
      }
  
      // Collect files cached by cloud file browser
      QStringList cached_files;
      QList<FileCache::CacheEntry> all_files =
          FileCache::instance()->getAllCachedFiles();
      foreach (const FileCache::CacheEntry& entry, all_files) {
          if (account.getSignature() == entry.account_sig) {
              QString fullpath = DataManager::getLocalCacheFilePath(entry.repo_id, entry.path);
              cached_files << DataManager::getLocalCacheFilePath(entry.repo_id, entry.path);
          }
      }
  
      WipeFilesThread *wiper = new WipeFilesThread(repos_to_wipe, cached_files);
      connect(wiper, SIGNAL(done()), this, SLOT(onWiperDone()));
  
      QThreadPool::globalInstance()->start(wiper);
  }
  
  void RepoService::onWiperDone()
  {
      RemoteWipeReportRequest *req = new RemoteWipeReportRequest(
          seafApplet->accountManager()->currentAccount());
  
      connect(req,
              SIGNAL(success()),
              this,
              SLOT(onRemoteWipeReportSuccess()));
  
      connect(req,
              SIGNAL(failed(const ApiError &)),
              this,
              SLOT(onRemoteWipeReportFailed(const ApiError &)));
  
      req->send();
  }
  
  void RepoService::onRemoteWipeReportSuccess()
  {
      wipe_in_progress_ = false;
      RemoteWipeReportRequest* req = qobject_cast<RemoteWipeReportRequest*>(sender());
      req->deleteLater();
      seafApplet->accountManager()->invalidateCurrentLogin();
  }
  
  void RepoService::onRemoteWipeReportFailed(const ApiError& error)
  {
      wipe_in_progress_ = false;
      RemoteWipeReportRequest* req = qobject_cast<RemoteWipeReportRequest*>(sender());
      req->deleteLater();
      seafApplet->accountManager()->invalidateCurrentLogin();
  }
  