/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
*/

// Regression test for issue #47: the fused UI uber-kernel
// (shaders/ui-fused.comp.glsl) and its C++ push-constant mirror
// (Crafter::UIFusedHeader). The fused kernel is large and hand-written, and
// its push-constant block must stay byte-for-byte identical to UIFusedHeader
// or DispatchFused silently corrupts every fused frame. This test:
//
//   1. compiles the real ui-fused.comp.glsl with glslang (same target-env the
//      engine builds it with), proving the uber-kernel still compiles and its
//      GL_GOOGLE_include of ui-shared.glsl resolves;
//   2. runs spirv-val over the result under the engine's block-layout flags;
//   3. asserts exactly one push-constant variable survives (a well-formed
//      single block, like every other UI shader);
//   4. parses the push-constant struct's member Offset decorations and asserts
//      they are exactly {0,16,32,48,64,80,96,112} — the layout of
//      UIFusedHeader (four uvec4 headers at 0/16/32/48 then four vec4 clip
//      rects at 64/80/96/112, 128 bytes total). Any add/remove/reorder of a
//      member shifts these offsets and fails here, catching a GLSL/C++ drift
//      that the C++-side static_assert(sizeof==128) alone cannot.
//
// glslang and spirv-val are invoked at runtime, so the test declares them as
// required tools (see project.cpp). It needs no GPU device.

#include <cstdlib>

import Crafter.Graphics;   // for the static_assert(sizeof(UIFusedHeader)==128)
import std;
using namespace Crafter;

namespace {

namespace fs = std::filesystem;

int RunCommand(const std::string& cmd) {
    int status = std::system(cmd.c_str());
    if (status == -1) return -1;
    return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f);
}

std::vector<std::uint32_t> ReadSpirv(const fs::path& p) {
    std::ifstream f(p, std::ios::binary | std::ios::ate);
    if (!f) return {};
    std::streamsize size = f.tellg();
    f.seekg(0);
    std::vector<std::uint32_t> words(static_cast<std::size_t>(size) / sizeof(std::uint32_t));
    f.read(reinterpret_cast<char*>(words.data()), size);
    return words;
}

// Count OpVariable instructions in the PushConstant storage class (SC == 9).
int CountPushConstantVariables(const std::vector<std::uint32_t>& words) {
    constexpr std::uint32_t OpVariable = 59;
    constexpr std::uint32_t StorageClassPushConstant = 9;
    int count = 0;
    for (std::size_t i = 5; i < words.size();) {
        std::uint32_t len = words[i] >> 16;
        if (len == 0 || i + len > words.size()) break;
        if ((words[i] & 0xFFFFu) == OpVariable && len >= 4 && words[i + 3] == StorageClassPushConstant)
            ++count;
        i += len;
    }
    return count;
}

// The single PushConstant struct's member byte offsets, sorted ascending.
// Finds the OpTypePointer in the PushConstant storage class (SC == 9), takes
// the struct type it points at, and collects every Offset (decoration 35)
// declared on that struct via OpMemberDecorate.
std::vector<std::uint32_t> PushConstantMemberOffsets(const std::vector<std::uint32_t>& words) {
    constexpr std::uint32_t OpTypePointer    = 32;
    constexpr std::uint32_t OpMemberDecorate  = 72;
    constexpr std::uint32_t StorageClassPushConstant = 9;
    constexpr std::uint32_t DecorationOffset  = 35;

    std::uint32_t structId = 0;
    for (std::size_t i = 5; i < words.size();) {
        std::uint32_t len = words[i] >> 16;
        if (len == 0 || i + len > words.size()) break;
        // OpTypePointer: [op][result id][storage class][pointee type id]
        if ((words[i] & 0xFFFFu) == OpTypePointer && len >= 4 &&
            words[i + 2] == StorageClassPushConstant) {
            structId = words[i + 3];
            break;
        }
        i += len;
    }
    if (structId == 0) return {};

    std::vector<std::uint32_t> offsets;
    for (std::size_t i = 5; i < words.size();) {
        std::uint32_t len = words[i] >> 16;
        if (len == 0 || i + len > words.size()) break;
        // OpMemberDecorate: [op][struct id][member][decoration][operands...]
        if ((words[i] & 0xFFFFu) == OpMemberDecorate && len >= 5 &&
            words[i + 1] == structId && words[i + 3] == DecorationOffset) {
            offsets.push_back(words[i + 4]);
        }
        i += len;
    }
    std::ranges::sort(offsets);
    offsets.erase(std::ranges::unique(offsets).begin(), offsets.end());
    return offsets;
}

