// 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 <memory>

#include "config.hpp"
#include "event.hpp"
#include "init.hpp"
#include "kernel.hpp"
#include "objects.hpp"
#include "printf.hpp"
#include "queue_controller.hpp"
#include "tracing.hpp"

struct cvk_command;
struct cvk_command_queue;
struct cvk_command_batch;
struct cvk_queue_controller;
using cvk_command_queue_holder = refcounted_holder<cvk_command_queue>;

struct cvk_command_group {
    std::deque<cvk_command*> commands;
    cl_int execute_cmds();
    void execute_cmds_in_executor();
};

struct cvk_executor_thread {

    cvk_executor_thread()
        : m_thread(nullptr), m_shutdown(false), m_running(false) {
        m_thread =
            std::make_unique<std::thread>(&cvk_executor_thread::executor, this);
    }

    void send_group(std::unique_ptr<cvk_command_group>&& group) {
        m_lock.lock();
        m_groups.push_back(std::move(group));
        m_cv.notify_one();
        m_running = true;
        m_lock.unlock();
    }

    bool is_idle() {
        std::unique_lock<std::mutex> lock(m_lock);
        return !m_running;
    }

    void shutdown() {

        // Tell the executor to shutdown
        m_lock.lock();
        m_shutdown = true;
        m_cv.notify_one();
        m_lock.unlock();

        // Wait for executor to shutdown
        if (m_thread != nullptr) {
            m_thread->join();
        }
    }

    cvk_command_group extract_cmds_required_by(bool only_non_batch_cmds,
                                               cl_uint num_events,
                                               _cl_event* const* event_list);

private:
    void executor();

    std::mutex m_lock;
    std::condition_variable m_cv;
    std::unique_ptr<std::thread> m_thread;
    bool m_shutdown;
    std::deque<std::unique_ptr<cvk_command_group>> m_groups;

    bool m_running;
};

struct cvk_command_pool {

    cvk_command_pool(cvk_device* device, uint32_t queue_family)
        : m_device(device), m_queue_family(queue_family),
          m_command_pool(VK_NULL_HANDLE) {}

    ~cvk_command_pool() {
        if (m_command_pool != VK_NULL_HANDLE) {
            vkDestroyCommandPool(m_device->vulkan_device(), m_command_pool,
                                 nullptr);
        }
    }

    CHECK_RETURN VkResult init() {
        VkCommandPoolCreateFlags flags = 0;
        if (m_device->is_driver_behavior_enabled(
                cvk_device::use_reset_command_buffer_bit)) {
            flags |= VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
        }

        // Create command pool
        VkCommandPoolCreateInfo createInfo = {
            VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, nullptr, flags,
            m_queue_family};

        return vkCreateCommandPool(m_device->vulkan_device(), &createInfo,
                                   nullptr, &m_command_pool);
    }

    VkResult allocate_command_buffer(VkCommandBuffer* buf);
    void free_command_buffer(VkCommandBuffer buf);

    void lock() { m_lock.lock(); }

    void unlock() { m_lock.unlock(); }

private:
    cvk_device* m_device;
    uint32_t m_queue_family;
    VkCommandPool m_command_pool;
    std::mutex m_lock;
};

