/******************************************************************************
 * The MIT License (MIT)
 *
 * Copyright (c) 2025-2026 Baldur Karlsson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/

#include "vk_test.h"

RD_TEST(VK_Groupshared, VulkanGraphicsTest)
{
  static constexpr const char *Description = "Test of compute shader that uses groupshared memory.";

  std::string comp = R"EOSHADER(
#version 460 core

#define MAX_THREADS 64

layout(push_constant) uniform PushData
{
  uint test;
} push;

layout(binding = 0, std430) buffer indataBuf
{
  float indata[MAX_THREADS];
};

layout(binding = 1, std430) buffer outdataBuf
{
  vec4 outdata[MAX_THREADS];
};

shared float gsmData[MAX_THREADS];
shared int gsmIntData[MAX_THREADS];
shared int gInt;

#define IsTest(x) (push.test == x)

float GetGSMValue(uint i)
{
  return gsmData[i % MAX_THREADS];
}

int GetGSMIntValue(uint i)
{
  return gsmIntData[i % MAX_THREADS];
}

layout(local_size_x = MAX_THREADS, local_size_y = 1, local_size_z = 1) in;

#define GroupMemoryBarrierWithGroupSync() memoryBarrierShared();groupMemoryBarrier();barrier();

void main()
{
  uvec3 gid = gl_LocalInvocationID;

  if(gl_LocalInvocationID.x == 0)
  {
    for(int i=0; i < MAX_THREADS; i++) gsmData[i] = 1.25f;
    for(int i=0; i < MAX_THREADS; i++) gsmIntData[i] = 125;
    gInt = 25;
  }

  GroupMemoryBarrierWithGroupSync();

  vec4 outval = vec4(0.0);
  int u = int(gid.x);

  if (IsTest(0))
  {
    // first write, should be the init value for all threads
    outval.x = GetGSMValue(gid.x);

    gsmData[gid.x] = indata[gid.x];

    // second write, should be the read value because we're reading our own value
    outval.y = GetGSMValue(gid.x);

    GroupMemoryBarrierWithGroupSync();

    // third write, should be our pairwise neighbour's value
    outval.z = GetGSMValue(gid.x ^ 1);

    // do calculation with our neighbour
    float value = (1.0f + GetGSMValue(gid.x)) * (1.0f + GetGSMValue(gid.x ^ 1));
    GroupMemoryBarrierWithGroupSync();
    gsmData[gid.x] = value;
    GroupMemoryBarrierWithGroupSync();

    // fourth write, our neighbour should be identical to our value
    outval.w = GetGSMValue(gid.x) == GetGSMValue(gid.x ^ 1) ? 9.99f : -9.99f;
  }
  else if (IsTest(1))
  {
    gsmData[gid.x] = float(gid.x);
    gsmData[gid.x] += 10.0f;
    GroupMemoryBarrierWithGroupSync();

    outval.x = GetGSMValue(gid.x);
    outval.y = GetGSMValue(gid.x + 1);

    GroupMemoryBarrierWithGroupSync();
    gsmData[gid.x] += 10.0f;
    GroupMemoryBarrierWithGroupSync();

    outval.z = GetGSMValue(gid.x + 2);

    GroupMemoryBarrierWithGroupSync();
    gsmData[gid.x] += 10.0f;
    GroupMemoryBarrierWithGroupSync();

    outval.w = GetGSMValue(gid.x + 3);
  }
  else if (IsTest(2))
  {
    // Deliberately no sync to test debugger behaviour not GPU correctness
    // Debugger should see the initial value of 1.25f for all of GSM
    gsmData[gid.x] = float(gid.x);
    outval.x = GetGSMValue(gid.x);
    outval.y = GetGSMValue(gid.x + 1);
    outval.z = GetGSMValue(gid.x + 2);
    outval.w = GetGSMValue(gid.x + 3);
  }
  else if (IsTest(3))
  {
    int value = int(indata[gid.x] * 100.0);
    gsmIntData[gid.x] = u;
    GroupMemoryBarrierWithGroupSync();
    atomicAdd(gsmIntData[u], value);
    atomicAdd(gsmIntData[u], -value);
    GroupMemoryBarrierWithGroupSync();
    outval.x = float(GetGSMIntValue(u+0));
    outval.y = float(GetGSMIntValue(u+1));
    outval.z = float(GetGSMIntValue(u+2));
    outval.w = float(GetGSMIntValue(u+3));
  }
  else if (IsTest(4))
  {
    int value = int(indata[gid.x] * 100.0);
    gsmIntData[gid.x] = u;
    GroupMemoryBarrierWithGroupSync();
    atomicAnd(gsmIntData[u], value);
    GroupMemoryBarrierWithGroupSync();
    outval.x = float(GetGSMIntValue(u+0));
    outval.y = float(GetGSMIntValue(u+1));
    outval.z = float(GetGSMIntValue(u+2));
    outval.w = float(GetGSMIntValue(u+3));
  }
  else if (IsTest(5))
  {
    int value = int(indata[gid.x] * 100.0);
    gsmIntData[gid.x] = u;
    GroupMemoryBarrierWithGroupSync();
    atomicOr(gsmIntData[u], value);
    GroupMemoryBarrierWithGroupSync();
    outval.x = float(GetGSMIntValue(u+0));
    outval.y = float(GetGSMIntValue(u+1));
    outval.z = float(GetGSMIntValue(u+2));
    outval.w = float(GetGSMIntValue(u+3));
  }
  else if (IsTest(6))
  {
    int value = int(indata[gid.x] * 100.0);
    gsmIntData[gid.x] = u;
    GroupMemoryBarrierWithGroupSync();
    atomicXor(gsmIntData[u], value);
    atomicXor(gsmIntData[u], value);
    GroupMemoryBarrierWithGroupSync();
    outval.x = float(GetGSMIntValue(u+0));
    outval.y = float(GetGSMIntValue(u+1));
    outval.z = float(GetGSMIntValue(u+2));
    outval.w = float(GetGSMIntValue(u+3));
  }
  else if (IsTest(7))
  {
    int value = int(indata[gid.x] * 100.0);
    gsmIntData[gid.x] = u;
    GroupMemoryBarrierWithGroupSync();
    atomicMin(gsmIntData[u], value);
    GroupMemoryBarrierWithGroupSync();
    outval.x = float(GetGSMIntValue(u+0));
    outval.y = float(GetGSMIntValue(u+1));
    outval.z = float(GetGSMIntValue(u+2));
    outval.w = float(GetGSMIntValue(u+3));
  }
  else if (IsTest(8))
  {
    int value = int(indata[gid.x] * 100.0);
    gsmIntData[gid.x] = u;
    GroupMemoryBarrierWithGroupSync();
    atomicMax(gsmIntData[u], value);
    GroupMemoryBarrierWithGroupSync();
    outval.x = float(GetGSMIntValue(u+0));
    outval.y = float(GetGSMIntValue(u+1));
    outval.z = float(GetGSMIntValue(u+2));
    outval.w = float(GetGSMIntValue(u+3));
  }
  else if (IsTest(9))
  {
    int value = int(indata[gid.x] * 100.0);
    gsmIntData[gid.x] = u;
    GroupMemoryBarrierWithGroupSync();
    int original = atomicExchange(gsmIntData[u], value);
    GroupMemoryBarrierWithGroupSync();
    outval.x = float(GetGSMIntValue(u+0));
    outval.y = float(GetGSMIntValue(u+1));
    outval.z = float(GetGSMIntValue(u+2));
    outval.w = float(GetGSMIntValue(u+3));
  }
  else if (IsTest(10))
  {
    int value = int(indata[gid.x] * 100.0);
    gsmIntData[gid.x] = u;
    GroupMemoryBarrierWithGroupSync();
    int original = atomicCompSwap(gsmIntData[u], value, value+1);
    GroupMemoryBarrierWithGroupSync();
    outval.x = float(GetGSMIntValue(u+0));
    outval.y = float(GetGSMIntValue(u+1));
    outval.z = float(GetGSMIntValue(u+2));
    outval.w = float(GetGSMIntValue(u+3));
  }
  else if (IsTest(11))
  {
    int value = int(indata[gid.x] * 100.0);
    gsmIntData[gid.x] = u;
    GroupMemoryBarrierWithGroupSync();
    atomicCompSwap(gsmIntData[u], value, value+1);
    GroupMemoryBarrierWithGroupSync();
    outval.x = float(GetGSMIntValue(u+0));
    outval.y = float(GetGSMIntValue(u+1));
    outval.z = float(GetGSMIntValue(u+2));
    outval.w = float(GetGSMIntValue(u+3));
  }
  else if (IsTest(12))
  {
    GroupMemoryBarrierWithGroupSync();
    outval.x = gInt;
    GroupMemoryBarrierWithGroupSync();
    atomicAdd(gInt,1);
    GroupMemoryBarrierWithGroupSync();
    outval.y = gInt;
    GroupMemoryBarrierWithGroupSync();
    atomicAdd(gInt,1);
    GroupMemoryBarrierWithGroupSync();
    outval.z = gInt;
    GroupMemoryBarrierWithGroupSync();
    atomicAdd(gInt,1);
    GroupMemoryBarrierWithGroupSync();
    outval.w = gInt;
  }

  outdata[gid.x] = outval;
}

)EOSHADER";

  int main()
  {
    // initialise, create window, create context, etc
    if(!Init())
      return 3;

    VkDescriptorSetLayout setLayout = createDescriptorSetLayout(vkh::DescriptorSetLayoutCreateInfo({
        {0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT},
        {1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT},
    }));
    VkPipelineLayout layout = createPipelineLayout(vkh::PipelineLayoutCreateInfo(
        {setLayout}, {vkh::PushConstantRange(VK_SHADER_STAGE_ALL, 0, 4)}));

    VkPipeline pipe = createComputePipeline(vkh::ComputePipelineCreateInfo(
        layout, CompileShaderModule(comp, ShaderLang::glsl, ShaderStage::comp)));

    VkDescriptorSet descSet = allocateDescriptorSet(setLayout);

    float values[64];
    for(int i = 0; i < 64; i++)
      values[i] = RANDF(1.0f, 100.0f);
    AllocatedBuffer inBuf(this,
                          vkh::BufferCreateInfo(sizeof(values), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT),
                          VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_CPU_TO_GPU}));
    inBuf.upload(values);

    AllocatedBuffer outBuf(
        this,
        vkh::BufferCreateInfo(sizeof(Vec4f) * 64, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
                                                      VK_BUFFER_USAGE_TRANSFER_DST_BIT),
        VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_GPU_ONLY}));

    vkh::updateDescriptorSets(
        device, {
                    vkh::WriteDescriptorSet(descSet, 0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
                                            {vkh::DescriptorBufferInfo(inBuf.buffer)}),
                    vkh::WriteDescriptorSet(descSet, 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
                                            {vkh::DescriptorBufferInfo(outBuf.buffer)}),
                });

    int numCompTests = 0;
    size_t pos = 0;
    while(pos != std::string::npos)
    {
      pos = comp.find("IsTest(", pos);
      if(pos == std::string::npos)
        break;
      pos += sizeof("IsTest(") - 1;
      numCompTests = std::max(numCompTests, atoi(comp.c_str() + pos) + 1);
    }

    while(Running())
    {
      VkCommandBuffer cmd = GetCommandBuffer();

      vkBeginCommandBuffer(cmd, vkh::CommandBufferBeginInfo());

      VkImage swapimg = StartUsingBackbuffer(cmd);

      vkh::cmdClearImage(cmd, swapimg, vkh::ClearColorValue(0.2f, 0.2f, 0.2f, 1.0f));

      vkh::cmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, layout, 0, {descSet}, {});
      vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipe);

      pushMarker(cmd, "Compute Tests");
      for(int i = 0; i < numCompTests; ++i)
      {
        vkh::cmdPipelineBarrier(
            cmd, {},
            {vkh::BufferMemoryBarrier(VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
                                      outBuf.buffer)});

        vkCmdFillBuffer(cmd, outBuf.buffer, 0, sizeof(Vec4f) * 64, 0);
        vkh::cmdPipelineBarrier(
            cmd, {},
            {vkh::BufferMemoryBarrier(VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_WRITE_BIT,
                                      outBuf.buffer)});

        vkh::cmdPushConstants(cmd, layout, i);
        vkCmdDispatch(cmd, 1, 1, 1);
      }
      popMarker(cmd);

      FinishUsingBackbuffer(cmd);

      vkEndCommandBuffer(cmd);

      SubmitAndPresent({cmd});
    }

    return 0;
  }
};

REGISTER_TEST();
