//===- InProcessModuleCache.cpp - Implicit Module Cache ---------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "clang/DependencyScanning/InProcessModuleCache.h"

#include "clang/Serialization/InMemoryModuleCache.h"
#include "llvm/Support/AdvisoryLock.h"
#include "llvm/Support/Chrono.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/IOSandbox.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"

using namespace clang;
using namespace dependencies;

void ModuleCacheEntries::flush() {
  auto BypassSandbox = llvm::sys::sandbox::scopedDisable();
  for (auto &[Path, Entry] : Map) {
    if (Entry->State == ModuleCacheEntry::S_Written) {
      assert(Entry->WrittenBuffer && "Wrote PCM with no contents");
      // Note: We could propagate Entry->ModTime to the on-disk file, but
      // implicitly-built modules (unlike explicitly-built modules) don't use
      // that metadata to refer to imports, rendering this unnecessary.
      off_t Size;
      time_t ModTime;
      // Best-effort: ignore errors (e.g. read-only cache directory).
      (void)writeImpl(Path, *Entry->WrittenBuffer, Size, ModTime);
    }
  }
}

namespace {
class ReaderWriterLock : public llvm::AdvisoryLock {
  ModuleCacheEntry &Entry;
  std::optional<unsigned> OwnedGeneration;

public:
  ReaderWriterLock(ModuleCacheEntry &Entry) : Entry(Entry) {}

  Expected<bool> tryLock() override {
    std::lock_guard<std::mutex> Lock(Entry.Mutex);
    if (Entry.Locked)
      return false;
    Entry.Locked = true;
    OwnedGeneration = Entry.Generation;
    return true;
  }

  llvm::WaitForUnlockResult
  waitForUnlockFor(std::chrono::seconds MaxSeconds) override {
    assert(!OwnedGeneration);
    std::unique_lock<std::mutex> Lock(Entry.Mutex);
    unsigned CurrentGeneration = Entry.Generation;
    bool Success = Entry.CondVar.wait_for(Lock, MaxSeconds, [&] {
      // We check not only Locked, but also Generation to break the wait in case
      // of unsafeUnlock() and successful tryLock().
      return !Entry.Locked || Entry.Generation != CurrentGeneration;
    });
    return Success ? llvm::WaitForUnlockResult::Success
                   : llvm::WaitForUnlockResult::Timeout;
  }

  std::error_code unsafeUnlock() override {
    {
      std::lock_guard<std::mutex> Lock(Entry.Mutex);
      Entry.Generation += 1;
      Entry.Locked = false;
    }
    Entry.CondVar.notify_all();
    return {};
  }

  ~ReaderWriterLock() override {
    if (OwnedGeneration) {
      {
        std::lock_guard<std::mutex> Lock(Entry.Mutex);
        // Avoid stomping over the state managed by someone else after
        // unsafeUnlock() and successful tryLock().
        if (*OwnedGeneration == Entry.Generation)
          Entry.Locked = false;
      }
      Entry.CondVar.notify_all();
    }
  }
};

class InProcessModuleCache : public ModuleCache {
  ModuleCacheEntries &Entries;

  // TODO: If we changed the InMemoryModuleCache API and relied on strict
  // context hash, we could probably create more efficient thread-safe
  // implementation of the InMemoryModuleCache such that it doesn't need to be
  // recreated for each translation unit.
  InMemoryModuleCache InMemory;

  ModuleCacheEntry &getOrCreateEntry(StringRef Filename) {
    std::lock_guard<std::mutex> Lock(Entries.Mutex);
    auto &Entry = Entries.Map[Filename];
    if (!Entry)
      Entry = std::make_unique<ModuleCacheEntry>();
    return *Entry;
  }

public:
  InProcessModuleCache(ModuleCacheEntries &Entries) : Entries(Entries) {}

  std::unique_ptr<llvm::AdvisoryLock> getLock(StringRef Filename) override {
    auto &Entry = getOrCreateEntry(Filename);
    return std::make_unique<ReaderWriterLock>(Entry);
  }

  std::time_t getModuleTimestamp(StringRef Filename) override {
    auto &Timestamp = getOrCreateEntry(Filename).Timestamp;

    return Timestamp.load();
  }

  void updateModuleTimestamp(StringRef Filename) override {
    // Note: This essentially replaces FS contention with mutex contention.
    auto &Timestamp = getOrCreateEntry(Filename).Timestamp;

    Timestamp.store(llvm::sys::toTimeT(std::chrono::system_clock::now()));
  }

  void maybePrune(StringRef Path, time_t PruneInterval,
                  time_t PruneAfter) override {
    // FIXME: This only needs to be ran once per build, not in every
    // compilation. Call it once per service.
    maybePruneImpl(Path, PruneInterval, PruneAfter);
  }

  InMemoryModuleCache &getInMemoryModuleCache() override { return InMemory; }
  const InMemoryModuleCache &getInMemoryModuleCache() const override {
    return InMemory;
  }

  std::error_code write(StringRef Path, llvm::MemoryBufferRef Buffer,
                        off_t &Size, time_t &ModTime) override {
    ModuleCacheEntry &Entry = getOrCreateEntry(Path);
    std::lock_guard<std::mutex> Lock(Entry.Mutex);
    if (Entry.State == ModuleCacheEntry::S_Written) {
      assert(Entry.WrittenBuffer && "Wrote PCM with no contents");
      assert(Entry.WrittenBuffer->getBuffer() == Buffer.getBuffer() &&
             "Wrote the same PCM with different contents");
      Size = Entry.WrittenBuffer->getBufferSize();
      ModTime = Entry.ModTime;
      return {};
    }
    Entry.WrittenBuffer =
        llvm::MemoryBuffer::getMemBufferCopy(Buffer.getBuffer(), Path);
    Entry.ModTime = llvm::sys::toTimeT(std::chrono::system_clock::now());
    Entry.State = ModuleCacheEntry::S_Written;
    Size = Entry.WrittenBuffer->getBufferSize();
    ModTime = Entry.ModTime;
    return {};
  }

  Expected<std::unique_ptr<llvm::MemoryBuffer>>
  read(StringRef FileName, off_t &Size, time_t &ModTime) override {
    ModuleCacheEntry &Entry = getOrCreateEntry(FileName);
    std::lock_guard<std::mutex> Lock(Entry.Mutex);
    if (Entry.State == ModuleCacheEntry::S_Unknown) {
      // This is a compiler-internal input/output, let's bypass the sandbox.
      auto BypassSandbox = llvm::sys::sandbox::scopedDisable();
      off_t ReadSize;
      time_t ReadModTime;
      auto ReadBuffer = readImpl(FileName, ReadSize, ReadModTime);
      if (!ReadBuffer)
        return ReadBuffer.takeError();
      Entry.ReadBuffer = std::move(*ReadBuffer);
      Entry.ModTime = ReadModTime;
      Entry.State = ModuleCacheEntry::S_Read;
    }
    // The written buffer takes precedence over any read buffer.
    llvm::MemoryBuffer *Buffer = Entry.WrittenBuffer ? Entry.WrittenBuffer.get()
                                                     : Entry.ReadBuffer.get();
    Size = Buffer->getBufferSize();
    ModTime = Entry.ModTime;
    // Note: Creates a reference to ReadBuffer or WrittenBuffer.
    return llvm::MemoryBuffer::getMemBuffer(*Buffer,
                                            /*RequiresNullTerminator=*/false);
  }
};
} // namespace

std::shared_ptr<ModuleCache>
dependencies::makeInProcessModuleCache(ModuleCacheEntries &Entries) {
  return std::make_shared<InProcessModuleCache>(Entries);
}