struct cvk_command_queue : public _cl_command_queue,
                           api_object<object_magic::command_queue> {

    cvk_command_queue(cvk_context* ctx, cvk_device* dev,
                      cl_command_queue_properties props,
                      std::vector<cl_queue_properties>&& properties_array);

    CHECK_RETURN cl_int init();

    virtual ~cvk_command_queue();

    bool has_property(cl_command_queue_properties prop) const {
        return (m_properties & prop) == prop;
    }

    CHECK_RETURN cl_int enqueue_command_with_deps(cvk_command* cmd,
                                                  cl_uint num_dep_events,
                                                  _cl_event* const* dep_events,
                                                  _cl_event** event);
    CHECK_RETURN cl_int enqueue_command_with_deps(cvk_command* cmd,
                                                  bool blocking,
                                                  cl_uint num_dep_events,
                                                  _cl_event* const* dep_events,
                                                  _cl_event** event);

    CHECK_RETURN static cl_int wait_for_events(cl_uint num_events,
                                               const cl_event* event_list);
    CHECK_RETURN cl_int flush_no_lock();
    CHECK_RETURN cl_int flush();
    CHECK_RETURN cl_int finish();
    bool profiling_on_device() const {
        return m_device->has_timer_support() ||
               config.queue_profiling_use_timestamp_queries;
    }

    CHECK_RETURN bool allocate_command_buffer(VkCommandBuffer* cmdbuf) {
        return m_command_pool.allocate_command_buffer(cmdbuf) == VK_SUCCESS;
    }

    void free_command_buffer(VkCommandBuffer cmdbuf) {
        return m_command_pool.free_command_buffer(cmdbuf);
    }

    cvk_buffer* get_or_create_printf_buffer() {
        CVK_ASSERT(m_context != nullptr);
        if (!m_printf_buffer) {
            cl_int status;
            m_printf_buffer = cvk_buffer::create(
                context(), 0, m_context->get_printf_buffersize(), nullptr,
                &status);
            CVK_ASSERT(status == CL_SUCCESS);
        }
        return m_printf_buffer.get();
    }

    cvk_buffer* get_printf_buffer() {
        if (!m_printf_buffer) {
            return nullptr;
        }
        return m_printf_buffer.get();
    }

    cl_int reset_printf_buffer() {
        if (m_printf_buffer && m_printf_buffer->map_write_only()) {
            memset(m_printf_buffer->host_va(), 0, 4);
            m_printf_buffer->unmap_to_write(0, 4);
            return CL_SUCCESS;
        }
        cvk_error_fn("Could not reset printf buffer");
        return CL_OUT_OF_RESOURCES;
    }

    void command_pool_lock() { m_command_pool.lock(); }

    void command_pool_unlock() { m_command_pool.unlock(); }

    cvk_vulkan_queue_wrapper& vulkan_queue() { return m_vulkan_queue; }

    cvk_device* device() const { return m_device; }
    cl_command_queue_properties properties() const { return m_properties; }
    const std::vector<cl_queue_properties>& properties_array() const {
        return m_properties_array;
    }

    void batch_enqueued() {
        uint64_t batches = m_nb_batch_in_flight.fetch_add(1);
        TRACE_CNT(batch_in_flight_counter, batches + 1);
    }
    void batch_completed() {
        uint64_t batches = m_nb_batch_in_flight.fetch_sub(1);
        TRACE_CNT(batch_in_flight_counter, batches - 1);
    }

    void group_sent() {
        uint64_t group = m_nb_group_in_flight.fetch_add(1);
        TRACE_CNT(group_in_flight_counter, group + 1);
    }
    void group_completed() {
        uint64_t group = m_nb_group_in_flight.fetch_sub(1);
        TRACE_CNT(group_in_flight_counter, group - 1);
    }

    cl_int execute_cmds_required_by(cl_uint num_events,
                                    _cl_event* const* event_list);
    cl_int execute_cmds_required_by_no_lock(cl_uint num_events,
                                            _cl_event* const* event_list,
                                            std::unique_lock<std::mutex>& lock);

    void detach_from_context();

private:
    CHECK_RETURN cl_int satisfy_data_dependencies(cvk_command* cmd);
    void enqueue_command(cvk_command* cmd);
    CHECK_RETURN cl_int enqueue_command_with_retry(cvk_command*,
                                                   _cl_event** event);
    CHECK_RETURN cl_int enqueue_command(cvk_command* cmd, _cl_event** event);
    CHECK_RETURN cl_int end_current_command_batch(bool from_flush = false);
    void executor();

    cvk_device* m_device;
    cl_command_queue_properties m_properties;
    std::vector<cl_queue_properties> m_properties_array;

    cvk_executor_thread* m_executor;
    cvk_event_holder m_finish_event;

    std::mutex m_lock;
    std::deque<std::unique_ptr<cvk_command_group>> m_groups;

    cvk_command_batch* m_command_batch;

    cvk_vulkan_queue_wrapper& m_vulkan_queue;
    cvk_command_pool m_command_pool;

    cl_uint m_max_cmd_batch_size;
    cl_uint m_max_first_cmd_batch_size;
    cl_uint m_max_cmd_group_size;
    cl_uint m_max_first_cmd_group_size;

    std::atomic<uint64_t> m_nb_batch_in_flight;
    std::atomic<uint64_t> m_nb_group_in_flight;

    TRACE_CNT_VAR(batch_in_flight_counter);
    TRACE_CNT_VAR(group_in_flight_counter);

    std::unique_ptr<cvk_buffer> m_printf_buffer;

    std::vector<std::unique_ptr<cvk_queue_controller>> m_controllers;

    friend struct cvk_queue_controller;
    friend struct cvk_queue_controller_batch_parameters;
};

static inline cvk_command_queue* icd_downcast(cl_command_queue queue) {
    return static_cast<cvk_command_queue*>(queue);
}

struct cvk_command_pool_lock_holder {
    cvk_command_pool_lock_holder(cvk_command_queue* queue) : m_queue(queue) {
        m_queue->command_pool_lock();
    }
    ~cvk_command_pool_lock_holder() { m_queue->command_pool_unlock(); }

private:
    cvk_command_queue* m_queue;
};

struct cvk_executor_thread_pool {

    ~cvk_executor_thread_pool() {
        // Shutdown all executors
        for (auto& exec_state : m_executors) {
            auto exec = exec_state.first;
            exec->shutdown();
            delete exec;
        }
    }

