// Copyright 2018 The clvk authors.
//
// 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.

#pragma once

#include <array>
#include <atomic>
#include <climits>
#include <cstdint>
#include <fstream>
#include <map>
#include <unordered_map>
#include <vector>

#include <vulkan/vulkan.h>

#include "spirv-tools/libspirv.h"
#include "spirv/1.0/spirv.hpp"

#include "config.hpp"
#include "init.hpp"
#include "log.hpp"
#include "memory.hpp"
#include "objects.hpp"
#include "printf.hpp"
#include "utils.hpp"

const int SPIR_WORD_SIZE = 4;

enum class kernel_argument_kind
{
    buffer,
    buffer_ubo,
    pod,
    pod_ubo,
    pod_pushconstant,
    pointer_ubo,
    pointer_pushconstant,
    sampled_image,
    storage_image,
    storage_texel_buffer,
    uniform_texel_buffer,
    sampler,
    local,
    unused,
};

struct kernel_argument_info {
    std::string name;
    bool extended_valid = false;
    std::string type_name;
    uint32_t address_qualifier;
    uint32_t access_qualifier;
    uint32_t type_qualifier;

    bool is_vec3() const {
        return extended_valid && type_name[type_name.length() - 1] == '3';
    }
};

struct kernel_argument {
    kernel_argument_info info;
    uint32_t pos;
    uint32_t descriptorSet;
    uint32_t binding;
    uint32_t offset;
    uint32_t size;
    kernel_argument_kind kind;
    uint32_t local_spec_id;
    uint32_t local_elem_size;

    bool is_pod() const {
        return (kind == kernel_argument_kind::pod) ||
               (kind == kernel_argument_kind::pod_ubo) ||
               (kind == kernel_argument_kind::pod_pushconstant) ||
               (kind == kernel_argument_kind::pointer_ubo) ||
               (kind == kernel_argument_kind::pointer_pushconstant);
    }

    bool is_pod_buffer() const {
        return (kind == kernel_argument_kind::pod) ||
               (kind == kernel_argument_kind::pod_ubo) ||
               (kind == kernel_argument_kind::pointer_ubo);
    }

    bool is_pod_pointer() const {
        return (kind == kernel_argument_kind::pointer_pushconstant) ||
               (kind == kernel_argument_kind::pointer_ubo);
    }

    bool is_vec3() const { return info.is_vec3(); }

    bool is_mem_object_backed() const {
        return (kind == kernel_argument_kind::buffer) ||
               (kind == kernel_argument_kind::buffer_ubo) ||
               (kind == kernel_argument_kind::sampled_image) ||
               (kind == kernel_argument_kind::storage_image) ||
               (kind == kernel_argument_kind::storage_texel_buffer) ||
               (kind == kernel_argument_kind::uniform_texel_buffer);
    }

    bool is_unused() const { return kind == kernel_argument_kind::unused; }
};

struct sampler_desc {
    uint32_t descriptorSet;
    uint32_t binding;
    bool normalized_coords;
    cl_addressing_mode addressing_mode;
    cl_filter_mode filter_mode;
};

enum class pushconstant
{
    global_offset,
    enqueued_local_size,
    global_size,
    region_offset,
    num_workgroups,
    region_group_offset,
    image_metadata,
    module_constants_pointer,
    printf_buffer_pointer,
    normalized_sampler_mask,
};

struct pushconstant_desc {
    uint32_t offset;
    uint32_t size;
};

enum class spec_constant
{
    workgroup_size_x,
    workgroup_size_y,
    workgroup_size_z,
    work_dim,
    global_offset_x,
    global_offset_y,
    global_offset_z,
    subgroup_max_size,
};

enum class module_buffer_type
{
    storage_buffer,
    pointer_push_constant,
};

struct user_spec_constant_data {
    std::string type;
    uint32_t size;
    bool set;
    union {
        uint8_t i8;
        uint16_t i16;
        uint32_t i32;
        uint64_t i64;
    } data;

    user_spec_constant_data(std::string type, uint32_t size)
        : type(type), size(size), set(false) {}

    void init_data(size_t size, const void* value) {
        if (type == "i8" || type == "i1") {
            std::memcpy(&data.i8, value, size);
        } else if (type == "i16" || type == "f16") {
            std::memcpy(&data.i16, value, size);
        } else if (type == "i32" || type == "f32") {
            std::memcpy(&data.i32, value, size);
        } else if (type == "i64" || type == "f64") {
            std::memcpy(&data.i64, value, size);
        } else {
            CVK_ASSERT(false && "Unexpected specialisation constant type");
        }
        set = true;
    }
};

