// Copyright (c) 2019 Google LLC.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <vector>

#include "test/opt/pass_fixture.h"
#include "test/opt/pass_utils.h"

namespace spvtools {
namespace opt {
namespace {

typedef std::tuple<std::string, bool> GenerateWebGPUInitializersParam;

using GlobalVariableTest =
    PassTest<::testing::TestWithParam<GenerateWebGPUInitializersParam>>;
using LocalVariableTest =
    PassTest<::testing::TestWithParam<GenerateWebGPUInitializersParam>>;

using GenerateWebGPUInitializersTest = PassTest<::testing::Test>;

void operator+=(std::vector<const char*>& lhs, const char* rhs) {
  lhs.push_back(rhs);
}

void operator+=(std::vector<const char*>& lhs,
                const std::vector<const char*>& rhs) {
  lhs.reserve(lhs.size() + rhs.size());
  for (auto* c : rhs) lhs.push_back(c);
}

std::string GetGlobalVariableTestString(std::string ptr_str,
                                        std::string var_str,
                                        std::string const_str = "") {
  std::vector<const char*> result = {
      // clang-format off
               "OpCapability Shader",
               "OpCapability VulkanMemoryModel",
               "OpExtension \"SPV_KHR_vulkan_memory_model\"",
               "OpMemoryModel Logical Vulkan",
               "OpEntryPoint Vertex %1 \"shader\"",
       "%uint = OpTypeInt 32 0",
                ptr_str.c_str()};
  // clang-format on

  if (!const_str.empty()) result += const_str.c_str();

  result += {
      // clang-format off
                var_str.c_str(),
     "%uint_0 = OpConstant %uint 0",
       "%void = OpTypeVoid",
          "%7 = OpTypeFunction %void",
          "%1 = OpFunction %void None %7",
          "%8 = OpLabel",
               "OpStore %4 %uint_0",
               "OpReturn",
               "OpFunctionEnd"
      // clang-format on
  };
  return JoinAllInsts(result);
}

std::string GetPointerString(std::string storage_type) {
  std::string result = "%_ptr_";
  result += storage_type + "_uint = OpTypePointer ";
  result += storage_type + " %uint";
  return result;
}

std::string GetGlobalVariableString(std::string storage_type,
                                    bool initialized) {
  std::string result = "%4 = OpVariable %_ptr_";
  result += storage_type + "_uint ";
  result += storage_type;
  if (initialized) result += " %9";
  return result;
}

std::string GetUninitializedGlobalVariableTestString(std::string storage_type) {
  return GetGlobalVariableTestString(
      GetPointerString(storage_type),
      GetGlobalVariableString(storage_type, false));
}

std::string GetNullConstantString() { return "%9 = OpConstantNull %uint"; }

std::string GetInitializedGlobalVariableTestString(std::string storage_type) {
  return GetGlobalVariableTestString(
      GetPointerString(storage_type),
      GetGlobalVariableString(storage_type, true), GetNullConstantString());
}

TEST_P(GlobalVariableTest, Check) {
  std::string storage_class = std::get<0>(GetParam());
  bool changed = std::get<1>(GetParam());
  std::string input = GetUninitializedGlobalVariableTestString(storage_class);
  std::string expected =
      changed ? GetInitializedGlobalVariableTestString(storage_class) : input;

  SinglePassRunAndCheck<GenerateWebGPUInitializersPass>(input, expected,
                                                        /* skip_nop = */ false);
}

// clang-format off
INSTANTIATE_TEST_SUITE_P(
    GenerateWebGPUInitializers, GlobalVariableTest,
    ::testing::ValuesIn(std::vector<GenerateWebGPUInitializersParam>({
       std::make_tuple("Private", true),
       std::make_tuple("Output", true),
       std::make_tuple("Function", true),
       std::make_tuple("UniformConstant", false),
       std::make_tuple("Input", false),
       std::make_tuple("Uniform", false),
       std::make_tuple("Workgroup", false)
    })));
// clang-format on

std::string GetLocalVariableTestString(std::string ptr_str, std::string var_str,
                                       std::string const_str = "") {
  std::vector<const char*> result = {
      // clang-format off
               "OpCapability Shader",
               "OpCapability VulkanMemoryModel",
               "OpExtension \"SPV_KHR_vulkan_memory_model\"",
               "OpMemoryModel Logical Vulkan",
               "OpEntryPoint Vertex %1 \"shader\"",
       "%uint = OpTypeInt 32 0",
                ptr_str.c_str(),
     "%uint_0 = OpConstant %uint 0",
       "%void = OpTypeVoid",
          "%6 = OpTypeFunction %void"};
  // clang-format on

  if (!const_str.empty()) result += const_str.c_str();

  result += {
      // clang-format off
          "%1 = OpFunction %void None %6",
          "%7 = OpLabel",
                var_str.c_str(),
               "OpStore %8 %uint_0"
      // clang-format on
  };
  return JoinAllInsts(result);
}

std::string GetLocalVariableString(std::string storage_type, bool initialized) {
  std::string result = "%8 = OpVariable %_ptr_";
  result += storage_type + "_uint ";
  result += storage_type;
  if (initialized) result += " %9";
  return result;
}

std::string GetUninitializedLocalVariableTestString(std::string storage_type) {
  return GetLocalVariableTestString(
      GetPointerString(storage_type),
      GetLocalVariableString(storage_type, false));
}

std::string GetInitializedLocalVariableTestString(std::string storage_type) {
  return GetLocalVariableTestString(GetPointerString(storage_type),
                                    GetLocalVariableString(storage_type, true),
                                    GetNullConstantString());
}

TEST_P(LocalVariableTest, Check) {
  std::string storage_class = std::get<0>(GetParam());
  bool changed = std::get<1>(GetParam());

  std::string input = GetUninitializedLocalVariableTestString(storage_class);
  std::string expected =
      changed ? GetInitializedLocalVariableTestString(storage_class) : input;

  SinglePassRunAndCheck<GenerateWebGPUInitializersPass>(input, expected,
                                                        /* skip_nop = */ false);
}

// clang-format off
INSTANTIATE_TEST_SUITE_P(
    GenerateWebGPUInitializers, LocalVariableTest,
    ::testing::ValuesIn(std::vector<GenerateWebGPUInitializersParam>({
       std::make_tuple("Private", true),
       std::make_tuple("Output", true),
       std::make_tuple("Function", true),
       std::make_tuple("UniformConstant", false),
       std::make_tuple("Input", false),
       std::make_tuple("Uniform", false),
       std::make_tuple("Workgroup", false)
    })));
// clang-format on

TEST_F(GenerateWebGPUInitializersTest, AlreadyInitializedUnchanged) {
  std::vector<const char*> spirv = {
      // clang-format off
                       "OpCapability Shader",
                       "OpCapability VulkanMemoryModel",
                       "OpExtension \"SPV_KHR_vulkan_memory_model\"",
                       "OpMemoryModel Logical Vulkan",
                       "OpEntryPoint Vertex %1 \"shader\"",
               "%uint = OpTypeInt 32 0",
  "%_ptr_Private_uint = OpTypePointer Private %uint",
             "%uint_0 = OpConstant %uint 0",
                  "%5 = OpVariable %_ptr_Private_uint Private %uint_0",
               "%void = OpTypeVoid",
                  "%7 = OpTypeFunction %void",
                  "%1 = OpFunction %void None %7",
                  "%8 = OpLabel",
                       "OpReturn",
                       "OpFunctionEnd"
      // clang-format on
  };
  std::string str = JoinAllInsts(spirv);

  SinglePassRunAndCheck<GenerateWebGPUInitializersPass>(str, str,
                                                        /* skip_nop = */ false);
}

TEST_F(GenerateWebGPUInitializersTest, AmbigiousArrays) {
  std::vector<const char*> input_spirv = {
      // clang-format off
                                   "OpCapability Shader",
                                   "OpCapability VulkanMemoryModel",
                                   "OpExtension \"SPV_KHR_vulkan_memory_model\"",
                                   "OpMemoryModel Logical Vulkan",
                                   "OpEntryPoint Vertex %1 \"shader\"",
                           "%uint = OpTypeInt 32 0",
                         "%uint_2 = OpConstant %uint 2",
               "%_arr_uint_uint_2 = OpTypeArray %uint %uint_2",
             "%_arr_uint_uint_2_0 = OpTypeArray %uint %uint_2",
  "%_ptr_Private__arr_uint_uint_2 = OpTypePointer Private %_arr_uint_uint_2",
"%_ptr_Private__arr_uint_uint_2_0 = OpTypePointer Private %_arr_uint_uint_2_0",
                              "%8 = OpConstantNull %_arr_uint_uint_2_0",
                              "%9 = OpVariable %_ptr_Private__arr_uint_uint_2 Private",
                             "%10 = OpVariable %_ptr_Private__arr_uint_uint_2_0 Private %8",
                           "%void = OpTypeVoid",
                             "%12 = OpTypeFunction %void",
                              "%1 = OpFunction %void None %12",
                             "%13 = OpLabel",
                                   "OpReturn",
                                   "OpFunctionEnd"
      // clang-format on
  };
  std::string input_str = JoinAllInsts(input_spirv);

  std::vector<const char*> expected_spirv = {
      // clang-format off
                                   "OpCapability Shader",
                                   "OpCapability VulkanMemoryModel",
                                   "OpExtension \"SPV_KHR_vulkan_memory_model\"",
                                   "OpMemoryModel Logical Vulkan",
                                   "OpEntryPoint Vertex %1 \"shader\"",
                           "%uint = OpTypeInt 32 0",
                         "%uint_2 = OpConstant %uint 2",
               "%_arr_uint_uint_2 = OpTypeArray %uint %uint_2",
             "%_arr_uint_uint_2_0 = OpTypeArray %uint %uint_2",
  "%_ptr_Private__arr_uint_uint_2 = OpTypePointer Private %_arr_uint_uint_2",
"%_ptr_Private__arr_uint_uint_2_0 = OpTypePointer Private %_arr_uint_uint_2_0",
                              "%8 = OpConstantNull %_arr_uint_uint_2_0",
                             "%14 = OpConstantNull %_arr_uint_uint_2",
                              "%9 = OpVariable %_ptr_Private__arr_uint_uint_2 Private %14",
                             "%10 = OpVariable %_ptr_Private__arr_uint_uint_2_0 Private %8",
                           "%void = OpTypeVoid",
                             "%12 = OpTypeFunction %void",
                              "%1 = OpFunction %void None %12",
                             "%13 = OpLabel",
                                   "OpReturn",
                                   "OpFunctionEnd"
      // clang-format on
  };
  std::string expected_str = JoinAllInsts(expected_spirv);

  SinglePassRunAndCheck<GenerateWebGPUInitializersPass>(input_str, expected_str,
                                                        /* skip_nop = */ false);
}

TEST_F(GenerateWebGPUInitializersTest, AmbigiousStructs) {
  std::vector<const char*> input_spirv = {
      // clang-format off
                          "OpCapability Shader",
                          "OpCapability VulkanMemoryModel",
                          "OpExtension \"SPV_KHR_vulkan_memory_model\"",
                          "OpMemoryModel Logical Vulkan",
                          "OpEntryPoint Vertex %1 \"shader\"",
                  "%uint = OpTypeInt 32 0",
             "%_struct_3 = OpTypeStruct %uint",
             "%_struct_4 = OpTypeStruct %uint",
"%_ptr_Private__struct_3 = OpTypePointer Private %_struct_3",
"%_ptr_Private__struct_4 = OpTypePointer Private %_struct_4",
                     "%7 = OpConstantNull %_struct_3",
                     "%8 = OpVariable %_ptr_Private__struct_3 Private %7",
                     "%9 = OpVariable %_ptr_Private__struct_4 Private",
                  "%void = OpTypeVoid",
                    "%11 = OpTypeFunction %void",
                     "%1 = OpFunction %void None %11",
                    "%12 = OpLabel",
                          "OpReturn",
                          "OpFunctionEnd"
      // clang-format on
  };
  std::string input_str = JoinAllInsts(input_spirv);

  std::vector<const char*> expected_spirv = {
      // clang-format off
                          "OpCapability Shader",
                          "OpCapability VulkanMemoryModel",
                          "OpExtension \"SPV_KHR_vulkan_memory_model\"",
                          "OpMemoryModel Logical Vulkan",
                          "OpEntryPoint Vertex %1 \"shader\"",
                  "%uint = OpTypeInt 32 0",
             "%_struct_3 = OpTypeStruct %uint",
             "%_struct_4 = OpTypeStruct %uint",
"%_ptr_Private__struct_3 = OpTypePointer Private %_struct_3",
"%_ptr_Private__struct_4 = OpTypePointer Private %_struct_4",
                     "%7 = OpConstantNull %_struct_3",
                     "%8 = OpVariable %_ptr_Private__struct_3 Private %7",
                    "%13 = OpConstantNull %_struct_4",
                     "%9 = OpVariable %_ptr_Private__struct_4 Private %13",
                  "%void = OpTypeVoid",
                    "%11 = OpTypeFunction %void",
                     "%1 = OpFunction %void None %11",
                    "%12 = OpLabel",
                          "OpReturn",
                          "OpFunctionEnd"
      // clang-format on
  };
  std::string expected_str = JoinAllInsts(expected_spirv);

  SinglePassRunAndCheck<GenerateWebGPUInitializersPass>(input_str, expected_str,
                                                        /* skip_nop = */ false);
}

}  // namespace
}  // namespace opt
}  // namespace spvtools