    cvk_executor_thread* get_executor() {

        std::unique_lock<std::mutex> lock(m_lock);

        // Try to find a free executor
        for (auto& exec_state : m_executors) {
            if (exec_state.second == executor_state::free) {
                exec_state.second = executor_state::bound;
                auto exec = exec_state.first;
                if (!exec->is_idle()) {
                    continue;
                }
                return exec;
            }
        }

        // No free executor found in the pool, create a new one
        cvk_executor_thread* exec = new cvk_executor_thread();
        m_executors[exec] = executor_state::bound;
        return exec;
    }

    void return_executor(cvk_executor_thread* exec) {
        // No need to drain all commands here.
        // We will make sure the thread is idle before giving it to another
        // queue.
        //
        // Also trying to wait_idle here can produce a deadlock.
        // When a queue is released before the execution of all commands, the
        // queue will be destroyed in `cvk_executor_thread::executor` when
        // releasing the holder on the queue. But at that point, the executor
        // will have the lock. When calling wait_idle here, we will never be
        // able to take the lock, generating the deadlock.
        std::unique_lock<std::mutex> lock(m_lock);

        m_executors[exec] = executor_state::free;
    }

private:
    enum class executor_state
    {
        free,
        bound,
    };

    std::mutex m_lock;
    std::unordered_map<cvk_executor_thread*, executor_state> m_executors;
};

struct cvk_command_buffer {
    cvk_command_buffer(cvk_command_queue* queue)
        : m_queue(queue), m_command_buffer(VK_NULL_HANDLE) {}

    ~cvk_command_buffer() {
        if (m_command_buffer != VK_NULL_HANDLE) {
            m_queue->free_command_buffer(m_command_buffer);
        }
    }

    CHECK_RETURN bool begin();

    CHECK_RETURN bool end() {
        auto res = vkEndCommandBuffer(m_command_buffer);
        return res == VK_SUCCESS;
    }

    CHECK_RETURN bool submit_and_wait();

    operator VkCommandBuffer() { return m_command_buffer; }

protected:
    cvk_command_queue_holder m_queue;
    VkCommandBuffer m_command_buffer;
};

#define CLVK_COMMAND_BATCH 0x5000
#define CLVK_COMMAND_IMAGE_INIT 0x5001

struct cvk_command {

    cvk_command(cl_command_type type, cvk_command_queue* queue)
        : m_type(type), m_queue(queue),
          m_event(new cvk_event_command(m_queue->context(), this, queue)) {}

    virtual ~cvk_command() { m_event->release(); }

    void set_dependencies(cl_uint num_event_deps,
                          _cl_event* const* event_deps) {
        CVK_ASSERT(m_event_deps.size() == 0);

        for (cl_uint i = 0; i < num_event_deps; i++) {
            auto evt = icd_downcast(event_deps[i]);
            evt->retain();
            cvk_debug_fn("adding dep on event %p", evt);
            m_event_deps.push_back(evt);
        }
    }

    void set_dependencies(const std::vector<cvk_event*>& deps) {
        CVK_ASSERT(m_event_deps.size() == 0);
        m_event_deps = deps;
    }

    virtual bool can_be_batched() const { return false; }

    virtual bool is_built_before_enqueue() const { return true; }

    // Data movement commands are only created as part of the centralised
    // memory object asynchronous data consistency management code when
    // another non-data movement command has reported the memory object. They
    // never have data movement requirements of their own.
    virtual bool is_data_movement() const { return false; }

    void add_dependency(cvk_event* dep) {
        dep->retain();
        m_event_deps.push_back(dep);
    }

    CHECK_RETURN cl_int execute() {

        // First wait for dependencies
        cl_int status = CL_COMPLETE;
        for (auto& ev : m_event_deps) {
            if (ev->wait() != CL_COMPLETE) {
                status = CL_EXEC_STATUS_ERROR_FOR_EVENTS_IN_WAIT_LIST;
            }
            ev->release();
        }

        // Then execute the action if no dependencies failed
        if (status != CL_COMPLETE) {
            cvk_error_fn("one or more dependencies have failed for cmd %p (%s)",
                         this, cl_command_type_to_string(m_type));
        } else {
            set_event_status(CL_RUNNING);
            TRACE_BEGIN_CMD(m_type, "queue", (uintptr_t) & (*m_queue),
                            "command", (uintptr_t)this);
            status = do_action();
            TRACE_END();
        }

        // When executing batch with many commands, "set_event_status" can take
        // a while. Trace it to be able to understand it easily.
        TRACE_BEGIN("set_event_status");
        set_event_status(status);
        TRACE_END();
        return status;
    }

    cvk_event_command* event() const { return m_event; }

