/**
 * @file shaders.hpp
 * @brief SPIR-V shader compilation and caching system.
 *
 * This file provides functionality for compiling GLSL shaders to SPIR-V and
 * managing a shader cache system. It supports automatic recompilation when
 * source files are modified and efficient caching of compiled shaders.
 */

#pragma once

#include <filesystem>
#include <fmt/format.h>
#include <fstream>
#include <ios>
#include <shaderc/shaderc.hpp>
#include <string>
#include <vector>

#include "types.hpp"

/**
 * @brief Handles shader compilation and caching operations.
 *
 * This class provides static methods for compiling GLSL shaders to SPIR-V
 * and managing a cache system. It automatically detects when shaders need
 * to be recompiled based on file timestamps and provides efficient caching
 * of compiled shader binaries.
 */
class ShaderCompiler {
 public:
  ShaderCompiler() = default;

  /**
   * @brief Compiles or retrieves a cached SPIR-V shader.
   *
   * @param shaderPath Path to the GLSL shader source file
   * @param kind Type of shader (vertex, fragment, compute, etc.)
   * @return std::vector<u32> Compiled SPIR-V binary code
   * @throws std::runtime_error If shader compilation fails or file is not found
   *
   * This function performs the following steps:
   * 1. Checks if a cached version exists and is up-to-date
   * 2. Loads from cache if available and valid
   * 3. Otherwise, compiles the shader from source
   * 4. Caches the newly compiled shader for future use
   * 5. Returns the SPIR-V binary code
   */
  static fn getCompiledShader(const std::filesystem::path& shaderPath, const shaderc_shader_kind& kind)
    -> std::vector<u32> {
    using namespace std;

    // Convert to absolute path if relative
    filesystem::path absPath = filesystem::absolute(shaderPath);

    if (!filesystem::exists(absPath))
      throw runtime_error("Shader file not found: " + absPath.string());

    const string           shaderName = absPath.stem().string();
    const filesystem::path cacheFile  = getCacheFilePath(shaderName);

    // Check if we need to recompile by comparing timestamps
    if (filesystem::exists(cacheFile)) {
      const auto sourceTime = filesystem::last_write_time(absPath);
      const auto cacheTime  = filesystem::last_write_time(cacheFile);

      if (cacheTime >= sourceTime) {
        // Cache is up to date, load it
        vector<u32> spirvCode = loadCachedShader(cacheFile);
        if (!spirvCode.empty()) {
          fmt::println("Loaded shader from cache: {}", cacheFile.string());
          return spirvCode;
        }
      }
    }

    // Need to compile the shader
    fmt::println("Compiling shader: {}", absPath.string());

    // Read shader source
    ifstream file(absPath);
    if (!file)
      throw runtime_error("Failed to open shader file: " + absPath.string());

    string shaderSource((istreambuf_iterator<char>(file)), istreambuf_iterator<char>());
    file.close();

    // Compile the shader
    vector<u32> spirvCode = compileShader(shaderSource.c_str(), kind);

    if (spirvCode.empty())
      throw runtime_error("Shader compilation failed for: " + absPath.string());

    // Cache the compiled SPIR-V binary
    saveCompiledShader(spirvCode, cacheFile.string());
    return spirvCode;
  }

 private:
  /**
   * @brief Determines the platform-specific shader cache directory.
   *
   * @param shaderName Base name of the shader file
   * @return std::filesystem::path Full path to the cache file
   *
   * Cache locations:
   * - Windows: %LOCALAPPDATA%/VulkanApp/Shaders/
   * - macOS: ~/Library/Application Support/VulkanApp/Shaders/
   * - Linux: ~/.local/share/VulkanApp/Shaders/
   *
   * The directory is created if it doesn't exist.
   */
  static fn getCacheFilePath(const string& shaderName) -> std::filesystem::path {
    using namespace std::filesystem;

#ifdef _WIN32
    path cacheDir = path(getenv("LOCALAPPDATA")) / "VulkanApp" / "Shaders";
#elif defined(__APPLE__)
    path cacheDir = path(getenv("HOME")) / "Library" / "Application Support" / "VulkanApp" / "Shaders";
#else // Assume Linux or other UNIX-like systems
    path cacheDir = path(getenv("HOME")) / ".local" / "share" / "VulkanApp" / "Shaders";
#endif

    if (!exists(cacheDir))
      create_directories(cacheDir);

    return cacheDir / (shaderName + ".spv");
  }

  /**
   * @brief Loads a cached SPIR-V shader from disk.
   *
   * @param cachePath Path to the cached shader file
   * @return std::vector<u32> SPIR-V binary code, empty if loading fails
   *
   * Reads the binary SPIR-V data from the cache file. Returns an empty
   * vector if the file cannot be opened or read properly.
   */
  static fn loadCachedShader(const std::filesystem::path& cachePath) -> std::vector<u32> {
    std::ifstream file(cachePath, std::ios::binary);
    if (!file.is_open())
      return {};

    // Read file size
    file.seekg(0, std::ios::end);
    const std::streamoff fileSize = file.tellg();
    file.seekg(0, std::ios::beg);

    // Allocate buffer and read data
    std::vector<u32> buffer(static_cast<u32>(fileSize) / sizeof(u32));
    file.read(std::bit_cast<char*>(buffer.data()), fileSize);

    return buffer;
  }

  /**
   * @brief Compiles GLSL source code to SPIR-V.
   *
   * @param source GLSL shader source code
   * @param kind Type of shader being compiled
   * @return std::vector<u32> Compiled SPIR-V binary code
   *
   * Uses the shaderc library to compile GLSL to SPIR-V. The compilation
   * is performed with optimization level set to performance and generates
   * debug information in debug builds.
   */
  static fn compileShader(const char* source, shaderc_shader_kind kind) -> std::vector<u32> {
    shaderc::Compiler       compiler;
    shaderc::CompileOptions options;

    // Set compilation options
#ifdef NDEBUG
    options.SetOptimizationLevel(shaderc_optimization_level_performance);
#else
    options.SetOptimizationLevel(shaderc_optimization_level_zero);
    options.SetGenerateDebugInfo();
#endif

    // Compile the shader
    shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(source, kind, "shader", options);

    if (module.GetCompilationStatus() != shaderc_compilation_status_success)
      return {};

    return { module.cbegin(), module.cend() };
  }

  /**
   * @brief Saves compiled SPIR-V code to the cache.
   *
   * @param spirv Compiled SPIR-V binary code
   * @param cachePath Path where the cache file should be saved
   * @return bool True if save was successful, false otherwise
   *
   * Writes the SPIR-V binary to disk for future use. Creates any
   * necessary parent directories if they don't exist.
   */
  static fn saveCompiledShader(const std::vector<u32>& spirv, const std::string& cachePath) -> bool {
    std::ofstream file(cachePath, std::ios::binary);
    if (!file.is_open())
      return false;

    file.write(
      std::bit_cast<const char*>(spirv.data()), static_cast<std::streamsize>(spirv.size() * sizeof(u32))
    );

    return file.good();
  }
};
