/* Copyright (c) 2021-2025, Arm Limited and Contributors
 * Copyright (c) 2024-2025, NVIDIA CORPORATION. All rights reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * 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 "common/vk_common.h"
#include "core/debug.h"
#include "vulkan_type_mapping.h"

#include <utility>
#include <vulkan/vulkan.hpp>

namespace vkb
{
namespace core
{
template <vkb::BindingType bindingType>
class Device;
using DeviceC   = Device<vkb::BindingType::C>;
using DeviceCpp = Device<vkb::BindingType::Cpp>;

/// Inherit this for any Vulkan object with a handle of type `Handle`.
///
/// This allows the derived class to store a Vulkan handle, and also a pointer to the parent vkb::Device.
/// It also allows to set a debug name for any Vulkan object.
template <vkb::BindingType bindingType, typename Handle>
class VulkanResource
{
  public:
	// we always want to store a vk::Handle as a resource, so we have to figure out that type, depending on the BindingType!
	using ResourceType = typename vkb::VulkanTypeMapping<bindingType, Handle>::Type;

  public:
	using ObjectType = typename std::conditional<bindingType == vkb::BindingType::Cpp, vk::ObjectType, VkObjectType>::type;

	VulkanResource(Handle handle = nullptr, Device<bindingType> *device_ = nullptr);

	VulkanResource(const VulkanResource &)            = delete;
	VulkanResource &operator=(const VulkanResource &) = delete;

	VulkanResource(VulkanResource &&other);
	VulkanResource &operator=(VulkanResource &&other);

	virtual ~VulkanResource() = default;

	const std::string                    &get_debug_name() const;
	vkb::core::Device<bindingType>       &get_device();
	vkb::core::Device<bindingType> const &get_device() const;
	Handle                               &get_handle();
	const Handle                         &get_handle() const;
	uint64_t                              get_handle_u64() const;
	ObjectType                            get_object_type() const;
	ResourceType const                   &get_resource() const;
	bool                                  has_device() const;
	bool                                  has_handle() const;
	void                                  set_debug_name(const std::string &name);
	void                                  set_handle(Handle hdl);

  private:
	std::string           debug_name;
	vkb::core::DeviceCpp *device;
	ResourceType          handle;
};

template <typename Handle>
using VulkanResourceC = VulkanResource<vkb::BindingType::C, Handle>;
template <typename Handle>
using VulkanResourceCpp = VulkanResource<vkb::BindingType::Cpp, Handle>;

template <vkb::BindingType bindingType, typename Handle>
inline VulkanResource<bindingType, Handle>::VulkanResource(Handle handle_, Device<bindingType> *device_) :
    handle{handle_}
{
	if constexpr (bindingType == vkb::BindingType::Cpp)
	{
		device = device_;
	}
	else
	{
		device = reinterpret_cast<vkb::core::DeviceCpp *>(device_);
	}
}

template <vkb::BindingType bindingType, typename Handle>
inline VulkanResource<bindingType, Handle>::VulkanResource(VulkanResource &&other) :
    handle(std::exchange(other.handle, {})),
    device(std::exchange(other.device, {})),
    debug_name(std::exchange(other.debug_name, {}))
{}

template <vkb::BindingType bindingType, typename Handle>
inline VulkanResource<bindingType, Handle> &VulkanResource<bindingType, Handle>::operator=(VulkanResource &&other)
{
	handle     = std::exchange(other.handle, {});
	device     = std::exchange(other.device, {});
	debug_name = std::exchange(other.debug_name, {});
	return *this;
}

template <vkb::BindingType bindingType, typename Handle>
inline const std::string &VulkanResource<bindingType, Handle>::get_debug_name() const
{
	return debug_name;
}

template <vkb::BindingType bindingType, typename Handle>
inline Device<bindingType> &VulkanResource<bindingType, Handle>::get_device()
{
	assert(device && "VKBDevice handle not set");
	if constexpr (bindingType == vkb::BindingType::Cpp)
	{
		return *device;
	}
	else
	{
		return *reinterpret_cast<vkb::core::DeviceC *>(device);
	}
}

template <vkb::BindingType bindingType, typename Handle>
inline Device<bindingType> const &VulkanResource<bindingType, Handle>::get_device() const
{
	assert(device && "VKBDevice handle not set");
	if constexpr (bindingType == vkb::BindingType::Cpp)
	{
		return *device;
	}
	else
	{
		return *reinterpret_cast<vkb::core::DeviceC const *>(device);
	}
}

template <vkb::BindingType bindingType, typename Handle>
inline Handle &VulkanResource<bindingType, Handle>::get_handle()
{
	if constexpr (bindingType == vkb::BindingType::Cpp)
	{
		return handle;
	}
	else
	{
		return *reinterpret_cast<typename ResourceType::NativeType *>(&handle);
	}
}

template <vkb::BindingType bindingType, typename Handle>
inline const Handle &VulkanResource<bindingType, Handle>::get_handle() const
{
	if constexpr (bindingType == vkb::BindingType::Cpp)
	{
		return handle;
	}
	else
	{
		return *reinterpret_cast<typename ResourceType::NativeType const *>(&handle);
	}
}

template <vkb::BindingType bindingType, typename Handle>
inline uint64_t VulkanResource<bindingType, Handle>::get_handle_u64() const
{
	// See https://github.com/KhronosGroup/Vulkan-Docs/issues/368 .
	// Dispatchable and non-dispatchable handle types are *not* necessarily binary-compatible!
	// Non-dispatchable handles _might_ be only 32-bit long. This is because, on 32-bit machines, they might be a typedef to a 32-bit pointer.
	using UintHandle = typename std::conditional<sizeof(ResourceType) == sizeof(uint32_t), uint32_t, uint64_t>::type;

	return static_cast<uint64_t>(*reinterpret_cast<UintHandle const *>(&handle));
}

template <vkb::BindingType bindingType, typename Handle>
inline typename VulkanResource<bindingType, Handle>::ObjectType VulkanResource<bindingType, Handle>::get_object_type() const
{
	if constexpr (bindingType == vkb::BindingType::Cpp)
	{
		return ResourceType::objectType;
	}
	else
	{
		return static_cast<VkObjectType>(ResourceType::objectType);
	}
}

template <vkb::BindingType bindingType, typename Handle>
inline typename VulkanResource<bindingType, Handle>::ResourceType const &VulkanResource<bindingType, Handle>::get_resource() const
{
	return handle;
}

template <vkb::BindingType bindingType, typename Handle>
inline bool VulkanResource<bindingType, Handle>::has_device() const
{
	return device != nullptr;
}

template <vkb::BindingType bindingType, typename Handle>
inline bool VulkanResource<bindingType, Handle>::has_handle() const
{
	return handle != nullptr;
}

template <vkb::BindingType bindingType, typename Handle>
inline void VulkanResource<bindingType, Handle>::set_debug_name(const std::string &name)
{
	debug_name = name;

	if (device && !debug_name.empty())
	{
		get_device().get_debug_utils().set_debug_name(get_device().get_handle(), get_object_type(), get_handle_u64(), debug_name.c_str());
	}
}

template <vkb::BindingType bindingType, typename Handle>
inline void VulkanResource<bindingType, Handle>::set_handle(Handle hdl)
{
	handle = hdl;
}

}        // namespace core
}        // namespace vkb