struct constant_data_buffer_info {
    module_buffer_type type;
    uint32_t set;
    uint32_t binding;
    uint32_t pc_offset;
    std::vector<char> data;
};

struct printf_buffer_desc_info {
    module_buffer_type type;
    uint32_t set;
    uint32_t binding;
    uint32_t pc_offset;
    uint32_t size = 0;
};

struct spirv_validation_options {
    bool uniform_buffer_std_layout = false;
};

struct image_metadata {
    image_metadata() : order_offset(UINT_MAX), data_type_offset(UINT_MAX) {}
    uint32_t order_offset;
    uint32_t data_type_offset;
    void set_order(uint32_t order) { order_offset = order; }
    void set_data_type(uint32_t data_type) { data_type_offset = data_type; }
    bool has_valid_order() const { return order_offset != UINT_MAX; }
    bool has_valid_data_type() const { return data_type_offset != UINT_MAX; }
};

using kernel_image_metadata_map =
    std::unordered_map<uint32_t, struct image_metadata>;
using image_metadata_map =
    std::unordered_map<std::string, kernel_image_metadata_map>;

using kernel_sampler_metadata_map = std::unordered_map<uint32_t, uint32_t>;
using sampler_metadata_map =
    std::unordered_map<std::string, kernel_sampler_metadata_map>;

class spir_binary {

    using kernels_arguments_map =
        std::unordered_map<std::string, std::vector<kernel_argument>>;
    using kernels_reqd_work_group_size_map =
        std::unordered_map<std::string, std::array<uint32_t, 3>>;
    using kernels_flags_map = std::unordered_map<std::string, uint32_t>;

public:
    spir_binary(spv_target_env env)
        : m_loaded_from_binary(false), m_target_env(env),
          m_workgroup_variables_size(0) {
        m_context = spvContextCreate(env);
    }
    ~spir_binary() { spvContextDestroy(m_context); }
    CHECK_RETURN bool load(const char* fname);
    CHECK_RETURN bool load(std::istream& istream, uint32_t size);
    CHECK_RETURN bool load_descriptor_map();
    CHECK_RETURN bool save(std::ostream& ostream) const;
    CHECK_RETURN bool save(const char* fname) const;
    CHECK_RETURN bool read(const unsigned char* src, size_t size);
    CHECK_RETURN bool write(unsigned char* dst) const;
    size_t size() const;
    bool loaded_from_binary() const { return m_loaded_from_binary; }
    size_t spir_size() const { return m_code.size() * sizeof(uint32_t); }
    const uint32_t* spir_data() const { return m_code.data(); }
    void use(std::vector<uint32_t>&& src);
    void set_target_env(spv_target_env env);
    const std::vector<uint32_t>& code() const { return m_code; };
    CHECK_RETURN bool validate(const spirv_validation_options&) const;
    size_t num_kernels() const { return m_dmaps.size(); }
    const kernels_arguments_map& kernels_arguments() const { return m_dmaps; }
    const sampler_metadata_map& sampler_metadata() const {
        return m_sampler_metadata;
    }
    const image_metadata_map& image_metadata() const {
        return m_image_metadata;
    }
    std::vector<uint32_t>* raw_binary() { return &m_code; }
    const std::vector<sampler_desc>& literal_samplers() {
        return m_literal_samplers;
    }
    const std::array<uint32_t, 3>&
    required_work_group_size(const std::string& kernel) const {
        return m_reqd_work_group_sizes.at(kernel);
    }
    CHECK_RETURN bool
    get_capabilities(std::vector<spv::Capability>& capabilities) const;
    static constexpr uint32_t MAX_DESCRIPTOR_SETS = 3;

    const std::unordered_map<pushconstant, pushconstant_desc>&
    push_constants() const {
        return m_push_constants;
    }

    const std::unordered_map<spec_constant, uint32_t>& spec_constants() const {
        return m_spec_constants;
    }

    CHECK_RETURN const pushconstant_desc* push_constant(pushconstant pc) const {
        if (m_push_constants.count(pc) != 0) {
            return &m_push_constants.at(pc);
        } else {
            return nullptr;
        }
    }

