/*
 * Copyright © Microsoft Corporation
 *
 * 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 (including the next
 * paragraph) 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 "dzn_private.h"
#include "dzn_abi_helper.h"

#include "vk_alloc.h"
#include "vk_debug_report.h"
#include "vk_util.h"

#include "os_time.h"

static D3D12_QUERY_HEAP_TYPE
dzn_query_pool_get_heap_type(VkQueryType in)
{
   switch (in) {
   case VK_QUERY_TYPE_OCCLUSION: return D3D12_QUERY_HEAP_TYPE_OCCLUSION;
   case VK_QUERY_TYPE_PIPELINE_STATISTICS: return D3D12_QUERY_HEAP_TYPE_PIPELINE_STATISTICS;
   case VK_QUERY_TYPE_TIMESTAMP: return D3D12_QUERY_HEAP_TYPE_TIMESTAMP;
   default: unreachable("Unsupported query type");
   }
}

D3D12_QUERY_TYPE
dzn_query_pool_get_query_type(const struct dzn_query_pool *qpool,
                              VkQueryControlFlags flags)
{
   switch (qpool->heap_type) {
   case D3D12_QUERY_HEAP_TYPE_OCCLUSION:
      return flags & VK_QUERY_CONTROL_PRECISE_BIT ?
             D3D12_QUERY_TYPE_OCCLUSION : D3D12_QUERY_TYPE_BINARY_OCCLUSION;
   case D3D12_QUERY_HEAP_TYPE_PIPELINE_STATISTICS: return D3D12_QUERY_TYPE_PIPELINE_STATISTICS;
   case D3D12_QUERY_HEAP_TYPE_TIMESTAMP: return D3D12_QUERY_TYPE_TIMESTAMP;
   default: unreachable("Unsupported query type");
   }
}

static void
dzn_query_pool_destroy(struct dzn_query_pool *qpool,
                       const VkAllocationCallbacks *alloc)
{
   if (!qpool)
      return;

   struct dzn_device *device = container_of(qpool->base.device, struct dzn_device, vk);

   if (qpool->collect_map)
      ID3D12Resource_Unmap(qpool->collect_buffer, 0, NULL);

   if (qpool->collect_buffer)
      ID3D12Resource_Release(qpool->collect_buffer);

   if (qpool->resolve_buffer)
      ID3D12Resource_Release(qpool->resolve_buffer);

   if (qpool->heap)
      ID3D12QueryHeap_Release(qpool->heap);

   for (uint32_t q = 0; q < qpool->query_count; q++) {
      if (qpool->queries[q].fence)
         ID3D12Fence_Release(qpool->queries[q].fence);
   }

   mtx_destroy(&qpool->queries_lock);
   vk_object_base_finish(&qpool->base);
   vk_free2(&device->vk.alloc, alloc, qpool);
}

static VkResult
dzn_query_pool_create(struct dzn_device *device,
                      const VkQueryPoolCreateInfo *info,
                      const VkAllocationCallbacks *alloc,
                      VkQueryPool *out)
{
   VK_MULTIALLOC(ma);
   VK_MULTIALLOC_DECL(&ma, struct dzn_query_pool, qpool, 1);
   VK_MULTIALLOC_DECL(&ma, struct dzn_query, queries, info->queryCount);

   if (!vk_multialloc_zalloc2(&ma, &device->vk.alloc, alloc,
                              VK_SYSTEM_ALLOCATION_SCOPE_OBJECT))
      return vk_error(device, VK_ERROR_OUT_OF_HOST_MEMORY);

   vk_object_base_init(&device->vk, &qpool->base, VK_OBJECT_TYPE_QUERY_POOL);

   mtx_init(&qpool->queries_lock, mtx_plain);
   qpool->query_count = info->queryCount;
   qpool->queries = queries;

   D3D12_QUERY_HEAP_DESC desc = { 0 };
   qpool->heap_type = desc.Type = dzn_query_pool_get_heap_type(info->queryType);
   desc.Count = info->queryCount;
   desc.NodeMask = 0;

   HRESULT hres =
      ID3D12Device1_CreateQueryHeap(device->dev, &desc,
                                    &IID_ID3D12QueryHeap,
                                    (void **)&qpool->heap);
   if (FAILED(hres)) {
      dzn_query_pool_destroy(qpool, alloc);
      return vk_error(device, VK_ERROR_OUT_OF_DEVICE_MEMORY);
   }

   switch (info->queryType) {
   case VK_QUERY_TYPE_OCCLUSION:
   case VK_QUERY_TYPE_TIMESTAMP:
      qpool->query_size = sizeof(uint64_t);
      break;
   case VK_QUERY_TYPE_PIPELINE_STATISTICS:
      qpool->pipeline_statistics = info->pipelineStatistics;
      qpool->query_size = sizeof(D3D12_QUERY_DATA_PIPELINE_STATISTICS);
      break;
   default: unreachable("Unsupported query type");
   }

   D3D12_HEAP_PROPERTIES hprops =
      dzn_ID3D12Device2_GetCustomHeapProperties(device->dev, 0, D3D12_HEAP_TYPE_DEFAULT);
   D3D12_RESOURCE_DESC rdesc = {
      .Dimension = D3D12_RESOURCE_DIMENSION_BUFFER,
      .Alignment = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT,
      .Width = info->queryCount * qpool->query_size,
      .Height = 1,
      .DepthOrArraySize = 1,
      .MipLevels = 1,
      .Format = DXGI_FORMAT_UNKNOWN,
      .SampleDesc = { .Count = 1, .Quality = 0 },
      .Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
      .Flags = D3D12_RESOURCE_FLAG_NONE,
   };

   hres = ID3D12Device1_CreateCommittedResource(device->dev, &hprops,
                                                D3D12_HEAP_FLAG_NONE,
                                                &rdesc,
                                                D3D12_RESOURCE_STATE_COPY_DEST,
                                                NULL,
                                                &IID_ID3D12Resource,
                                                (void **)&qpool->resolve_buffer);
   if (FAILED(hres)) {
      dzn_query_pool_destroy(qpool, alloc);
      return vk_error(device, VK_ERROR_OUT_OF_DEVICE_MEMORY);
   }

   hprops = dzn_ID3D12Device2_GetCustomHeapProperties(device->dev, 0,
                                                      D3D12_HEAP_TYPE_READBACK);
   rdesc.Width = info->queryCount * (qpool->query_size + sizeof(uint64_t));
   hres = ID3D12Device1_CreateCommittedResource(device->dev, &hprops,
                                                D3D12_HEAP_FLAG_NONE,
                                                &rdesc,
                                                D3D12_RESOURCE_STATE_COPY_DEST,
                                                NULL,
                                                &IID_ID3D12Resource,
                                                (void **)&qpool->collect_buffer);
   if (FAILED(hres)) {
      dzn_query_pool_destroy(qpool, alloc);
      return vk_error(device, VK_ERROR_OUT_OF_DEVICE_MEMORY);
   }

   hres = ID3D12Resource_Map(qpool->collect_buffer, 0, NULL, (void **)&qpool->collect_map);
   if (FAILED(hres)) {
      dzn_query_pool_destroy(qpool, alloc);
      return vk_error(device, VK_ERROR_OUT_OF_HOST_MEMORY);
   }

   memset(qpool->collect_map, 0, rdesc.Width);

   *out = dzn_query_pool_to_handle(qpool);
   return VK_SUCCESS;
}

uint32_t
dzn_query_pool_get_result_offset(const struct dzn_query_pool *qpool, uint32_t query)
{
   return query * qpool->query_size;
}

uint32_t
dzn_query_pool_get_result_size(const struct dzn_query_pool *qpool, uint32_t query_count)
{
   return query_count * qpool->query_size;
}

uint32_t
dzn_query_pool_get_availability_offset(const struct dzn_query_pool *qpool, uint32_t query)
{
   return (qpool->query_count * qpool->query_size) + (sizeof(uint64_t) * query);
}

VKAPI_ATTR VkResult VKAPI_CALL
dzn_CreateQueryPool(VkDevice device,
                    const VkQueryPoolCreateInfo *pCreateInfo,
                    const VkAllocationCallbacks *pAllocator,
                    VkQueryPool *pQueryPool)
{
   return dzn_query_pool_create(dzn_device_from_handle(device),
                                pCreateInfo, pAllocator, pQueryPool);
}

VKAPI_ATTR void VKAPI_CALL
dzn_DestroyQueryPool(VkDevice device,
                     VkQueryPool queryPool,
                     const VkAllocationCallbacks *pAllocator)
{
   dzn_query_pool_destroy(dzn_query_pool_from_handle(queryPool), pAllocator);
}

VKAPI_ATTR void VKAPI_CALL
dzn_ResetQueryPool(VkDevice device,
                   VkQueryPool queryPool,
                   uint32_t firstQuery,
                   uint32_t queryCount)
{
   VK_FROM_HANDLE(dzn_query_pool, qpool, queryPool);

   mtx_lock(&qpool->queries_lock);
   for (uint32_t q = 0; q < queryCount; q++) {
      struct dzn_query *query = &qpool->queries[firstQuery + q];

      query->fence_value = 0;
      if (query->fence) {
         ID3D12Fence_Release(query->fence);
         query->fence = NULL;
      }
   }
   mtx_lock(&qpool->queries_lock);

   memset((uint8_t *)qpool->collect_map + dzn_query_pool_get_result_offset(qpool, firstQuery),
          0, queryCount * qpool->query_size);
   memset((uint8_t *)qpool->collect_map + dzn_query_pool_get_availability_offset(qpool, firstQuery),
          0, queryCount * sizeof(uint64_t));
}

VKAPI_ATTR VkResult VKAPI_CALL
dzn_GetQueryPoolResults(VkDevice device,
                        VkQueryPool queryPool,
                        uint32_t firstQuery,
                        uint32_t queryCount,
                        size_t dataSize,
                        void *pData,
                        VkDeviceSize stride,
                        VkQueryResultFlags flags)
{
   VK_FROM_HANDLE(dzn_query_pool, qpool, queryPool);

   uint32_t step = (flags & VK_QUERY_RESULT_64_BIT) ?
                   sizeof(uint64_t) : sizeof(uint32_t);
   VkResult result = VK_SUCCESS;

   for (uint32_t q = 0; q < queryCount; q++) {
      struct dzn_query *query = &qpool->queries[q + firstQuery];

      uint8_t *dst_ptr = (uint8_t *)pData + (stride * q);
      uint8_t *src_ptr =
         (uint8_t *)qpool->collect_map +
         dzn_query_pool_get_result_offset(qpool, firstQuery + q);
      uint64_t available = 0;

      if (flags & VK_QUERY_RESULT_WAIT_BIT) {
         ID3D12Fence *query_fence = NULL;
         uint64_t query_fence_val = 0;

         while (true) {
            mtx_lock(&qpool->queries_lock);
            if (query->fence) {
               query_fence = query->fence;
               ID3D12Fence_AddRef(query_fence);
            }
            query_fence_val = query->fence_value;
            mtx_unlock(&qpool->queries_lock);

            if (query_fence)
               break;

            /* Check again in 10ms.
             * FIXME: decrease the polling period if it happens to hurt latency.
             */
            os_time_sleep(10 * 1000);
         }

         ID3D12Fence_SetEventOnCompletion(query_fence, query_fence_val, NULL);
         ID3D12Fence_Release(query_fence);
         available = UINT64_MAX;
      } else {
         ID3D12Fence *query_fence = NULL;
         mtx_lock(&qpool->queries_lock);
         if (query->fence) {
            query_fence = query->fence;
            ID3D12Fence_AddRef(query_fence);
         }
         uint64_t query_fence_val = query->fence_value;
         mtx_unlock(&qpool->queries_lock);

         if (query_fence) {
            if (ID3D12Fence_GetCompletedValue(query_fence) >= query_fence_val)
               available = UINT64_MAX;
            ID3D12Fence_Release(query_fence);
         }
      }

      if (qpool->heap_type != D3D12_QUERY_HEAP_TYPE_PIPELINE_STATISTICS) {
         if (available)
            memcpy(dst_ptr, src_ptr, step);
         else if (flags & VK_QUERY_RESULT_PARTIAL_BIT)
            memset(dst_ptr, 0, step);

         dst_ptr += step;
      } else {
         for (uint32_t c = 0; c < sizeof(D3D12_QUERY_DATA_PIPELINE_STATISTICS) / sizeof(uint64_t); c++) {
            if (!(BITFIELD_BIT(c) & qpool->pipeline_statistics))
               continue;

            if (available)
               memcpy(dst_ptr, src_ptr + (c * sizeof(uint64_t)), step);
            else if (flags & VK_QUERY_RESULT_PARTIAL_BIT)
               memset(dst_ptr, 0, step);

            dst_ptr += step;
         }
      }

      if (flags & VK_QUERY_RESULT_WITH_AVAILABILITY_BIT)
         memcpy(dst_ptr, &available, step);

      if (!available && !(flags & VK_QUERY_RESULT_PARTIAL_BIT))
         result = VK_NOT_READY;
   }

   return result;
}
