//==---------- persistent_device_code_cache.hpp -----------------*- 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
//
//===----------------------------------------------------------------------===//

#pragma once

#include <detail/config.hpp>
#include <detail/device_binary_image.hpp>
#include <fcntl.h>
#include <string>
#include <sycl/detail/os_util.hpp>
#include <sycl/detail/ur.hpp>
#include <sycl/detail/util.hpp>
#include <sycl/device.hpp>
#include <sys/stat.h>
#include <thread>
#include <vector>

namespace sycl {
inline namespace _V1 {
namespace detail {

/* The class manages inter-process synchronization:
 *  - Path passed to the constructor is appended with .lock and used as lock
 *    file.
 *  - All operations are not blocking and failure ignoring (diagnostic may be
 *    sent to std::cerr when SYCL_CACHE_TRACE environment variable is set).
 *  - There are two modes of accessing shared resource:
 *    - write access assumes that lock is acquired (object is created and
 *      isOwned() method confirms that current executor owns the lock);
 *    - read access checks that the lock is not acquired for write by others
 *      with the help of isLocked() method.
 */
class LockCacheItem {
private:
  const std::string FileName;
  bool Owned = false;
  static const char LockSuffix[];

public:
  LockCacheItem(const std::string &Path);

  bool isOwned() { return Owned; }
  static bool isLocked(const std::string &Path) {
    return OSUtil::isPathPresent(Path + LockSuffix);
  }
  ~LockCacheItem();
};
/* End of temporary solution*/

class PersistentDeviceCodeCache {
  /* The device code images are stored on file system using structure below:
   * <cache_root>/
   *     <device_hash>/
   *         <device_image_hash>/
   *             <spec_constants_values_hash>/
   *                 <build_options_hash>/
   *                     <n>.src
   *                     <n>.bin
   *                     .lock
   *   <cache_root>                 - root directory storing cache files;
   *   <device_hash>                - hash out of device information used to
   *                                  identify target device;
   *   <device_image_hash>          - hash made out of device images used as
   *                                  input for the JIT compilation;
   *   <spec_constants_values_hash> - hash for specialization constants values;
   *   <build_options_hash>         - hash for all build options;
   *   <n>                          - sequential number of hash collisions.
   *                                  When hashes match for the specific build
   *                                  but full values don't, new cache item is
   *                                  added with incremented value(enumeration
   *                                  started from 0).
   * Two files per cache item are stored on disk:
   *   <n>.src  - contains full values for build parameters (device information,
   *              specialization constant values, build options, device images)
   *              which is used to resolve hash collisions and analysis of
   *              cached items.
   *   <n>.bin  - contains built device code.
   *   <n>.lock - cache item lock file. It is created when data is saved to
   *              filesystem. On read operation the absence of file is checked
   *              but it is not created to avoid lock.
   * All filesystem operation failures are not treated as SYCL errors and
   * ignored. If such errors happen warning messages are written to std::cerr
   * and:
   *  - on cache write operation cache item is not created;
   *  - on cache read operation it is treated as cache miss.
   */
private:
  /* Write built binary to persistent cache
   * Format: NumBinaries(=1), BinarySize, Binary
   * The reason why we need to write a number of binaries (always 1 in current
   * implementation) is to keep compatibility with the old format of files in
   * the persistent cache, so that new runtime can use binaries from the
   * persistent cache generated by an old compiler/runtime. NumBinaries can be
   * removed at next ABI breaking window.
   */
  static void writeBinaryDataToFile(const std::string &FileName,
                                    const std::vector<char> &Data);

  /* Read built binary from persistent cache
   * Format: NumBinaries(=1), BinarySize, Binary
   * See comment above regarding the reason why we need NumBinaries.
   */
  static std::vector<char> readBinaryDataFromFile(const std::string &FileName);

  /* Writing cache item key sources to be used for reliable identification
   * Format: Four pairs of [size, value] for device, build options,
   * specialization constant values, device code SPIR-V images.
   */
  static void
  writeSourceItem(const std::string &FileName, const device &Device,
                  const std::vector<const RTDeviceBinaryImage *> &SortedImgs,
                  const SerializedObj &SpecConsts,
                  const std::string &BuildOptionsString);

  /* Check that cache item key sources are equal to the current program
   */
  static bool isCacheItemSrcEqual(
      const std::string &FileName, const device &Device,
      const std::vector<const RTDeviceBinaryImage *> &SortedImgs,
      const SerializedObj &SpecConsts, const std::string &BuildOptionsString);

  /* Form string representing device version */
  static std::string getDeviceIDString(const device &Device);

  /* Returns true if specified images should be cached on disk. It checks if
   * cache is enabled, images have SPIRV type and match thresholds. */
  static bool areImagesCacheable(
      const std::vector<const RTDeviceBinaryImage *> &SortedImgs);

  /* Returns value of specified parameter. Default value is used if failure
   * happens during obtaining value. */
  template <ConfigID Config>
  static unsigned long getNumParam(unsigned long Default) {
    auto Value = SYCLConfig<Config>::get();
    try {
      if (Value)
        return std::stol(Value);
    } catch (std::exception const &) {
      PersistentDeviceCodeCache::trace("Invalid value provided, use default " +
                                       std::to_string(Default));
    }
    return Default;
  }

  /* Default value for minimum device code size to be cached on disk in bytes */
  static constexpr unsigned long DEFAULT_MIN_DEVICE_IMAGE_SIZE = 0;

  /* Default value for maximum device code size to be cached on disk in bytes */
  static constexpr unsigned long DEFAULT_MAX_DEVICE_IMAGE_SIZE =
      1024 * 1024 * 1024;

public:
  /* Returns the path to directory storing persistent device code cache.*/
  static std::string getRootDir();

  /* Check if on-disk cache enabled.
   */
  static bool isEnabled();

  /* Get directory name for storing current cache item
   */
  static std::string
  getCacheItemPath(const device &Device,
                   const std::vector<const RTDeviceBinaryImage *> &SortedImgs,
                   const SerializedObj &SpecConsts,
                   const std::string &BuildOptionsString);

  /*  Get directory name when storing runtime compiled kernels ( via
   * kernel_compiler ).
   */
  static std::string
  getCompiledKernelItemPath(const device &Device,
                            const std::string &BuildOptionsString,
                            const std::string &SourceString);

  /* Program binaries built for one or more devices are read from persistent
   * cache and returned in form of vector of programs. Each binary program is
   * stored in vector of chars.
   */
  static std::vector<std::vector<char>>
  getItemFromDisc(const std::vector<device> &Devices,
                  const std::vector<const RTDeviceBinaryImage *> &Imgs,
                  const SerializedObj &SpecConsts,
                  const std::string &BuildOptionsString);

  static std::vector<std::vector<char>>
  getCompiledKernelFromDisc(const std::vector<device> &Devices,
                            const std::string &BuildOptionsString,
                            const std::string &SourceStr);

  /* Stores build program in persistent cache
   */
  static void
  putItemToDisc(const std::vector<device> &Devices,
                const std::vector<const RTDeviceBinaryImage *> &Imgs,
                const SerializedObj &SpecConsts,
                const std::string &BuildOptionsString,
                const ur_program_handle_t &NativePrg);

  static void putCompiledKernelToDisc(const std::vector<device> &Devices,
                                      const std::string &BuildOptionsString,
                                      const std::string &SourceStr,
                                      const ur_program_handle_t &NativePrg);

  /* Sends message to std:cerr stream when SYCL_CACHE_TRACE environemnt is set*/
  static void trace(const std::string &msg, const std::string &path = "") {
    static const bool traceEnabled =
        SYCLConfig<SYCL_CACHE_TRACE>::isTraceDiskCache();
    if (traceEnabled) {
      auto outputPath = path;
      std::replace(outputPath.begin(), outputPath.end(), '\\', '/');
      std::cerr << "[Persistent Cache]: " << msg << outputPath << std::endl;
    }
  }
  static void trace_KernelCompiler(const std::string &msg,
                                   const std::string &path = "") {
    static const bool traceEnabled =
        SYCLConfig<SYCL_CACHE_TRACE>::isTraceKernelCompiler();
    if (traceEnabled) {
      auto outputPath = path;
      std::replace(outputPath.begin(), outputPath.end(), '\\', '/');
      std::cerr << "[kernel_compiler Persistent Cache]: " << msg << outputPath
                << std::endl;
    }
  }

private:
  // Check if cache_size.lock file is present in the cache root directory.
  // If not, create it and populate it with the size of the cache directory.
  static void repopulateCacheSizeFile(const std::string &CacheRoot);

  // Update the cache size file and trigger cache eviction if needed.
  static void
  updateCacheFileSizeAndTriggerEviction(const std::string &CacheRoot,
                                        size_t CacheSize);

  // Evict LRU items from the cache to make space for new items.
  static void evictItemsFromCache(const std::string &CacheRoot,
                                  size_t CacheSize, size_t MaxCacheSize);

  static void saveCurrentTimeInAFile(std::string FileName);

  // Check if eviction is enabled.
  static bool isEvictionEnabled() {
    return SYCLConfig<SYCL_CACHE_MAX_SIZE>::isPersistentCacheEvictionEnabled();
  }

  // Suffix for access time file. Every cache entry will have one.
  static inline std::string CacheEntryAccessTimeSuffix = "_access_time.txt";
  // Suffix for eviction in progress file. It is created when eviction is
  // triggered and removed when eviction is done.
  static inline std::string EvictionInProgressFileSuffix =
      "_eviction_in_progress";
};
} // namespace detail
} // namespace _V1
} // namespace sycl