    const printf_descriptor_map_t& printf_descriptors() const {
        return m_printf_descriptors;
    }

    void add_kernel(const std::string& name, uint32_t num_args,
                    const std::string& attributes, uint32_t flags) {
        m_flags[name] = flags;
        auto& args = m_dmaps[name];
        kernel_argument unused = {
            {}, 0, 0, 0, 0, 0, kernel_argument_kind::unused, 0, 0};
        // Generate a placeholder for each argument in the kernel.
        args.resize(num_args, unused);
        uint32_t pos = 0;
        // Assign the argument ordinals. Any used argument will overwrite these,
        // but they are necessary for unused arguments.
        for (auto& arg : args) {
            arg.pos = pos++;
        }
        m_reqd_work_group_sizes[name] = {0, 0, 0};
        m_kernels_attributes[name] = attributes;
    }

    const std::unordered_map<std::string, std::string>&
    kernels_attributes() const {
        return m_kernels_attributes;
    }

    void add_kernel_argument(const std::string& name, kernel_argument&& arg) {
        // Overwrite the placeholder argument.
        m_dmaps[name][arg.pos] = std::move(arg);
    }

    void add_spec_constant(spec_constant constant, uint32_t spec_id) {
        m_spec_constants[constant] = spec_id;
    }

    void add_push_constant(pushconstant pc, pushconstant_desc&& desc) {
        m_push_constants[pc] = desc;
    }

    void add_literal_sampler(sampler_desc&& desc) {
        m_literal_samplers.push_back(desc);
    }

    void set_required_work_group_size(const std::string& kernel, uint32_t x,
                                      uint32_t y, uint32_t z) {
        m_reqd_work_group_sizes[kernel] = {x, y, z};
    }

    bool strip_reflection(std::vector<uint32_t>* stripped);

    const constant_data_buffer_info* constant_data_buffer() const {
        return m_constant_data_buffer.get();
    }

    void set_constant_data_buffer(const constant_data_buffer_info& info) {
        m_constant_data_buffer.reset(new constant_data_buffer_info(info));
    }

    void add_sampler_metadata(const std::string& name, uint32_t ordinal,
                              uint32_t offset) {
        m_sampler_metadata[name][ordinal] = offset;
    }

    void add_image_channel_order_metadata(const std::string& name,
                                          uint32_t ordinal, uint32_t offset) {
        m_image_metadata[name][ordinal].set_order(offset);
    }
    void add_image_channel_data_type_metadata(const std::string& name,
                                              uint32_t ordinal,
                                              uint32_t offset) {
        m_image_metadata[name][ordinal].set_data_type(offset);
    }

    void add_printf_descriptor(printf_descriptor&& desc) {
        m_printf_descriptors[desc.printf_id] = desc;
    }

    void set_printf_buffer_info(const printf_buffer_desc_info& info) {
        m_printf_buffer_info = info;
    }

    const printf_buffer_desc_info& printf_buffer_info() const {
        return m_printf_buffer_info;
    }

    const printf_descriptor_map_t& get_printf_descriptors() const {
        return m_printf_descriptors;
    }

    const kernels_flags_map& kernels_flags() const { return m_flags; }

    void add_workgroup_variable_size(uint32_t size) {
        m_workgroup_variables_size += size;
    }

    uint32_t get_workgroup_variables_size() const {
        return m_workgroup_variables_size;
    }

private:
    spv_context m_context;
    std::vector<uint32_t> m_code;
    std::vector<sampler_desc> m_literal_samplers;
    std::unordered_map<pushconstant, pushconstant_desc> m_push_constants;
    std::unordered_map<spec_constant, uint32_t> m_spec_constants;
    sampler_metadata_map m_sampler_metadata;
    image_metadata_map m_image_metadata;
    std::unordered_map<uint32_t, printf_descriptor> m_printf_descriptors;
    printf_buffer_desc_info m_printf_buffer_info;
    std::unique_ptr<constant_data_buffer_info> m_constant_data_buffer;
    kernels_arguments_map m_dmaps;
    kernels_reqd_work_group_size_map m_reqd_work_group_sizes;
    std::unordered_map<std::string, std::string> m_kernels_attributes;
    kernels_flags_map m_flags;
    bool m_loaded_from_binary;
    spv_target_env m_target_env;
    uint32_t m_workgroup_variables_size;
};

enum class build_operation
{
    build,
    build_binary,
    compile,
    link
};