    cl_command_type type() const { return m_type; }

    cvk_command_queue* queue() const { return m_queue; }

    const std::vector<cvk_event*>& dependencies() const { return m_event_deps; }

    virtual const std::vector<cvk_mem*> memory_objects() const {
        CVK_ASSERT(false && "Should never be called");
        return {};
    }

    virtual void set_event_status(cl_int status) {
        m_event->set_status(status);
    }

    CHECK_RETURN virtual cl_int set_profiling_info(cl_profiling_info pinfo) {
        m_event->set_profiling_info_from_monotonic_clock(pinfo);
        return CL_SUCCESS;
    }

protected:
    cl_command_type m_type;
    cvk_command_queue_holder m_queue;
    cvk_event_command* m_event;

private:
    CHECK_RETURN virtual cl_int do_action() = 0;

    std::vector<cvk_event*> m_event_deps;
};

struct cvk_command_buffer_base : public cvk_command {

    cvk_command_buffer_base(cvk_command_queue* queue, cl_command_type type,
                            cvk_buffer* buffer)
        : cvk_command(type, queue), m_buffer(buffer) {}

protected:
    cvk_buffer_holder m_buffer;
};

struct cvk_command_buffer_base_region : public cvk_command_buffer_base {

    cvk_command_buffer_base_region(cvk_command_queue* queue,
                                   cl_command_type type, cvk_buffer* buffer,
                                   size_t offset, size_t size)
        : cvk_command_buffer_base(queue, type, buffer), m_offset(offset),
          m_size(size) {}

protected:
    size_t m_offset;
    size_t m_size;
};

struct cvk_command_buffer_host_copy final
    : public cvk_command_buffer_base_region {

    cvk_command_buffer_host_copy(cvk_command_queue* q, cl_command_type type,
                                 cvk_buffer* buffer, const void* ptr,
                                 size_t offset, size_t size)
        : cvk_command_buffer_base_region(q, type, buffer, offset, size),
          m_ptr(const_cast<void*>(ptr)) {}

    const std::vector<cvk_mem*> memory_objects() const override {
        return {m_buffer};
    }

private:
    CHECK_RETURN cl_int do_action() override final;

    void* m_ptr;
};

struct cvk_rectangle_copier {

    cvk_rectangle_copier(const size_t* a_origin, const size_t* b_origin,
                         const size_t* region, size_t a_row_pitch,
                         size_t a_slice_pitch, size_t b_row_pitch,
                         size_t b_slice_pitch, size_t elem_size)
        : m_elem_size(elem_size) {

        m_a_row_pitch = initialize_pitch(a_row_pitch, region[0]);
        m_b_row_pitch = initialize_pitch(b_row_pitch, region[0]);
        m_a_slice_pitch =
            initialize_pitch(a_slice_pitch, m_a_row_pitch * region[1]);
        m_b_slice_pitch =
            initialize_pitch(b_slice_pitch, m_b_row_pitch * region[1]);

        m_a_origin[0] = a_origin[0];
        m_a_origin[1] = a_origin[1];
        m_a_origin[2] = a_origin[2];

        m_b_origin[0] = b_origin[0];
        m_b_origin[1] = b_origin[1];
        m_b_origin[2] = b_origin[2];

        m_region[0] = region[0];
        m_region[1] = region[1];
        m_region[2] = region[2];

        cvk_debug_fn("region = {%zu,%zu,%zu}", m_region[0], m_region[1],
                     m_region[2]);
    }

    enum class direction
    {
        A_TO_B,
        B_TO_A,
    };

    void do_copy(direction dir, void* src_base, void* dst_base);

private:
    size_t initialize_pitch(size_t default_pitch, size_t region_pitch) {
        return default_pitch == 0 ? region_pitch : default_pitch;
    }

    std::array<size_t, 3> m_a_origin;
    size_t m_a_row_pitch;
    size_t m_a_slice_pitch;
    std::array<size_t, 3> m_b_origin;
    size_t m_b_row_pitch;
    size_t m_b_slice_pitch;
    std::array<size_t, 3> m_region;
    size_t m_elem_size;
};

struct cvk_command_copy_host_buffer_rect final : public cvk_command {

    cvk_command_copy_host_buffer_rect(
        cvk_command_queue* queue, cl_command_type type, cvk_buffer* buffer,
        void* hostptr, const size_t* host_origin, const size_t* buffer_origin,
        const size_t* region, size_t host_row_pitch, size_t host_slice_pitch,
        size_t buffer_row_pitch, size_t buffer_slice_pitch,
        size_t elem_size = 1)
        : cvk_command(type, queue),
          m_copier(buffer_origin, host_origin, region, buffer_row_pitch,
                   buffer_slice_pitch, host_row_pitch, host_slice_pitch,
                   elem_size),
          m_buffer(buffer), m_hostptr(hostptr) {}

