// Copyright 2020 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.

#pragma once

#include <memory>
#include <unordered_map>
#include <vector>
#include <queue>

#include "common/bit_field.h"
#include "common/common_types.h"
#include "video_core/command_classes/sync_manager.h"

namespace Tegra {

class GPU;
class Nvdec;
class Vic;
class Host1x;

enum class ChSubmissionMode : u32 {
    SetClass = 0,
    Incrementing = 1,
    NonIncrementing = 2,
    Mask = 3,
    Immediate = 4,
    Restart = 5,
    Gather = 6,
};

enum class ChClassId : u32 {
    NoClass = 0x0,
    Host1x = 0x1,
    VideoEncodeMpeg = 0x20,
    VideoEncodeNvEnc = 0x21,
    VideoStreamingVi = 0x30,
    VideoStreamingIsp = 0x32,
    VideoStreamingIspB = 0x34,
    VideoStreamingViI2c = 0x36,
    GraphicsVic = 0x5d,
    Graphics3D = 0x60,
    GraphicsGpu = 0x61,
    Tsec = 0xe0,
    TsecB = 0xe1,
    NvJpg = 0xc0,
    NvDec = 0xf0
};

enum class ChMethod : u32 {
    Empty = 0,
    SetMethod = 0x10,
    SetData = 0x11,
};

union ChCommandHeader {
    u32 raw;
    BitField<0, 16, u32> value;
    BitField<16, 12, ChMethod> method_offset;
    BitField<28, 4, ChSubmissionMode> submission_mode;
};
static_assert(sizeof(ChCommandHeader) == sizeof(u32), "ChCommand header is an invalid size");

struct ChCommand {
    ChClassId class_id{};
    int method_offset{};
    std::vector<u32> arguments;
};

using ChCommandHeaderList = std::vector<Tegra::ChCommandHeader>;
using ChCommandList = std::vector<Tegra::ChCommand>;

struct ThiRegisters {
    u32_le increment_syncpt{};
    INSERT_PADDING_WORDS(1);
    u32_le increment_syncpt_error{};
    u32_le ctx_switch_incremement_syncpt{};
    INSERT_PADDING_WORDS(4);
    u32_le ctx_switch{};
    INSERT_PADDING_WORDS(1);
    u32_le ctx_syncpt_eof{};
    INSERT_PADDING_WORDS(5);
    u32_le method_0{};
    u32_le method_1{};
    INSERT_PADDING_WORDS(12);
    u32_le int_status{};
    u32_le int_mask{};
};

enum class ThiMethod : u32 {
    IncSyncpt = offsetof(ThiRegisters, increment_syncpt) / sizeof(u32),
    SetMethod0 = offsetof(ThiRegisters, method_0) / sizeof(u32),
    SetMethod1 = offsetof(ThiRegisters, method_1) / sizeof(u32),
};

class CDmaPusher {
public:
    explicit CDmaPusher(GPU& gpu);
    ~CDmaPusher();

    /// Push NVDEC command buffer entries into queue
    void Push(ChCommandHeaderList&& entries);

    /// Process queued command buffer entries
    void DispatchCalls();

    /// Process one queue element
    void Step();

    /// Invoke command class devices to execute the command based on the current state
    void ExecuteCommand(u32 offset, u32 data);

private:
    /// Write arguments value to the ThiRegisters member at the specified offset
    void ThiStateWrite(ThiRegisters& state, u32 offset, const std::vector<u32>& arguments);

    GPU& gpu;

    std::shared_ptr<Tegra::Nvdec> nvdec_processor;
    std::unique_ptr<Tegra::Vic> vic_processor;
    std::unique_ptr<Tegra::Host1x> host1x_processor;
    std::unique_ptr<SyncptIncrManager> nvdec_sync;
    std::unique_ptr<SyncptIncrManager> vic_sync;
    ChClassId current_class{};
    ThiRegisters vic_thi_state{};
    ThiRegisters nvdec_thi_state{};

    s32 count{};
    s32 offset{};
    s32 mask{};
    bool incrementing{};

    // Queue of command lists to be processed
    std::queue<ChCommandHeaderList> cdma_queue;
};

} // namespace Tegra