using cvk_program_callback = void(CL_CALLBACK*)(cl_program, void*);

using cvk_spec_constant_map = std::map<uint32_t, uint32_t>;

struct cvk_program;

class cvk_entry_point {
public:
    cvk_entry_point(cvk_device* dev, cvk_program* program,
                    const std::string& name);

    ~cvk_entry_point() {
        VkDevice vkdev = m_device->vulkan_device();
        for (auto pipeline : m_pipelines) {
            cvk_info("destroying pipeline %p for kernel %s", pipeline.second,
                     m_name.c_str());
            vkDestroyPipeline(vkdev, pipeline.second, nullptr);
        }
        if (m_descriptor_pool != VK_NULL_HANDLE) {
            vkDestroyDescriptorPool(vkdev, m_descriptor_pool, nullptr);
        }
        if (m_pipeline_layout != VK_NULL_HANDLE) {
            vkDestroyPipelineLayout(vkdev, m_pipeline_layout, nullptr);
        }
        for (auto layout : m_descriptor_set_layouts) {
            vkDestroyDescriptorSetLayout(vkdev, layout, nullptr);
        }
    }

    CHECK_RETURN cl_int init();

    CHECK_RETURN VkPipeline
    create_pipeline(const cvk_spec_constant_map& spec_constants);

    CHECK_RETURN bool allocate_descriptor_sets(VkDescriptorSet* ds);

    void free_descriptor_set(VkDescriptorSet ds) {
        TRACE_FUNCTION();
        std::lock_guard<std::mutex> lock(m_descriptor_pool_lock);
        vkFreeDescriptorSets(m_device->vulkan_device(), m_descriptor_pool, 1,
                             &ds);
        m_nb_descriptor_set_allocated--;
        TRACE_CNT(descriptor_set_allocated_counter,
                  m_nb_descriptor_set_allocated);
    }

    uint32_t num_set_layouts() const { return m_descriptor_set_layouts.size(); }

    std::unique_ptr<cvk_buffer> allocate_pod_buffer();

    const std::vector<kernel_argument>& args() const { return m_args; }

    const kernel_sampler_metadata_map* sampler_metadata() const {
        return m_sampler_metadata;
    }

    const kernel_image_metadata_map* image_metadata() const {
        return m_image_metadata;
    }

    bool has_pod_arguments() const { return m_has_pod_arguments; }

    bool has_pod_buffer_arguments() const { return m_has_pod_buffer_arguments; }

    bool has_sampler_metadata() const { return m_sampler_metadata != nullptr; }

    bool has_image_metadata() const { return m_image_metadata != nullptr; }

    uint32_t pod_buffer_size() const { return m_pod_buffer_size; }

    uint32_t num_resource_slots() const { return m_num_resource_slots; }

    VkPipelineLayout pipeline_layout() const { return m_pipeline_layout; }

    VkDescriptorType pod_descriptor_type() const {
        return m_pod_descriptor_type;
    }

    cvk_program* program() const { return m_program; }

    bool uses_printf() const;

private:
    const uint32_t MAX_INSTANCES = config.max_entry_points_instances;

    cvk_device* m_device;
    cvk_context* m_context;
    cvk_program* m_program;
    std::string m_name;
    VkDescriptorType m_pod_descriptor_type;
    uint32_t m_pod_buffer_size;
    bool m_has_pod_arguments;
    bool m_has_pod_buffer_arguments;
    std::vector<kernel_argument> m_args;
    const kernel_sampler_metadata_map* m_sampler_metadata;
    const kernel_image_metadata_map* m_image_metadata;
    uint32_t m_num_resource_slots;
    VkDescriptorPool m_descriptor_pool;
    std::vector<VkDescriptorSetLayout> m_descriptor_set_layouts;
    VkPipelineLayout m_pipeline_layout;

    std::mutex m_pipeline_cache_lock;
    std::mutex m_descriptor_pool_lock;

    using binding_stat_map = std::unordered_map<VkDescriptorType, uint32_t>;
    bool build_descriptor_set_layout(
        const std::vector<VkDescriptorSetLayoutBinding>& bindings);
    bool build_descriptor_sets_layout_bindings_for_arguments(
        binding_stat_map& smap, uint32_t& num_resource_slots);
    bool build_descriptor_sets_layout_bindings_for_literal_samplers(
        binding_stat_map& smap);
    bool build_descriptor_sets_layout_bindings_for_program_scope_buffers(
        binding_stat_map& smap);
    bool build_descriptor_sets_layout_bindings_for_printf_buffer(
        binding_stat_map& smap);