    const std::vector<cvk_mem*> memory_objects() const override final {
        return {m_buffer};
    }

private:
    CHECK_RETURN cl_int do_action() override final;

    cvk_rectangle_copier m_copier;
    cvk_buffer_holder m_buffer;
    void* m_hostptr;
};

struct cvk_command_copy_buffer final : public cvk_command {

    cvk_command_copy_buffer(cvk_command_queue* q, cl_command_type type,
                            cvk_buffer* src, cvk_buffer* dst, size_t src_offset,
                            size_t dst_offset, size_t size)
        : cvk_command(type, q), m_src_buffer(src), m_dst_buffer(dst),
          m_src_offset(src_offset), m_dst_offset(dst_offset), m_size(size) {}

    const std::vector<cvk_mem*> memory_objects() const override {
        return {m_src_buffer, m_dst_buffer};
    }

private:
    CHECK_RETURN cl_int do_action() override final;

    cvk_buffer_holder m_src_buffer;
    cvk_buffer_holder m_dst_buffer;
    size_t m_src_offset;
    size_t m_dst_offset;
    size_t m_size;
};

struct cvk_command_copy_buffer_rect final : public cvk_command {
    cvk_command_copy_buffer_rect(cvk_command_queue* queue,
                                 cvk_buffer* src_buffer, cvk_buffer* dst_buffer,
                                 const size_t* src_origin,
                                 const size_t* dst_origin, const size_t* region,
                                 size_t src_row_pitch, size_t src_slice_pitch,
                                 size_t dst_row_pitch, size_t dst_slice_pitch)
        : cvk_command(CL_COMMAND_COPY_BUFFER_RECT, queue),
          m_copier(src_origin, dst_origin, region, src_row_pitch,
                   src_slice_pitch, dst_row_pitch, dst_slice_pitch, 1),
          m_src_buffer(src_buffer), m_dst_buffer(dst_buffer) {}

    const std::vector<cvk_mem*> memory_objects() const override {
        return {m_src_buffer, m_dst_buffer};
    }

private:
    CHECK_RETURN cl_int do_action() override final;

    cvk_rectangle_copier m_copier;
    cvk_buffer_holder m_src_buffer;
    cvk_buffer_holder m_dst_buffer;
};

struct cvk_command_fill_buffer final : public cvk_command_buffer_base_region {

    cvk_command_fill_buffer(cvk_command_queue* q, cvk_buffer* buffer,
                            size_t offset, size_t size, const void* pattern,
                            size_t pattern_size, cl_command_type type)
        : cvk_command_buffer_base_region(q, type, buffer, offset, size),
          m_pattern_size(pattern_size) {
        memcpy(m_pattern.data(), pattern, pattern_size);
    }

    const std::vector<cvk_mem*> memory_objects() const override {
        return {m_buffer};
    }

private:
    CHECK_RETURN cl_int do_action() override final;

    static constexpr int MAX_PATTERN_SIZE = 128;
    std::array<char, MAX_PATTERN_SIZE> m_pattern;
    size_t m_pattern_size;
};

struct cvk_command_batchable : public cvk_command {
    cvk_command_batchable(cl_command_type type, cvk_command_queue* queue)
        : cvk_command(type, queue), m_query_pool(VK_NULL_HANDLE) {}

    virtual ~cvk_command_batchable() {
        if (m_query_pool != VK_NULL_HANDLE) {
            auto vkdev = m_queue->device()->vulkan_device();
            vkDestroyQueryPool(vkdev, m_query_pool, nullptr);
        }
    }

    bool can_be_batched() const override;
    bool is_built_before_enqueue() const override final { return false; }

    CHECK_RETURN cl_int get_timestamp_query_results(cl_ulong* start,
                                                    cl_ulong* end);

    CHECK_RETURN cl_int build();
    CHECK_RETURN cl_int build(cvk_command_buffer& cmdbuf);
    CHECK_RETURN virtual cl_int
    build_batchable_inner(cvk_command_buffer& cmdbuf) = 0;

    CHECK_RETURN cl_int set_profiling_info_end() {
        // If it has already been set, don't override it
        if (m_event->get_profiling_info(CL_PROFILING_COMMAND_END) != 0) {
            return CL_SUCCESS;
        }
        cl_ulong start_dev, end_dev;
        cl_int perr = get_timestamp_query_results(&start_dev, &end_dev);
        if (perr != CL_COMPLETE) {
            return perr;
        }
        cl_ulong start_host, end_host;
        perr = m_queue->device()->device_timer_to_host(start_dev, start_host);
        if (perr != CL_SUCCESS) {
            return perr;
        }
        perr = m_queue->device()->device_timer_to_host(end_dev, end_host);
        if (perr != CL_SUCCESS) {
            return perr;
        }
        m_event->set_profiling_info(CL_PROFILING_COMMAND_START, start_host);
        m_event->set_profiling_info(CL_PROFILING_COMMAND_END, end_host);
        return CL_SUCCESS;
    }