// Locate shaders/ui-fused.comp.glsl whether the test runs from the build
// output dir or the project root (mirrors ShapeTextCache's font probing).
fs::path LocateShader() {
    const std::array<fs::path, 4> candidates = {
        "shaders/ui-fused.comp.glsl",
        "../shaders/ui-fused.comp.glsl",
        "../../shaders/ui-fused.comp.glsl",
        "../../../shaders/ui-fused.comp.glsl",
    };
    for (const auto& c : candidates) {
        std::error_code ec;
        if (fs::exists(c, ec)) return c;
    }
    return {};
}

} // namespace

int main() {
    // Sanity: the C++ mirror is the size the shader layout assumes.
    static_assert(sizeof(UIFusedHeader) == 128);

    const fs::path src = LocateShader();
    if (src.empty()) {
        std::println(std::cerr, "could not locate shaders/ui-fused.comp.glsl from {}",
                     fs::current_path().string());
        return 1;
    }

    const fs::path dir = fs::temp_directory_path() / "crafter-uifused-test";
    std::error_code ec;
    fs::create_directories(dir, ec);
    const fs::path spv = dir / "ui-fused.comp.spv";

    // 1. Compile the real uber-kernel. GL_GOOGLE_include resolves
    //    ui-shared.glsl relative to the source directory.
    std::string compile = "glslang --target-env vulkan1.4 -V -S comp \""
        + src.string() + "\" -o \"" + spv.string() + "\" > /dev/null";
    if (RunCommand(compile) != 0) {
        std::println(std::cerr, "glslang failed to compile ui-fused.comp.glsl");
        return 1;
    }

    std::vector<std::uint32_t> words = ReadSpirv(spv);
    if (words.size() < 5) {
        std::println(std::cerr, "could not read compiled SPIR-V");
        return 1;
    }

    // 2. spirv-val under the engine's relaxed block-layout flags.
    std::string validate = "spirv-val \"" + spv.string()
        + "\" --relax-block-layout --scalar-block-layout --target-env vulkan1.4";
    if (RunCommand(validate) != 0) {
        std::println(std::cerr, "spirv-val rejected ui-fused.comp.spv");
        return 1;
    }

    // 3. Exactly one push-constant block.
    int pcVars = CountPushConstantVariables(words);
    if (pcVars != 1) {
        std::println(std::cerr, "expected exactly 1 push-constant variable, found {}", pcVars);
        return 1;
    }

    // 4. Member offsets must match UIFusedHeader's layout exactly.
    const std::vector<std::uint32_t> expected = {0, 16, 32, 48, 64, 80, 96, 112};
    std::vector<std::uint32_t> offsets = PushConstantMemberOffsets(words);
    if (offsets != expected) {
        std::print(std::cerr, "push-constant member offsets mismatch — expected {{");
        for (auto o : expected) std::print(std::cerr, "{} ", o);
        std::print(std::cerr, "}}, got {{");
        for (auto o : offsets) std::print(std::cerr, "{} ", o);
        std::println(std::cerr, "}}");
        return 1;
    }

    std::println(std::cout,
        "ui-fused shader ok (push-constant vars: {}, member offsets pinned to UIFusedHeader, 128 bytes)",
        pcVars);
    return 0;
}