    // Structures for caching pipelines based on specialization constants
    struct SpecConstantMapHash {
        size_t operator()(const cvk_spec_constant_map& spec_constants) const {
            // TODO: better hash?
            size_t result = 0;
            for (auto& entry : spec_constants) {
                result ^= std::hash<uint32_t>{}(entry.first) * 31;
                result ^= std::hash<uint32_t>{}(entry.second) * 59;
            }
            return result;
        }
    };
    struct SpecConstantMapEqual {
        bool operator()(const cvk_spec_constant_map& lhs,
                        const cvk_spec_constant_map& rhs) const {
            if (lhs.size() != rhs.size())
                return false;
            for (auto& lhs_entry : lhs) {
                if (!rhs.count(lhs_entry.first))
                    return false;
                if (lhs_entry.second != rhs.at(lhs_entry.first))
                    return false;
            }
            return true;
        }
    };
    std::unordered_map<cvk_spec_constant_map, VkPipeline, SpecConstantMapHash,
                       SpecConstantMapEqual>
        m_pipelines;

    uint32_t m_nb_descriptor_set_allocated;
    TRACE_CNT_VAR(descriptor_set_allocated_counter);

    bool m_first_allocation_failure;
};

struct cvk_program : public _cl_program, api_object<object_magic::program> {

    cvk_program(cvk_context* ctx)
        : api_object(ctx), m_num_devices(1U),
          m_binary_type(CL_PROGRAM_BINARY_TYPE_NONE),
          m_shader_module(VK_NULL_HANDLE),
          m_binary(m_context->device()->vulkan_spirv_env()) {
        m_dev_status[m_context->device()] = CL_BUILD_NONE;
    }

    cvk_program(cvk_context* ctx, const void* il, size_t length)
        : cvk_program(ctx) {
        m_il.resize(length);
        memcpy(m_il.data(), il, length);
    }

    virtual ~cvk_program() {
        if (m_shader_module != VK_NULL_HANDLE) {
            auto vkdev = m_context->device()->vulkan_device();
            vkDestroyShaderModule(vkdev, m_shader_module, nullptr);
        }
        for (auto& s : m_literal_samplers) {
            s->release();
        }
    }

    void append_source(const char* src, size_t len) {
        if (len != 0) {
            m_source.append(src, len);
        } else {
            m_source.append(src);
        }
    }

    const std::string& source() const { return m_source; }

    const std::vector<uint8_t>& il() const { return m_il; }

    uint32_t num_devices() const { return m_num_devices; }

    cl_program_binary_type binary_type(const cvk_device*) const {
        return m_binary_type;
    }

    bool can_be_linked() const {
        auto dev = m_context->device();
        return ((build_status() == CL_BUILD_SUCCESS) &&
                ((binary_type(dev) == CL_PROGRAM_BINARY_TYPE_COMPILED_OBJECT) ||
                 (binary_type(dev) == CL_PROGRAM_BINARY_TYPE_LIBRARY)));
    }

    CHECK_RETURN cl_int build(build_operation operation, cl_uint num_devices,
                              const cl_device_id* device_list,
                              const char* options, cl_uint num_input_programs,
                              const cl_program* input_programs,
                              const char** header_include_names,
                              cvk_program_callback cb, void* data);

    cl_int set_user_spec_constant(uint32_t spec_id, size_t spec_size,
                                  const void* spec_value) {
        auto spec_const_iter = m_user_spec_constants.find(spec_id);
        if (spec_const_iter == m_user_spec_constants.end()) {
            return CL_INVALID_SPEC_ID;
        }
        if (spec_const_iter->second.size != spec_size) {
            return CL_INVALID_VALUE;
        }
        spec_const_iter->second.init_data(spec_size, spec_value);
        return CL_SUCCESS;
    }

    const std::string& build_options() const { return m_build_options; }

    cl_build_status build_status(const cvk_device* device) const {
        return m_dev_status.at(device);
    }

    cl_build_status build_status() const {
        for (auto& dev_st : m_dev_status) {
            if (dev_st.second != CL_BUILD_SUCCESS) {
                return dev_st.second;
            }
        }

        return CL_BUILD_SUCCESS;
    }