    CHECK_RETURN cl_int
    set_profiling_info(cl_profiling_info pinfo) override final {
        if (!m_queue->profiling_on_device()) {
            return cvk_command::set_profiling_info(pinfo);
        }

        if (pinfo == CL_PROFILING_COMMAND_QUEUED ||
            pinfo == CL_PROFILING_COMMAND_SUBMIT) {
            return cvk_command::set_profiling_info(pinfo);
        } else if (pinfo == CL_PROFILING_COMMAND_START) {
            return m_queue->device()->update_device_host_timer();
        } else {
            CVK_ASSERT(pinfo == CL_PROFILING_COMMAND_END);
            return set_profiling_info_end();
        }
    }

private:
    CHECK_RETURN virtual cl_int do_post_action() { return CL_SUCCESS; }
    CHECK_RETURN cl_int do_action() override;

    std::unique_ptr<cvk_command_buffer> m_command_buffer;
    VkQueryPool m_query_pool;

    static const int NUM_POOL_QUERIES_PER_COMMAND = 2;
    static const int POOL_QUERY_CMD_START = 0;
    static const int POOL_QUERY_CMD_END = 1;
};

struct cvk_ndrange {

    cvk_ndrange() : offset({0}), gws({1, 1, 1}), lws({1, 1, 1}) {}

    cvk_ndrange(cl_uint work_dim, const size_t* global_work_offset,
                const size_t* global_work_size, const size_t* local_work_size)
        : cvk_ndrange() {
        for (cl_uint dim = 0; dim < work_dim; dim++) {
            if (global_work_offset != nullptr) {
                offset[dim] = global_work_offset[dim];
            }
            gws[dim] = global_work_size[dim];
            if (local_work_size != nullptr) {
                lws[dim] = local_work_size[dim];
            }
        }
    }

    bool is_uniform() const {
        for (cl_uint i = 0; i < 3; i++) {
            if (gws[i] % lws[i] != 0) {
                return false;
            }
        }
        return true;
    }

    std::array<uint32_t, 3> offset;
    std::array<uint32_t, 3> gws;
    std::array<uint32_t, 3> lws;
};

struct cvk_command_kernel final : public cvk_command_batchable {

    cvk_command_kernel(cvk_command_queue* q, cvk_kernel* kernel, uint32_t dims,
                       const cvk_ndrange& ndrange)
        : cvk_command_batchable(CL_COMMAND_NDRANGE_KERNEL, q), m_kernel(kernel),
          m_dimensions(dims), m_ndrange(ndrange), m_pipeline(VK_NULL_HANDLE),
          m_argument_values(nullptr) {}

    ~cvk_command_kernel() {
        if (m_argument_values) {
            m_argument_values->release_resources();
        }
    }

    CHECK_RETURN cl_int
    build_batchable_inner(cvk_command_buffer& cmdbuf) override final;

    bool can_be_batched() const override final {
        return !m_kernel->uses_printf() &&
               cvk_command_batchable::can_be_batched();
    }

    const std::vector<cvk_mem*> memory_objects() const override {
        std::vector<cvk_mem*> ret;
        std::shared_ptr<cvk_kernel_argument_values> argvals = m_argument_values;
        if (argvals == nullptr) {
            argvals = m_kernel->argument_values();
        }
        return argvals->memory_objects();
    }

private:
    CHECK_RETURN cl_int do_post_action() override final;

    CHECK_RETURN cl_int
    build_and_dispatch_regions(cvk_command_buffer& command_buffer);
    CHECK_RETURN cl_int
    update_global_push_constants(cvk_command_buffer& command_buffer);
    CHECK_RETURN cl_int dispatch_uniform_region_within_vklimits(
        const cvk_ndrange& region, cvk_command_buffer& command_buffer);
    CHECK_RETURN cl_int dispatch_uniform_region_iterate(
        unsigned int dim, const cvk_ndrange& region, const size_t* region_lws,
        size_t* region_gws, size_t* region_offset,
        cvk_command_buffer& command_buffer, uint32_t* num_workgroups);
    CHECK_RETURN cl_int dispatch_uniform_region(
        const cvk_ndrange& region, cvk_command_buffer& command_buffer);

    cvk_kernel_holder m_kernel;
    uint32_t m_dimensions;
    cvk_ndrange m_ndrange;
    VkPipeline m_pipeline;
    std::shared_ptr<cvk_kernel_argument_values> m_argument_values;
};

struct cvk_command_batch : public cvk_command {
    cvk_command_batch(cvk_command_queue* queue)
        : cvk_command(CLVK_COMMAND_BATCH, queue) {}

