/*
   +----------------------------------------------------------------------+
   | HipHop for PHP                                                       |
   +----------------------------------------------------------------------+
   | Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com)  |
   +----------------------------------------------------------------------+
   | This source file is subject to version 3.01 of the PHP license,      |
   | that is bundled with this package in the file LICENSE, and is        |
   | available through the world-wide-web at the following url:           |
   | http://www.php.net/license/3_01.txt                                  |
   | If you did not receive a copy of the PHP license and are unable to   |
   | obtain it through the world-wide-web, please send a note to          |
   | license@php.net so we can mail you a copy immediately.               |
   +----------------------------------------------------------------------+
*/

#include "hphp/util/cache/cache-manager.h"

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <cstdint>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "hphp/util/cache/cache-data.h"
#include "hphp/util/cache/cache-saver.h"
#include "hphp/util/cache/magic-numbers.h"
#include "hphp/util/cache/mmap-file.h"
#include "hphp/util/logger.h"
#include "hphp/util/text-util.h"

#include <folly/portability/SysMman.h>

namespace HPHP {

std::function<void(bool, const std::string&)> CacheManager::s_logger;

CacheManager::CacheManager() {}
CacheManager::~CacheManager() {}

bool CacheManager::getFileContents(const std::string& name, const char** data,
                                   uint64_t* data_len,
                                   bool* compressed) const {
  const auto it = m_cache_map.find(name);

  if (it == m_cache_map.end()) {
    return false;
  }

  if (s_logger && !it->second->dataFetched()) {
    s_logger(false, name);
  }

  const CacheData& cd = *it->second;
  return cd.getDataPointer(data, data_len, compressed);
}

bool CacheManager::getDecompressed(const std::string& name,
                                   std::string* data) const {
  const auto it = m_cache_map.find(name);

  if (it == m_cache_map.end()) {
    return false;
  }

  const CacheData& cd = *it->second;
  return cd.getDecompressedData(data);
}

bool CacheManager::isCompressed(const std::string& name) const {
  const auto it = m_cache_map.find(name);

  if (it == m_cache_map.end()) {
    return false;
  }

  const CacheData& cd = *it->second;
  return cd.isCompressed();
}

bool CacheManager::addFileContents(const std::string& name,
                                   const std::string& path) {
  if (m_cache_map.count(name)) {
    return false;
  }

  std::unique_ptr<CacheData> cd(new CacheData);

  if (!cd->loadFromFile(name, m_entry_counter++, path)) {
    return false;
  }

  m_cache_map[name] = std::move(cd);

  addDirectories(name);

  return true;
}

bool CacheManager::addEmptyEntry(const std::string& name) {
  if (m_cache_map.count(name)) {
    return false;
  }

  std::unique_ptr<CacheData> cd(new CacheData);
  cd->createEmpty(name, m_entry_counter++);

  m_cache_map[name] = std::move(cd);

  addDirectories(name);

  return true;
}

template<class F>
bool CacheManager::existsHelper(const std::string& name, F fn) const {
  auto it = m_cache_map.find(name);
  if (it == m_cache_map.end()) return false;

  auto cd = it->second.get();

  if (!fn(cd)) return false;

  if (s_logger && !cd->existChecked()) {
    s_logger(true, name);
  }

  return true;
}

bool CacheManager::entryExists(const std::string& name) const {
  return existsHelper(name, [] (CacheData*) { return true; });
}

bool CacheManager::fileExists(const std::string& name) const {
  return existsHelper(name, [] (CacheData* cd) {
      return cd->isRegularFile();
    });
}

bool CacheManager::dirExists(const std::string& name) const {
  return existsHelper(name, [] (CacheData* cd) {
      return cd->isDirectory();
    });
}

bool CacheManager::emptyEntryExists(const std::string& name) const {
  return existsHelper(name, [] (CacheData* cd) {
      return cd->isEmpty();
    });
}

bool CacheManager::getUncompressedFileSize(const std::string& name,
                                           uint64_t* size) const {
  const auto it = m_cache_map.find(name);

  if (it == m_cache_map.end()) {
    return false;
  }

  const CacheData& cd = *it->second;

  if (!cd.isRegularFile()) {
    return false;
  }

  if (!cd.isCompressed()) {
    *size = cd.fileSize();
    return true;
  }

  std::string data;

  if (!cd.getDecompressedData(&data)) {
    return false;
  }

  *size = data.length();
  return true;
}

bool CacheManager::loadCache(const std::string& path) {
  m_mmap_file.reset(new MmapFile(path));

  if (!m_mmap_file->init()) {
    Logger::Error("Unable to mmap cache file " + path);
    return false;
  }

  uint64_t magic;
  if (!m_mmap_file->readUInt64(&magic)) {
    Logger::Error("Unable to read magic number from " + path);
    return false;
  }

  if (magic != kCacheFileMagic) {
    Logger::Error("Bad magic number in " + path);
    return false;
  }

  uint64_t dirlen;
  if (!m_mmap_file->readUInt64(&dirlen)) {
    return false;
  }

  for (uint64_t i = 0; i < dirlen; ++i) {
    std::unique_ptr<CacheData> cd(new CacheData);

    std::string name;
    if (!cd->loadFromMmap(m_mmap_file.get(), &name)) {
      return false;
    }

    m_cache_map[name] = std::move(cd);
  }

  uint64_t dirterm;
  if (!m_mmap_file->readUInt64(&dirterm)) {
    return false;
  }

  if (dirterm != kDirectoryTerminatorMagic) {
    Logger::Error("Bad end-of-directory magic number in " + path);
    return false;
  }

  return true;
}

bool CacheManager::saveCache(const std::string& path) const {
  CacheSaver cs(path);

  if (!cs.init(kCacheFileMagic, m_cache_map.size())) {
    Logger::Error("Unable to initialize CacheSaver");
    return false;
  }

  for (const auto& it : m_cache_map) {
    const std::string& name = it.first;
    const CacheData* cd = it.second.get();

    if (!cd->save(&cs)) {
      Logger::Error("Failed to save CacheData for " + name);
      return false;
    }
  }

  if (!cs.endDirectory(kDirectoryTerminatorMagic)) {
    Logger::Error("Unable to write directory terminator");
    return false;
  }

  if (!cs.writeFiles()) {
    Logger::Error("Unable to write files");
    return false;
  }

  if (!cs.rewriteDirectory(m_cache_map.size())) {
    Logger::Error("Unable to rewrite directory");
    return false;
  }

  if (!cs.finish()) {
    Logger::Error("CacheSaver finish failed");
    return false;
  }

  return true;
}

void CacheManager::getEntryNames(std::set<std::string>* names) const {
  std::set<std::string> temp;

  for (const auto& it : m_cache_map) {
    temp.insert(it.first);
  }

  *names = temp;
}

// --- Private functions.

void CacheManager::addDirectories(const std::string& path) {

  if (path.empty()) {
    return;
  }

  size_t start = 0;
  for (size_t i = 1; i < path.length(); ++i) {
    if (path[i] == '/') {
      auto pathdir = path.substr(start, i);
      std::unique_ptr<CacheData> cd(new CacheData);
      cd->createDirectory(pathdir, m_entry_counter++);
      m_cache_map[pathdir] = std::move(cd);
    }
  }
}

VFileType CacheManager::getFileType(const std::string& name) const {
  auto file = m_cache_map.find(name);

  if (file == m_cache_map.end()) return VFileType::NotFound;
  if (file->second->isDirectory()) return VFileType::Directory;
  return VFileType::PlainFile;
}

std::vector<std::string>
CacheManager::readDirectory(const std::string& name) const {
  auto start = m_cache_map.find(name);
  if (start == m_cache_map.end() || !start->second->isDirectory()) {
    return std::vector<std::string>{};
  }

  std::set<std::string> ret;
  for (; start != m_cache_map.end() && start->first.find(name) == 0; ++start) {
    // Remove Prefix
    folly::StringPiece sp = folly::StringPiece(start->first, name.size());

    // Removing leading '/'
    while (sp.size() && sp.front() == '/') sp.pop_front();

    // Skip subpaths
    if (!sp.size() || sp.find_first_of('/') != std::string::npos) continue;

    ret.emplace(sp.str());
  }

  return std::vector<std::string>(ret.begin(), ret.end());
}

void CacheManager::dump() const {
  for (const auto& it : m_cache_map ) {
    printf("- %s\n", it.first.c_str());
    it.second->dump();
  }
}

}  // namespace HPHP