    const std::string& build_log(const cvk_device* device) const {
        UNUSED(device); // TODO support per-device build log
        return m_build_log;
    }

    std::vector<const cvk_device*> devices() const {
        std::vector<const cvk_device*> ret;

        for (auto& dev_st : m_dev_status) {
            ret.push_back(dev_st.first);
        }

        return ret;
    }

    VkShaderModule shader_module() const { return m_shader_module; }

    void complete_operation(cvk_device* device, cl_build_status status) {
        m_dev_status[device] = status;
        if (m_operation_callback != nullptr) {
            m_operation_callback(this, m_operation_callback_data);
        }
        release();
    }

    unsigned num_kernels() const { return m_binary.num_kernels(); }
    bool loaded_from_binary() const { return m_binary.loaded_from_binary(); }
    bool uses_printf() { return !m_binary.printf_descriptors().empty(); }
    const std::unordered_map<uint32_t, printf_descriptor>&
    printf_descriptors() {
        return m_binary.get_printf_descriptors();
    }

    const std::vector<kernel_argument>* args_for_kernel(std::string& name) {
        auto const& args = m_binary.kernels_arguments().find(name);
        if (args != m_binary.kernels_arguments().end()) {
            return &args->second;
        } else {
            return nullptr;
        }
    }

    const kernel_sampler_metadata_map* sampler_metadata(std::string& name) {
        auto const& md = m_binary.sampler_metadata().find(name);
        if (md != m_binary.sampler_metadata().end()) {
            return &md->second;
        } else {
            return nullptr;
        }
    }

    const kernel_image_metadata_map* image_metadata(std::string& name) {
        auto const& md = m_binary.image_metadata().find(name);
        if (md != m_binary.image_metadata().end()) {
            return &md->second;
        } else {
            return nullptr;
        }
    }

private:
    bool read_llvm_bitcode(const unsigned char* src, size_t size);

    void write_binary_header(unsigned char* dst) const;
    CHECK_RETURN cl_program_binary_type

    read_binary_header(const unsigned char* src, size_t size);

public:
    CHECK_RETURN bool read(const unsigned char* src, size_t size);

    CHECK_RETURN bool write(unsigned char* dst) const;

    size_t binary_size() const;

    std::vector<const char*> kernel_names() const {
        std::vector<const char*> ret;
        for (auto& kname_args : m_binary.kernels_arguments()) {
            ret.push_back(kname_args.first.c_str());
        }
        return ret;
    }

    const std::vector<sampler_desc>& literal_sampler_descs() {
        return m_binary.literal_samplers();
    }

    const std::vector<cvk_sampler_holder>& literal_samplers() {
        return m_literal_samplers;
    }

    const VkPushConstantRange& push_constant_range() const {
        return m_push_constant_range;
    }

    CHECK_RETURN const pushconstant_desc* push_constant(pushconstant pc) const {
        return m_binary.push_constant(pc);
    }

    CHECK_RETURN const std::unordered_map<spec_constant, uint32_t>&
    spec_constants() const {
        return m_binary.spec_constants();
    }

    const std::array<uint32_t, 3>&
    required_work_group_size(const std::string& kernel) const {
        return m_binary.required_work_group_size(kernel);
    }

    uint32_t required_sub_group_size(std::string& kernel) const {
        auto forced_subgroup_size_or_zero = []() {
            if (config.force_subgroup_size.set) {
                return config.force_subgroup_size();
            }
            return 0u;
        };

        const char* sub_group_size_attr = "intel_reqd_sub_group_size(";
        auto attrs = kernel_attributes(kernel);
        auto it = attrs.find(sub_group_size_attr);

        if (it == std::string::npos) {
            return forced_subgroup_size_or_zero();
        }
        it += strlen(sub_group_size_attr);
        auto it2 = attrs.substr(it).find(")");
        if (it2 == std::string::npos) {
            return forced_subgroup_size_or_zero();
        }

        uint32_t kernel_subgroup_size = atoi(attrs.substr(it, it2).c_str());

        if (config.force_subgroup_size.set) {
            uint32_t force_subgroup_size = config.force_subgroup_size();
            if (force_subgroup_size != kernel_subgroup_size) {
                cvk_warn_fn("overriding subgroup size specified inside kernel "
                            "'%s', using '%u' instead of '%u'",
                            kernel.c_str(), force_subgroup_size,
                            kernel_subgroup_size);
            }
            return config.force_subgroup_size();
        }

        return kernel_subgroup_size;
    }