    cl_int add_command(cvk_command_batchable* cmd) {
        if (!m_command_buffer) {
            // Create command buffer and start recording on first call
            m_command_buffer = std::make_unique<cvk_command_buffer>(m_queue);
            if (!m_command_buffer->begin()) {
                return CL_OUT_OF_RESOURCES;
            }
        }
        cvk_command_pool_lock_holder lock(m_queue);

        cl_int ret = cmd->build(*m_command_buffer);
        if (ret != CL_SUCCESS) {
            return ret;
        }

        cvk_debug_fn("add command %p (%s) to batch %p", cmd,
                     cl_command_type_to_string(cmd->type()), this);
        m_commands.emplace_back(cmd);

        return ret;
    }

    CHECK_RETURN bool end() {
        cvk_command_pool_lock_holder lock(m_queue);
        return m_command_buffer->end();
    }

    cl_uint batch_size() { return m_commands.size(); }

    CHECK_RETURN cl_int
    set_profiling_info(cl_profiling_info pinfo) override final {
        cl_int status = cvk_command::set_profiling_info(pinfo);
        if (m_queue->profiling_on_device()) {
            if (pinfo == CL_PROFILING_COMMAND_START) {
                return status;
            } else {
                for (auto& cmd : m_commands) {
                    cl_int err;
                    if (pinfo == CL_PROFILING_COMMAND_END) {
                        err = cmd->set_profiling_info_end();
                    } else {
                        err = cmd->set_profiling_info(pinfo);
                    }
                    // do not stop at first error, but record only the first one
                    if (err != CL_SUCCESS && status == CL_SUCCESS) {
                        status = err;
                    }
                }
            }
        } else {
            for (auto& cmd : m_commands) {
                cmd->event()->copy_profiling_info(pinfo, m_event);
            }
        }
        return status;
    }

    void set_event_status(cl_int status) override final {
        m_event->set_status(status);
        for (auto& cmd : m_commands) {
            cmd->set_event_status(status);
        }
    }

private:
    CHECK_RETURN cl_int do_action() override final;

    std::vector<std::unique_ptr<cvk_command_batchable>> m_commands;
    std::unique_ptr<cvk_command_buffer> m_command_buffer;
};

struct cvk_command_map_buffer final : public cvk_command_buffer_base_region {

    cvk_command_map_buffer(cvk_command_queue* queue, cvk_buffer* buffer,
                           size_t offset, size_t size, cl_map_flags flags,
                           cl_command_type type, cvk_image* image = nullptr)
        : cvk_command_buffer_base_region(queue, type, buffer, offset, size),
          m_flags(flags), m_mapping_needs_releasing_on_destruction(false),
          m_image(image) {}
    ~cvk_command_map_buffer() {
        if (m_mapping_needs_releasing_on_destruction) {
            m_buffer->cleanup_mapping(m_mapping);
        }
    }
    CHECK_RETURN cl_int build(void** map_ptr);

    const std::vector<cvk_mem*> memory_objects() const override {
        return {m_buffer};
    }

private:
    CHECK_RETURN cl_int do_action() override final;

    cl_map_flags m_flags;
    cvk_buffer_mapping m_mapping;
    bool m_mapping_needs_releasing_on_destruction;
    cvk_image* m_image;
};

struct cvk_command_unmap_buffer final : public cvk_command_buffer_base {

    cvk_command_unmap_buffer(cvk_command_queue* queue, cvk_buffer* buffer,
                             void* map_ptr)
        : cvk_command_buffer_base(queue, CL_COMMAND_UNMAP_MEM_OBJECT, buffer),
          m_mapped_ptr(map_ptr) {}

    const std::vector<cvk_mem*> memory_objects() const override {
        return {m_buffer};
    }

private:
    CHECK_RETURN cl_int do_action() override final;

    void* m_mapped_ptr;
};

struct cvk_command_dep : public cvk_command {
    cvk_command_dep(cvk_command_queue* q, cl_command_type type)
        : cvk_command(type, q) {}

    const std::vector<cvk_mem*> memory_objects() const override { return {}; }

private:
    CHECK_RETURN cl_int do_action() override final { return CL_COMPLETE; }
};

struct cvk_command_buffer_image_copy final : public cvk_command_batchable {
    cvk_command_buffer_image_copy(cl_command_type type,
                                  cvk_command_queue* queue, cvk_buffer* buffer,
                                  cvk_image* image, size_t offset,
                                  const std::array<size_t, 3>& origin,
                                  const std::array<size_t, 3>& region)
        : cvk_command_batchable(type, queue), m_buffer(buffer), m_image(image),
          m_offset(offset), m_origin(origin), m_region(region),
          m_copy_type(type) {}

