/*
   +----------------------------------------------------------------------+
   | HipHop for PHP                                                       |
   +----------------------------------------------------------------------+
   | Copyright (c) 2010-2015 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/runtime/base/apc-file-storage.h"

#include <sys/mman.h>

#include "hphp/util/alloc.h"
#include "hphp/util/timer.h"
#include "hphp/util/logger.h"

#include "hphp/runtime/base/runtime-option.h"
#include "hphp/runtime/base/type-conversions.h"
#include "hphp/runtime/base/builtin-functions.h"
#include "hphp/runtime/server/server-stats.h"
#include "hphp/runtime/base/apc-stats.h"
#include "hphp/runtime/ext/apc/ext_apc.h"

#if !defined(HAVE_POSIX_FALLOCATE) && \
  (_XOPEN_SOURCE >= 600 || _POSIX_C_SOURCE >= 200112L || defined(__CYGWIN__))
# define HAVE_POSIX_FALLOCATE 1
#endif

namespace HPHP {

//////////////////////////////////////////////////////////////////////

APCFileStorage s_apc_file_storage;

void APCFileStorage::enable(const std::string& prefix, int64_t chunkSize) {
  Lock lock(m_lock);
  m_prefix = prefix;
  m_chunkSize = chunkSize;
  if (m_state != StorageState::Invalid) {
    return;
  }
  if (!addFile()) {
    Logger::Error(
        "Failed to open initial file for file-backed APC storage, "
        "falling back to in-memory mode");
    m_state = StorageState::Full;
    return;
  }
  m_state = StorageState::Open;
}

char *APCFileStorage::put(const char *data, int32_t len) {
  Lock lock(m_lock);
  if (m_state != StorageState::Open ||
      len + PaddingSize > m_chunkSize - PaddingSize) {
    return nullptr;
  }
  if (len + PaddingSize > m_chunkRemain && !addFile()) {
    Logger::Error(
        "Failed to open additional files for file-backed APC storage, falling "
        "back to in-memory mode");
    m_state = StorageState::Full;
    return nullptr;
  }
  assert(m_current);
  assert(len + PaddingSize <= m_chunkRemain);
  strhash_t h = hash_string_unsafe(data, len);
  *(strhash_t*)m_current = h;
  m_current += sizeof(h);
  *(int32_t*)m_current = len;
  m_current += sizeof(len);
  // should be no overlap
  memcpy(m_current, data, len);
  char *addr = m_current;
  addr[len] = '\0';
  m_current += len + sizeof(char);
  *(strhash_t*)m_current = TombHash;
  m_chunkRemain -= len + PaddingSize;
  return addr;
}

void APCFileStorage::seal() {
  Lock lock(m_lock);
  if (m_state == StorageState::Sealed) {
    return;
  }
  assert(m_state == StorageState::Open || m_state == StorageState::Full);
  m_current = nullptr;
  m_chunkRemain = 0;
  m_state = StorageState::Sealed;

  for (int i = 0; i < (int)m_chunks.size(); i++) {
    if (mprotect(m_chunks[i], m_chunkSize, PROT_READ) < 0) {
      Logger::Error("Failed to mprotect chunk %d", i);
    }
  }
}

void APCFileStorage::adviseOut() {
  Lock lock(m_lock);
  Timer timer(Timer::WallTime, "advising out apc prime");
  Logger::FInfo("Advising out {} APCFileStorage chunks\n", m_chunks.size());
  for (int i = 0; i < (int)m_chunks.size(); i++) {
    if (madvise(m_chunks[i], m_chunkSize, MADV_DONTNEED) < 0) {
      Logger::Error("Failed to madvise chunk %d", i);
    }
  }
}

bool APCFileStorage::hashCheck() {
  Lock lock(m_lock);
  for (int i = 0; i < (int)m_chunks.size(); i++) {
    char *current = (char*)m_chunks[i];
    char *boundary = (char*)m_chunks[i] + m_chunkSize;
    while (1) {
      strhash_t h = *(strhash_t*)current;
      if (h == TombHash) {
        break;
      }
      current += sizeof(h);
      int32_t len = *(int32_t*)current;
      current += sizeof(len);
      if (len < 0 ||
          len + PaddingSize >= (int64_t)boundary - (int64_t)current) {
        Logger::Error("invalid len %d at chunk %d offset %" PRId64, len, i,
                      (int64_t)current - (int64_t)m_chunks[i]);
        return false;
      }
      strhash_t h_data = hash_string(current, len);
      if (h_data != h) {
        Logger::Error("invalid hash at chunk %d offset %" PRId64, i,
                      (int64_t)current - (int64_t)m_chunks[i]);
        return false;
      }
      current += len;
      if (*current != '\0') {
        Logger::Error("missing \\0 at chunk %d offset %" PRId64, i,
                      (int64_t)current - (int64_t)m_chunks[i]);
        return false;
      }
      current++;
    }
  }
  return true;
}

void APCFileStorage::cleanup() {
  Lock lock(m_lock);
  for (unsigned int i = 0 ; i < m_fileNames.size(); i++) {
    unlink(m_fileNames[i].c_str());
  }
  m_chunks.clear();
}

bool APCFileStorage::addFile() {
  char name[PATH_MAX];
  snprintf(name, sizeof(name), "%s.XXXXXX", m_prefix.c_str());
  int fd = mkstemp(name);
  if (fd < 0) {
    Logger::Error("Failed to open temp file");
    return false;
  }
  bool couldAllocate = false;
#if defined(HAVE_POSIX_FALLOCATE) && HAVE_POSIX_FALLOCATE
  couldAllocate = posix_fallocate(fd, 0, m_chunkSize) == 0;
#elif defined(__APPLE__)
  fstore_t store = { F_ALLOCATECONTIG, F_PEOFPOSMODE, 0, m_chunkSize };
  int ret = fcntl(fd, F_PREALLOCATE, &store);
  if (ret == -1) {
    store.fst_flags = F_ALLOCATEALL;
    ret = fcntl(fd, F_PREALLOCATE, &store);
    if (ret == -1) {
        couldAllocate = false;
    }
  }
  couldAllocate = ftruncate(fd, m_chunkSize) == 0;
#else
 #error "No implementation for posix_fallocate on your platform."
#endif
  if (!couldAllocate) {
    Logger::Error("Failed to posix_fallocate of size %" PRId64, m_chunkSize);
    close(fd);
    return false;
  }
  if (apcExtension::FileStorageKeepFileLinked) {
    m_fileNames.push_back(std::string(name));
  } else {
    unlink(name);
  }
  char *addr = (char *)mmap(nullptr, m_chunkSize, PROT_READ | PROT_WRITE,
                            MAP_SHARED, fd, 0);
  if (addr == (char *)-1) {
    Logger::Error("Failed to mmap %s of size %" PRId64, name, m_chunkSize);
    close(fd);
    return false;
  }
  numa_interleave(addr, m_chunkSize);
  m_current = addr;
  m_chunkRemain = m_chunkSize - PaddingSize;
  m_chunks.push_back(addr);
  close(fd);
  return true;
}

///////////////////////////////////////////////////////////////////////////////

}