    const VkPipelineCache& pipeline_cache() const { return m_pipeline_cache; }

    CHECK_RETURN std::shared_ptr<cvk_entry_point>
    get_entry_point(std::string& name, cl_int* errcode_ret);

    bool create_module_constant_data_buffer() {
        cl_int err;
        if (m_binary.constant_data_buffer() != nullptr) {
            auto& init_data = m_binary.constant_data_buffer()->data;
            void* init_data_ptr =
                reinterpret_cast<void*>(const_cast<char*>(init_data.data()));
            m_module_constant_data_buffer =
                cvk_buffer::create(m_context, CL_MEM_COPY_HOST_PTR,
                                   init_data.size(), init_data_ptr, &err);
            if (m_module_constant_data_buffer == nullptr) {
                return false;
            }
        }
        return true;
    }

    const cvk_buffer* module_constant_data_buffer() const {
        return m_module_constant_data_buffer.get();
    }

    const constant_data_buffer_info* module_constant_data_buffer_info() const {
        return m_binary.constant_data_buffer();
    }

    const printf_buffer_desc_info& printf_buffer_info() const {
        return m_binary.printf_buffer_info();
    }

    bool options_allow_split_region(std::string options) {
        if (options.find("-uniform-workgroup-size") != std::string::npos)
            return false;
        return true;
    }

    bool can_split_region() {
        int status = options_allow_split_region(m_build_options);
#if COMPILER_AVAILABLE
        status &= options_allow_split_region(config.clspv_options);
#endif
        return status;
    }

    CHECK_RETURN cl_int parse_user_spec_constants();

    const std::string& kernel_attributes(const std::string& kernel_name) const {
        return m_binary.kernels_attributes().at(kernel_name);
    }

    uint32_t kernel_flags(const std::string& kernel) const {
        return m_binary.kernels_flags().at(kernel);
    }

    uint32_t workgroup_variables_size() const {
        return m_binary.get_workgroup_variables_size();
    }

private:
    void do_build();
    void do_build_in_separate_thread() {
        cvk_set_current_thread_name_if_supported("clvk-build");
        do_build();
    };
    std::string prepare_build_options(const cvk_device* device) const;
    CHECK_RETURN cl_build_status do_build_inner(const cvk_device* device);

#if COMPILER_AVAILABLE
#ifndef CLSPV_ONLINE_COMPILER
    CHECK_RETURN cl_build_status
    do_build_inner_offline(bool build_to_ir, bool build_from_il,
                           std::string& build_options, std::string& tmp_folder);
#else
    CHECK_RETURN cl_build_status do_build_inner_online(
        bool build_to_ir, bool build_from_il, std::string& build_options);
#endif
#endif

    void prepare_push_constant_range();

    /// Check if all of the capabilities required by the SPIR-V module are
    /// supported by `device`.
    CHECK_RETURN bool check_capabilities(const cvk_device* device);

    uint32_t m_num_devices;
    cl_uint m_num_input_programs;
    std::vector<const cvk_program*> m_input_programs;
    std::vector<const char*> m_header_include_names;
    build_operation m_operation;
    cl_program_binary_type m_binary_type;
    cvk_program_callback m_operation_callback;
    void* m_operation_callback_data;
    std::mutex m_lock;
    std::unique_ptr<std::thread> m_thread;
    std::string m_source;
    std::vector<uint8_t> m_ir;
    std::vector<uint8_t> m_il;
    VkShaderModule m_shader_module;
    std::unordered_map<const cvk_device*, std::atomic<cl_build_status>>
        m_dev_status;
    std::string m_build_options;
    spir_binary m_binary;
    std::string m_build_log;
    std::vector<cvk_sampler_holder> m_literal_samplers;
    VkPushConstantRange m_push_constant_range;
    std::unordered_map<std::string, std::shared_ptr<cvk_entry_point>>
        m_entry_points;
    std::vector<uint32_t> m_stripped_binary;
    VkPipelineCache m_pipeline_cache;
    std::unique_ptr<cvk_buffer> m_module_constant_data_buffer;
    std::unordered_map<uint32_t, user_spec_constant_data> m_user_spec_constants;
};

static inline cvk_program* icd_downcast(cl_program program) {
    return static_cast<cvk_program*>(program);
}

using cvk_program_holder = refcounted_holder<cvk_program>;