    cvk_command_buffer_image_copy(cl_command_type type,
                                  cl_command_type copy_type,
                                  cvk_command_queue* queue, cvk_buffer* buffer,
                                  cvk_image* image, size_t offset,
                                  const std::array<size_t, 3>& origin,
                                  const std::array<size_t, 3>& region)
        : cvk_command_batchable(type, queue), m_buffer(buffer), m_image(image),
          m_offset(offset), m_origin(origin), m_region(region),
          m_copy_type(copy_type) {}

    CHECK_RETURN cl_int
    build_batchable_inner(cvk_command_buffer& cmdbuf) override final;

    const std::vector<cvk_mem*> memory_objects() const override final {
        return {m_buffer, m_image};
    }

private:
    void build_inner_image_to_buffer(cvk_command_buffer& cmdbuf,
                                     const VkBufferImageCopy& region);
    void build_inner_buffer_to_image(cvk_command_buffer& cmdbuf,
                                     const VkBufferImageCopy& region);

    cvk_buffer_holder m_buffer;
    cvk_image_holder m_image;
    size_t m_offset;
    std::array<size_t, 3> m_origin;
    std::array<size_t, 3> m_region;
    cl_command_type m_copy_type;
};

struct cvk_command_map_image final : public cvk_command {
    // This command intend to manage the dependency with m_image initialization
    cvk_command_map_image(cvk_command_queue* q, cvk_image* img)
        : cvk_command(CL_COMMAND_MAP_IMAGE, q), m_image(img) {}

    const std::vector<cvk_mem*> memory_objects() const override {
        return {m_image};
    }

private:
    CHECK_RETURN cl_int do_action() override final { return CL_COMPLETE; };

    cvk_image_holder m_image;
};

struct cvk_command_unmap_image final : public cvk_command {

    cvk_command_unmap_image(cvk_command_queue* queue, cvk_image* image,
                            void* mapped_ptr)
        : cvk_command(CL_COMMAND_UNMAP_MEM_OBJECT, queue),
          m_mapped_ptr(mapped_ptr),
          m_image(image) { // FIXME crashes when the mapping doesn't exist
    }

    const std::vector<cvk_mem*> memory_objects() const override final {
        return {m_image};
    }

private:
    CHECK_RETURN cl_int do_action() override final;

    void* m_mapped_ptr;
    cvk_image_holder m_image;
};

struct cvk_command_image_image_copy final : public cvk_command_batchable {

    cvk_command_image_image_copy(cvk_command_queue* queue, cvk_image* src_image,
                                 cvk_image* dst_image,
                                 const std::array<size_t, 3>& src_origin,
                                 const std::array<size_t, 3>& dst_origin,
                                 const std::array<size_t, 3>& region)
        : cvk_command_batchable(CL_COMMAND_COPY_IMAGE, queue),
          m_src_image(src_image), m_dst_image(dst_image),
          m_src_origin(src_origin), m_dst_origin(dst_origin), m_region(region) {
    }
    CHECK_RETURN cl_int
    build_batchable_inner(cvk_command_buffer& cmdbuf) override final;

    const std::vector<cvk_mem*> memory_objects() const override final {
        return {m_src_image, m_dst_image};
    }

private:
    cvk_image_holder m_src_image;
    cvk_image_holder m_dst_image;
    std::array<size_t, 3> m_src_origin;
    std::array<size_t, 3> m_dst_origin;
    std::array<size_t, 3> m_region;
};

struct cvk_command_fill_image final : public cvk_command {

    cvk_command_fill_image(cvk_command_queue* queue, void* ptr,
                           const cvk_image::fill_pattern_array& pattern,
                           size_t pattern_size,
                           const std::array<size_t, 3>& region)
        : cvk_command(CL_COMMAND_FILL_IMAGE, queue), m_ptr(ptr),
          m_pattern(pattern), m_pattern_size(pattern_size), m_region(region) {}

    const std::vector<cvk_mem*> memory_objects() const override { return {}; }

private:
    CHECK_RETURN cl_int do_action() override final;

    void* m_ptr;
    cvk_image::fill_pattern_array m_pattern;
    size_t m_pattern_size;
    std::array<size_t, 3> m_region;
};

struct cvk_command_image_init final : public cvk_command_batchable {

    cvk_command_image_init(cvk_command_queue* queue, cvk_image* image)
        : cvk_command_batchable(CLVK_COMMAND_IMAGE_INIT, queue),
          m_image(image) {
        CVK_ASSERT(!m_image->is_backed_by_buffer_view());
    }
    bool is_data_movement() const override { return true; }
    CHECK_RETURN cl_int
    build_batchable_inner(cvk_command_buffer& cmdbuf) override final;
    ~cvk_command_image_init() { m_image->discard_init_data(); }

private:
    cvk_image_holder m_image;
};
