// Copyright (c) 2017 Google Inc.
//
// 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.

// Validates OpCapability instruction.

#include <cassert>
#include <string>

#include "source/opcode.h"
#include "source/val/instruction.h"
#include "source/val/validate.h"
#include "source/val/validation_state.h"

namespace spvtools {
namespace val {
namespace {

bool IsSupportGuaranteedVulkan_1_0(uint32_t capability) {
  switch (spv::Capability(capability)) {
    case spv::Capability::Matrix:
    case spv::Capability::Shader:
    case spv::Capability::InputAttachment:
    case spv::Capability::Sampled1D:
    case spv::Capability::Image1D:
    case spv::Capability::SampledBuffer:
    case spv::Capability::ImageBuffer:
    case spv::Capability::ImageQuery:
    case spv::Capability::DerivativeControl:
      return true;
    default:
      break;
  }
  return false;
}

bool IsSupportGuaranteedVulkan_1_1(uint32_t capability) {
  if (IsSupportGuaranteedVulkan_1_0(capability)) return true;
  switch (spv::Capability(capability)) {
    case spv::Capability::DeviceGroup:
    case spv::Capability::MultiView:
      return true;
    default:
      break;
  }
  return false;
}

bool IsSupportGuaranteedVulkan_1_2(uint32_t capability) {
  if (IsSupportGuaranteedVulkan_1_1(capability)) return true;
  switch (spv::Capability(capability)) {
    case spv::Capability::ShaderNonUniform:
      return true;
    default:
      break;
  }
  return false;
}

bool IsSupportOptionalVulkan_1_0(uint32_t capability) {
  switch (spv::Capability(capability)) {
    case spv::Capability::Geometry:
    case spv::Capability::Tessellation:
    case spv::Capability::Float64:
    case spv::Capability::Int64:
    case spv::Capability::Int16:
    case spv::Capability::TessellationPointSize:
    case spv::Capability::GeometryPointSize:
    case spv::Capability::ImageGatherExtended:
    case spv::Capability::StorageImageMultisample:
    case spv::Capability::UniformBufferArrayDynamicIndexing:
    case spv::Capability::SampledImageArrayDynamicIndexing:
    case spv::Capability::StorageBufferArrayDynamicIndexing:
    case spv::Capability::StorageImageArrayDynamicIndexing:
    case spv::Capability::ClipDistance:
    case spv::Capability::CullDistance:
    case spv::Capability::ImageCubeArray:
    case spv::Capability::SampleRateShading:
    case spv::Capability::SparseResidency:
    case spv::Capability::MinLod:
    case spv::Capability::SampledCubeArray:
    case spv::Capability::ImageMSArray:
    case spv::Capability::StorageImageExtendedFormats:
    case spv::Capability::InterpolationFunction:
    case spv::Capability::StorageImageReadWithoutFormat:
    case spv::Capability::StorageImageWriteWithoutFormat:
    case spv::Capability::MultiViewport:
    case spv::Capability::Int64Atomics:
    case spv::Capability::TransformFeedback:
    case spv::Capability::GeometryStreams:
    case spv::Capability::Float16:
    case spv::Capability::Int8:
      return true;
    default:
      break;
  }
  return false;
}

bool IsSupportOptionalVulkan_1_1(uint32_t capability) {
  if (IsSupportOptionalVulkan_1_0(capability)) return true;

  switch (spv::Capability(capability)) {
    case spv::Capability::GroupNonUniform:
    case spv::Capability::GroupNonUniformVote:
    case spv::Capability::GroupNonUniformArithmetic:
    case spv::Capability::GroupNonUniformBallot:
    case spv::Capability::GroupNonUniformShuffle:
    case spv::Capability::GroupNonUniformShuffleRelative:
    case spv::Capability::GroupNonUniformClustered:
    case spv::Capability::GroupNonUniformQuad:
    case spv::Capability::DrawParameters:
    // Alias spv::Capability::StorageBuffer16BitAccess.
    case spv::Capability::StorageUniformBufferBlock16:
    // Alias spv::Capability::UniformAndStorageBuffer16BitAccess.
    case spv::Capability::StorageUniform16:
    case spv::Capability::StoragePushConstant16:
    case spv::Capability::StorageInputOutput16:
    case spv::Capability::DeviceGroup:
    case spv::Capability::MultiView:
    case spv::Capability::VariablePointersStorageBuffer:
    case spv::Capability::VariablePointers:
      return true;
    default:
      break;
  }
  return false;
}

bool IsSupportOptionalVulkan_1_2(uint32_t capability) {
  if (IsSupportOptionalVulkan_1_1(capability)) return true;

  switch (spv::Capability(capability)) {
    case spv::Capability::DenormPreserve:
    case spv::Capability::DenormFlushToZero:
    case spv::Capability::SignedZeroInfNanPreserve:
    case spv::Capability::RoundingModeRTE:
    case spv::Capability::RoundingModeRTZ:
    case spv::Capability::VulkanMemoryModel:
    case spv::Capability::VulkanMemoryModelDeviceScope:
    case spv::Capability::StorageBuffer8BitAccess:
    case spv::Capability::UniformAndStorageBuffer8BitAccess:
    case spv::Capability::StoragePushConstant8:
    case spv::Capability::ShaderViewportIndex:
    case spv::Capability::ShaderLayer:
    case spv::Capability::PhysicalStorageBufferAddresses:
    case spv::Capability::RuntimeDescriptorArray:
    case spv::Capability::UniformTexelBufferArrayDynamicIndexing:
    case spv::Capability::StorageTexelBufferArrayDynamicIndexing:
    case spv::Capability::UniformBufferArrayNonUniformIndexing:
    case spv::Capability::SampledImageArrayNonUniformIndexing:
    case spv::Capability::StorageBufferArrayNonUniformIndexing:
    case spv::Capability::StorageImageArrayNonUniformIndexing:
    case spv::Capability::InputAttachmentArrayNonUniformIndexing:
    case spv::Capability::UniformTexelBufferArrayNonUniformIndexing:
    case spv::Capability::StorageTexelBufferArrayNonUniformIndexing:
      return true;
    default:
      break;
  }
  return false;
}

bool IsSupportGuaranteedOpenCL_1_2(uint32_t capability, bool embedded_profile) {
  switch (spv::Capability(capability)) {
    case spv::Capability::Addresses:
    case spv::Capability::Float16Buffer:
    case spv::Capability::Int16:
    case spv::Capability::Int8:
    case spv::Capability::Kernel:
    case spv::Capability::Linkage:
    case spv::Capability::Vector16:
      return true;
    case spv::Capability::Int64:
      return !embedded_profile;
    default:
      break;
  }
  return false;
}

bool IsSupportGuaranteedOpenCL_2_0(uint32_t capability, bool embedded_profile) {
  if (IsSupportGuaranteedOpenCL_1_2(capability, embedded_profile)) return true;

  switch (spv::Capability(capability)) {
    case spv::Capability::DeviceEnqueue:
    case spv::Capability::GenericPointer:
    case spv::Capability::Groups:
    case spv::Capability::Pipes:
      return true;
    default:
      break;
  }
  return false;
}

bool IsSupportGuaranteedOpenCL_2_2(uint32_t capability, bool embedded_profile) {
  if (IsSupportGuaranteedOpenCL_2_0(capability, embedded_profile)) return true;

  switch (spv::Capability(capability)) {
    case spv::Capability::SubgroupDispatch:
    case spv::Capability::PipeStorage:
      return true;
    default:
      break;
  }
  return false;
}

bool IsSupportOptionalOpenCL_1_2(uint32_t capability) {
  switch (spv::Capability(capability)) {
    case spv::Capability::ImageBasic:
    case spv::Capability::Float64:
      return true;
    default:
      break;
  }
  return false;
}

// Checks if |capability| was enabled by extension.
bool IsEnabledByExtension(ValidationState_t& _, uint32_t capability) {
  spv_operand_desc operand_desc = nullptr;
  _.grammar().lookupOperand(SPV_OPERAND_TYPE_CAPABILITY, capability,
                            &operand_desc);

  // operand_desc is expected to be not null, otherwise validator would have
  // failed at an earlier stage. This 'assert' is 'just in case'.
  assert(operand_desc);

  ExtensionSet operand_exts(operand_desc->numExtensions,
                            operand_desc->extensions);
  if (operand_exts.empty()) return false;

  return _.HasAnyOfExtensions(operand_exts);
}

bool IsEnabledByCapabilityOpenCL_1_2(ValidationState_t& _,
                                     uint32_t capability) {
  if (_.HasCapability(spv::Capability::ImageBasic)) {
    switch (spv::Capability(capability)) {
      case spv::Capability::LiteralSampler:
      case spv::Capability::Sampled1D:
      case spv::Capability::Image1D:
      case spv::Capability::SampledBuffer:
      case spv::Capability::ImageBuffer:
        return true;
      default:
        break;
    }
    return false;
  }
  return false;
}

bool IsEnabledByCapabilityOpenCL_2_0(ValidationState_t& _,
                                     uint32_t capability) {
  if (_.HasCapability(spv::Capability::ImageBasic)) {
    switch (spv::Capability(capability)) {
      case spv::Capability::ImageReadWrite:
      case spv::Capability::LiteralSampler:
      case spv::Capability::Sampled1D:
      case spv::Capability::Image1D:
      case spv::Capability::SampledBuffer:
      case spv::Capability::ImageBuffer:
        return true;
      default:
        break;
    }
    return false;
  }
  return false;
}

}  // namespace

// Validates that capability declarations use operands allowed in the current
// context.
spv_result_t CapabilityPass(ValidationState_t& _, const Instruction* inst) {
  if (inst->opcode() != spv::Op::OpCapability) return SPV_SUCCESS;

  assert(inst->operands().size() == 1);

  const spv_parsed_operand_t& operand = inst->operand(0);

  assert(operand.num_words == 1);
  assert(operand.offset < inst->words().size());

  const uint32_t capability = inst->word(operand.offset);
  const auto capability_str = [&_, capability]() {
    spv_operand_desc desc = nullptr;
    if (_.grammar().lookupOperand(SPV_OPERAND_TYPE_CAPABILITY, capability,
                                  &desc) != SPV_SUCCESS ||
        !desc) {
      return std::string("Unknown");
    }
    return std::string(desc->name);
  };

  const auto env = _.context()->target_env;
  const bool opencl_embedded = env == SPV_ENV_OPENCL_EMBEDDED_1_2 ||
                               env == SPV_ENV_OPENCL_EMBEDDED_2_0 ||
                               env == SPV_ENV_OPENCL_EMBEDDED_2_1 ||
                               env == SPV_ENV_OPENCL_EMBEDDED_2_2;
  const std::string opencl_profile = opencl_embedded ? "Embedded" : "Full";
  if (env == SPV_ENV_VULKAN_1_0) {
    if (!IsSupportGuaranteedVulkan_1_0(capability) &&
        !IsSupportOptionalVulkan_1_0(capability) &&
        !IsEnabledByExtension(_, capability)) {
      return _.diag(SPV_ERROR_INVALID_CAPABILITY, inst)
             << "Capability " << capability_str()
             << " is not allowed by Vulkan 1.0 specification"
             << " (or requires extension)";
    }
  } else if (env == SPV_ENV_VULKAN_1_1) {
    if (!IsSupportGuaranteedVulkan_1_1(capability) &&
        !IsSupportOptionalVulkan_1_1(capability) &&
        !IsEnabledByExtension(_, capability)) {
      return _.diag(SPV_ERROR_INVALID_CAPABILITY, inst)
             << "Capability " << capability_str()
             << " is not allowed by Vulkan 1.1 specification"
             << " (or requires extension)";
    }
  } else if (env == SPV_ENV_VULKAN_1_2) {
    if (!IsSupportGuaranteedVulkan_1_2(capability) &&
        !IsSupportOptionalVulkan_1_2(capability) &&
        !IsEnabledByExtension(_, capability)) {
      return _.diag(SPV_ERROR_INVALID_CAPABILITY, inst)
             << "Capability " << capability_str()
             << " is not allowed by Vulkan 1.2 specification"
             << " (or requires extension)";
    }
  } else if (env == SPV_ENV_OPENCL_1_2 || env == SPV_ENV_OPENCL_EMBEDDED_1_2) {
    if (!IsSupportGuaranteedOpenCL_1_2(capability, opencl_embedded) &&
        !IsSupportOptionalOpenCL_1_2(capability) &&
        !IsEnabledByExtension(_, capability) &&
        !IsEnabledByCapabilityOpenCL_1_2(_, capability)) {
      return _.diag(SPV_ERROR_INVALID_CAPABILITY, inst)
             << "Capability " << capability_str()
             << " is not allowed by OpenCL 1.2 " << opencl_profile
             << " Profile specification"
             << " (or requires extension or capability)";
    }
  } else if (env == SPV_ENV_OPENCL_2_0 || env == SPV_ENV_OPENCL_EMBEDDED_2_0 ||
             env == SPV_ENV_OPENCL_2_1 || env == SPV_ENV_OPENCL_EMBEDDED_2_1) {
    if (!IsSupportGuaranteedOpenCL_2_0(capability, opencl_embedded) &&
        !IsSupportOptionalOpenCL_1_2(capability) &&
        !IsEnabledByExtension(_, capability) &&
        !IsEnabledByCapabilityOpenCL_2_0(_, capability)) {
      return _.diag(SPV_ERROR_INVALID_CAPABILITY, inst)
             << "Capability " << capability_str()
             << " is not allowed by OpenCL 2.0/2.1 " << opencl_profile
             << " Profile specification"
             << " (or requires extension or capability)";
    }
  } else if (env == SPV_ENV_OPENCL_2_2 || env == SPV_ENV_OPENCL_EMBEDDED_2_2) {
    if (!IsSupportGuaranteedOpenCL_2_2(capability, opencl_embedded) &&
        !IsSupportOptionalOpenCL_1_2(capability) &&
        !IsEnabledByExtension(_, capability) &&
        !IsEnabledByCapabilityOpenCL_2_0(_, capability)) {
      return _.diag(SPV_ERROR_INVALID_CAPABILITY, inst)
             << "Capability " << capability_str()
             << " is not allowed by OpenCL 2.2 " << opencl_profile
             << " Profile specification"
             << " (or requires extension or capability)";
    }
  }

  return SPV_SUCCESS;
}

}  // namespace val
}  // namespace spvtools
